Compare commits

..

No commits in common. "3.0.0" and "2" have entirely different histories.
3.0.0 ... 2

11 changed files with 118 additions and 57 deletions

View File

@ -12,19 +12,14 @@ jobs:
with:
# set phpunit to false to prevent automatic generation of mysql phpunit jobs
phpunit: false
preserve_vendor_tests: true
extra_jobs: |
- php: 8.1
- php: 7.4
db: pgsql
phpunit: true
composer_args: --prefer-lowest
phpunit_suite: all
- php: 8.0
db: pgsql
phpunit: true
- php: 8.1
db: pgsql
phpunit: true
phpunit_suite: all
- php: 8.2
db: pgsql
phpunit: true
phpunit_suite: all

View File

@ -3,9 +3,15 @@
[![CI](https://github.com/silverstripe/silverstripe-postgresql/actions/workflows/ci.yml/badge.svg)](https://github.com/silverstripe/silverstripe-postgresql/actions/workflows/ci.yml)
[![Silverstripe supported module](https://img.shields.io/badge/silverstripe-supported-0071C4.svg)](https://www.silverstripe.org/software/addons/silverstripe-commercially-supported-module-list/)
## Requirements
* Silverstripe 4.0
* PostgreSQL >=9.2
* Note: PostgreSQL 10 has not been tested
## Installation
```sh
```
composer require silverstripe/postgresql
```
@ -21,7 +27,7 @@ SS_DATABASE_USERNAME=
SS_DATABASE_PASSWORD=
```
See [environment variables](https://docs.silverstripe.org/en/getting_started/environment_management) for more details. Note that a database will automatically be created via `dev/build`.
See [environment variables](https://docs.silverstripe.org/en/4/getting_started/environment_management) for more details. Note that a database will automatically be created via `dev/build`.
### Through the installer

View File

@ -2,6 +2,12 @@
name: postgresqlconnectors
---
SilverStripe\Core\Injector\Injector:
PostgrePDODatabase:
class: 'SilverStripe\PostgreSQL\PostgreSQLDatabase'
properties:
connector: '%$PDOConnector'
schemaManager: '%$PostgreSQLSchemaManager'
queryBuilder: '%$PostgreSQLQueryBuilder'
PostgreSQLDatabase:
class: 'SilverStripe\PostgreSQL\PostgreSQLDatabase'
properties:

View File

@ -3,6 +3,23 @@
use SilverStripe\Dev\Install\DatabaseAdapterRegistry;
use SilverStripe\PostgreSQL\PostgreSQLDatabaseConfigurationHelper;
// PDO Postgre database
DatabaseAdapterRegistry::register(array(
/** @skipUpgrade */
'class' => 'PostgrePDODatabase',
'module' => 'postgresql',
'title' => 'PostgreSQL 8.3+ (using PDO)',
'helperPath' => __DIR__.'/code/PostgreSQLDatabaseConfigurationHelper.php',
'helperClass' => PostgreSQLDatabaseConfigurationHelper::class,
'supported' => (class_exists('PDO') && in_array('postgresql', PDO::getAvailableDrivers())),
'missingExtensionText' =>
'Either the <a href="http://www.php.net/manual/en/book.pdo.php">PDO Extension</a> or
the <a href="http://www.php.net/manual/en/ref.pdo-sqlsrv.php">SQL Server PDO Driver</a>
are unavailable. Please install or enable these and refresh this page.'
));
// PDO Postgre database
DatabaseAdapterRegistry::register(array(
/** @skipUpgrade */
'class' => 'PostgreSQLDatabase',

View File

@ -11,6 +11,9 @@ use ErrorException;
* The connector doesn't know anything about schema selection, so code related to
* masking multiple databases as schemas should be handled in the database controller
* and schema manager.
*
* @package sapphire
* @subpackage model
*/
class PostgreSQLConnector extends DBConnector
{
@ -72,7 +75,7 @@ class PostgreSQLConnector extends DBConnector
// Note: Postgres always behaves as though $selectDB = true, ignoring
// any value actually passed in. The controller passes in true for other
// connectors
// connectors such as PDOConnector.
// Escape parameters
$arguments = array(
@ -113,7 +116,8 @@ class PostgreSQLConnector extends DBConnector
public function getGeneratedID($table)
{
return $this->query("SELECT currval(pg_get_serial_sequence('\"{$table}\"','ID'))")->value();
$result = $this->query("SELECT currval('\"{$table}_ID_seq\"')")->first();
return $result['currval'];
}
public function getLastError()
@ -218,6 +222,7 @@ class PostgreSQLConnector extends DBConnector
// Execute query
// Unfortunately error-suppression is required in order to handle sql errors elegantly.
// Please use PDO if you can help it
if (!empty($parameters)) {
$result = @pg_query_params($this->dbConn, $sql, $parameters);
} else {

View File

@ -214,7 +214,7 @@ class PostgreSQLDatabase extends Database
}
$this->schemaOriginal = $parameters['schema'];
// Ensure that driver is available
// Ensure that driver is available (required by PDO)
if (empty($parameters['driver'])) {
$parameters['driver'] = $this->getDatabaseServer();
}
@ -574,14 +574,13 @@ class PostgreSQLDatabase extends Database
return $this->transactionNesting;
}
public function transactionEnd($chain = false): ?bool
public function transactionEnd($chain = false)
{
--$this->transactionNesting;
if ($this->transactionNesting <= 0) {
$this->transactionNesting = 0;
$this->query('COMMIT;');
}
return null;
}
public function comparisonClause($field, $value, $exact = false, $negate = false, $caseSensitive = null, $parameterised = false)

View File

@ -5,6 +5,7 @@ namespace SilverStripe\PostgreSQL;
use SilverStripe\Dev\Install\DatabaseAdapterRegistry;
use SilverStripe\Dev\Install\DatabaseConfigurationHelper;
use Exception;
use PDO;
/**
* This is a helper class for the SS installer.
@ -39,6 +40,10 @@ class PostgreSQLDatabaseConfigurationHelper implements DatabaseConfigurationHelp
$connstring = "host=$server port=5432 dbname=postgres{$userPart}{$passwordPart}";
$conn = pg_connect($connstring);
break;
case 'PostgrePDODatabase':
// May throw a PDOException if fails
$conn = @new PDO('postgresql:host='.$server.';dbname=postgres;port=5432', $username, $password);
break;
default:
$error = 'Invalid connection type: ' . $databaseConfig['type'];
return null;
@ -87,6 +92,8 @@ class PostgreSQLDatabaseConfigurationHelper implements DatabaseConfigurationHelp
$conn = $this->createConnection($databaseConfig, $error);
if (!$conn) {
return false;
} elseif ($conn instanceof PDO) {
return $conn->getAttribute(PDO::ATTR_SERVER_VERSION);
} elseif (is_resource($conn)) {
$info = pg_version($conn);
return $info['server'];
@ -132,7 +139,11 @@ class PostgreSQLDatabaseConfigurationHelper implements DatabaseConfigurationHelp
protected function query($conn, $sql)
{
$items = array();
if (is_resource($conn)) {
if ($conn instanceof PDO) {
foreach ($conn->query($sql) as $row) {
$items[] = $row[0];
}
} elseif (is_resource($conn)) {
$result = pg_query($conn, $sql);
while ($row = pg_fetch_row($result)) {
$items[] = $row[0];

View File

@ -2,7 +2,6 @@
namespace SilverStripe\PostgreSQL;
use Iterator;
use SilverStripe\ORM\Connect\Query;
/**
@ -57,13 +56,12 @@ class PostgreSQLQuery extends Query
}
}
public function getIterator(): Iterator
public function seek($row)
{
while ($row = pg_fetch_array($this->handle, null, PGSQL_NUM)) {
yield $this->parseResult($row);
}
// Reset so the query can be iterated over again
pg_result_seek($this->handle, 0);
// Specifying the zero-th record here will reset the pointer
$result = pg_fetch_array($this->handle, $row, PGSQL_NUM);
return $this->parseResult($result);
}
public function numRecords()
@ -71,6 +69,18 @@ class PostgreSQLQuery extends Query
return pg_num_rows($this->handle);
}
public function nextRecord()
{
$row = pg_fetch_array($this->handle, null, PGSQL_NUM);
// Correct non-string types
if ($row) {
return $this->parseResult($row);
}
return false;
}
/**
* @param array $row
* @return array

View File

@ -79,7 +79,7 @@ class PostgreSQLSchemaManager extends DBSchemaManager
public function postgresDatabaseExists($name)
{
$result = $this->preparedQuery("SELECT datname FROM pg_database WHERE datname = ?;", array($name));
return $result->value() ? true : false;
return $result->first() ? true : false;
}
public function databaseExists($name)
@ -146,7 +146,7 @@ class PostgreSQLSchemaManager extends DBSchemaManager
return $this->preparedQuery(
"SELECT nspname FROM pg_catalog.pg_namespace WHERE nspname = ?;",
array($name)
)->value() ? true : false;
)->first() ? true : false;
}
/**
@ -462,18 +462,18 @@ class PostgreSQLSchemaManager extends DBSchemaManager
$stats = $this->preparedQuery(
"SELECT relid FROM pg_stat_user_tables WHERE relname = ?;",
array($table)
)->record();
)->first();
$oid = $stats['relid'];
//Now we can run a long query to get the clustered status:
//If anyone knows a better way to get the clustered status, then feel free to replace this!
$clustered = $this->preparedQuery(
"
SELECT c2.relname, i.indisclustered
SELECT c2.relname, i.indisclustered
FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i
WHERE c.oid = ? AND c.oid = i.indrelid AND i.indexrelid = c2.oid AND indisclustered='t';",
array($oid)
)->value();
)->first();
if ($clustered) {
$this->query("ALTER TABLE \"$table\" SET WITHOUT CLUSTER;");
@ -830,9 +830,9 @@ class PostgreSQLSchemaManager extends DBSchemaManager
protected function extractTriggerColumns($triggerName, $table)
{
$trigger = $this->preparedQuery(
"SELECT t.tgargs
"SELECT t.tgargs
FROM pg_catalog.pg_trigger t
INNER JOIN pg_catalog.pg_class c ON c.oid = t.tgrelid
INNER JOIN pg_catalog.pg_class c ON c.oid = t.tgrelid
INNER JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relname = ?
AND n.nspname = ?
@ -842,25 +842,30 @@ class PostgreSQLSchemaManager extends DBSchemaManager
$this->database->currentSchema(),
$triggerName
]
)->record();
)->first();
// Convert stream to string
if (is_resource($trigger['tgargs'])) {
$trigger['tgargs'] = stream_get_contents($trigger['tgargs']);
}
// hex-encoded (pg_sql non-pdo)
$bytes = str_split($trigger['tgargs'], 2);
$argList = array();
$nextArg = "";
foreach ($bytes as $byte) {
if ($byte == '\x') {
continue;
} elseif ($byte == "00") {
$argList[] = $nextArg;
$nextArg = "";
} else {
$nextArg .= chr(hexdec($byte));
if (strpos($trigger['tgargs'], "\000") !== false) {
// Option 1: output as a string (PDO)
$argList = array_filter(explode("\000", $trigger['tgargs']));
} else {
// Option 2: hex-encoded (pg_sql non-pdo)
$bytes = str_split($trigger['tgargs'], 2);
$argList = array();
$nextArg = "";
foreach ($bytes as $byte) {
if ($byte == '\x') {
continue;
} elseif ($byte == "00") {
$argList[] = $nextArg;
$nextArg = "";
} else {
$nextArg .= chr(hexdec($byte));
}
}
}
@ -963,7 +968,7 @@ class PostgreSQLSchemaManager extends DBSchemaManager
WHERE r.contype = 'c' AND conname = ? AND n.nspname = ?
ORDER BY 1;",
array($constraint, $this->database->currentSchema())
)->record();
)->first();
if (!$cache) {
return $value;
}
@ -1043,7 +1048,7 @@ class PostgreSQLSchemaManager extends DBSchemaManager
FROM information_schema.triggers
WHERE trigger_name = ? AND trigger_schema = ?;",
array($triggerName, $this->database->currentSchema())
)->value();
)->first();
if ($exists) {
$this->query("DROP trigger IF EXISTS $triggerName ON \"$tableName\";");
}
@ -1359,7 +1364,7 @@ class PostgreSQLSchemaManager extends DBSchemaManager
$existing = $this->preparedQuery(
"SELECT spcname, spclocation FROM pg_tablespace WHERE spcname = ?;",
array($name)
)->record();
)->first();
//NOTE: this location must be empty for this to work
//We can't seem to change the location of the tablespace through any ALTER commands :(
@ -1484,7 +1489,7 @@ class PostgreSQLSchemaManager extends DBSchemaManager
$result = $this->preparedQuery(
"SELECT lanname FROM pg_language WHERE lanname = ?;",
array($language)
)->value();
)->first();
if (!$result) {
$this->query("CREATE LANGUAGE $language;");

View File

@ -15,9 +15,8 @@
}
],
"require": {
"silverstripe/framework": "^5",
"silverstripe/vendor-plugin": "^2",
"ext-pgsql": "*"
"silverstripe/framework": "^4",
"silverstripe/vendor-plugin": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5",

View File

@ -1,9 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/silverstripe/framework/tests/bootstrap.php" colors="true">
<testsuites>
<testsuite name="postgresql">
<directory>tests</directory>
<directory>vendor/silverstripe/framework/tests/php</directory>
</testsuite>
</testsuites>
<testsuite name="Default">
<directory>tests</directory>
</testsuite>
<filter>
<whitelist addUncoveredFilesFromWhitelist="true">
<directory suffix=".php">.</directory>
<exclude>
<directory suffix=".php">tests/</directory>
</exclude>
</whitelist>
</filter>
</phpunit>