BUG Fix postgres + PDO not working

BUG Fix empty enums
This commit is contained in:
Damian Mooyman 2017-11-17 10:58:14 +13:00
parent 8c5f95fdaa
commit 685e33cf84
No known key found for this signature in database
GPG Key ID: 78B823A10DE27D1A
11 changed files with 332 additions and 266 deletions

View File

@ -10,8 +10,15 @@ indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[{*.yml,*.json}]
[*.md]
trim_trailing_whitespace = false
[*.{yml,js,json,css,scss,feature}]
indent_size = 2
indent_style = space
[composer.json]
indent_size = 4
# The indent size used in the package.json file cannot be changed:
# https://github.com/npm/npm/pull/3180#issuecomment-16336516

View File

@ -1,69 +1,15 @@
inherit: true
build:
nodes:
analysis:
tests:
override: [php-scrutinizer-run]
checks:
php:
verify_property_names: true
verify_argument_usable_as_reference: true
verify_access_scope_valid: true
useless_calls: true
use_statement_alias_conflict: true
variable_existence: true
unused_variables: true
unused_properties: true
unused_parameters: true
unused_methods: true
unreachable_code: true
too_many_arguments: true
sql_injection_vulnerabilities: true
simplify_boolean_return: true
side_effects_or_types: true
security_vulnerabilities: true
return_doc_comments: true
return_doc_comment_if_not_inferrable: true
require_scope_for_properties: true
require_scope_for_methods: true
require_php_tag_first: true
psr2_switch_declaration: true
psr2_class_declaration: true
property_assignments: true
prefer_while_loop_over_for_loop: true
precedence_mistakes: true
precedence_in_conditions: true
phpunit_assertions: true
php5_style_constructor: true
parse_doc_comments: true
parameter_non_unique: true
parameter_doc_comments: true
param_doc_comment_if_not_inferrable: true
optional_parameters_at_the_end: true
one_class_per_file: true
no_unnecessary_if: true
no_trailing_whitespace: true
no_property_on_interface: true
no_non_implemented_abstract_methods: true
no_error_suppression: true
no_duplicate_arguments: true
no_commented_out_code: true
newline_at_end_of_file: true
missing_arguments: true
method_calls_on_non_object: true
instanceof_class_exists: true
foreach_traversable: true
fix_line_ending: true
fix_doc_comments: true
duplication: true
deprecated_code_usage: true
deadlock_detection_in_loops: true
code_rating: true
closure_use_not_conflicting: true
catch_class_exists: true
blank_line_after_namespace_declaration: false
avoid_multiple_statements_on_same_line: true
avoid_duplicate_types: true
avoid_conflicting_incrementers: true
avoid_closing_tag: true
assignment_of_null_return: true
argument_type_checks: true
duplication: true
filter:
paths: [code/*, tests/*]

View File

@ -1,8 +1,10 @@
language: php
dist: precise
dist: trusty
sudo: false
cache:
directories:
- $HOME/.composer/cache/files
php:
- 5.6
@ -10,23 +12,33 @@ php:
env:
global:
- DB="PGSQL"
- CORE_RELEASE="4"
matrix:
fast_finish: true
include:
- php: 5.6
env:
- PHPUNIT_TEST=framework
- php: 5.6
env:
- PHPUNIT_TEST=postgresql
- PHPCS_TEST=1
before_script:
# Init PHP
- printf "\n" | pecl install imagick
- phpenv rehash
- phpenv config-rm xdebug.ini
- export PATH=~/.composer/vendor/bin:$PATH
- echo 'memory_limit = 2048M' >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini
# Temporarily update to 1.5.x-dev of composer
- composer self-update --snapshot
# Install composer dependencies
- composer validate
- composer install --prefer-dist
- composer require --prefer-dist --no-update symfony/config:^3.2 silverstripe/framework:4.0.x-dev silverstripe/cms:4.0.x-dev silverstripe/siteconfig:4.0.x-dev silverstripe/config:1.0.x-dev silverstripe/admin:1.0.x-dev silverstripe/assets:1.0.x-dev silverstripe/versioned:1.0.x-dev
- composer require --prefer-dist --no-update silverstripe/recipe-cms:1.0.x-dev
- composer update
- if [[ $PHPCS_TEST ]]; then composer global require squizlabs/php_codesniffer:^3 --prefer-dist --no-interaction --no-progress --no-suggest -o; fi
script:
- vendor/bin/phpunit ./tests
- vendor/bin/phpunit ./framework/tests
- if [[ $PHPUNIT_TEST == postgresql ]]; then vendor/bin/phpunit ./tests; fi
- if [[ $PHPUNIT_TEST == framework ]]; then vendor/bin/phpunit ./vendor/silverstripe/framework/tests/php; fi
- if [[ $PHPCS_TEST ]]; then composer run-script lint; fi

View File

@ -282,7 +282,7 @@ class PostgreSQLDatabase extends Database
public function getDatabaseServer()
{
return "postgresql";
return "pgsql";
}
/**
@ -374,14 +374,15 @@ class PostgreSQLDatabase extends Database
// Get tables
$tablesToSearch = [];
foreach($classesToSearch as $class) {
foreach ($classesToSearch as $class) {
$tablesToSearch[$class] = DataObject::getSchema()->baseDataTable($class);
}
//We can get a list of all the tsvector columns though this query:
//We know what tables to search in based on the $classesToSearch variable:
$classesPlaceholders = DB::placeholders($classesToSearch);
$searchableColumns = $this->preparedQuery("
$searchableColumns = $this->preparedQuery(
"
SELECT table_name, column_name, data_type
FROM information_schema.columns
WHERE data_type='tsvector' AND table_name in ($classesPlaceholders);",
@ -500,7 +501,7 @@ class PostgreSQLDatabase extends Database
/*
* This is a quick lookup to discover if the database supports particular extensions
*/
public function supportsExtensions($extensions=array('partitions', 'tablespaces', 'clustering'))
public function supportsExtensions($extensions = array('partitions', 'tablespaces', 'clustering'))
{
if (isset($extensions['partitions'])) {
return true;
@ -683,8 +684,10 @@ class PostgreSQLDatabase extends Database
public function schemaToDatabaseName($schema)
{
switch ($schema) {
case $this->schemaOriginal: return $this->databaseOriginal;
default: return $schema;
case $this->schemaOriginal:
return $this->databaseOriginal;
default:
return $schema;
}
}
@ -698,8 +701,10 @@ class PostgreSQLDatabase extends Database
public function databaseToSchemaName($database)
{
switch ($database) {
case $this->databaseOriginal: return $this->schemaOriginal;
default: return $database;
case $this->databaseOriginal:
return $this->schemaOriginal;
default:
return $database;
}
}

View File

@ -113,6 +113,7 @@ class PostgreSQLSchemaManager extends DBSchemaManager
}
return $this->postgresDatabaseList();
}
/**
* Drops a postgres database, ignoring model_schema_as_database
*
@ -177,8 +178,8 @@ class PostgreSQLSchemaManager extends DBSchemaManager
*/
public function schemaList()
{
return $this->query("
SELECT nspname
return $this->query(
"SELECT nspname
FROM pg_catalog.pg_namespace
WHERE nspname <> 'information_schema' AND nspname !~ E'^pg_'"
)->column();
@ -186,7 +187,7 @@ class PostgreSQLSchemaManager extends DBSchemaManager
public function createTable($table, $fields = null, $indexes = null, $options = null, $advancedOptions = null)
{
$fieldSchemas = $indexSchemas = "";
$fieldSchemas = "";
if ($fields) {
foreach ($fields as $k => $v) {
$fieldSchemas .= "\"$k\" $v,\n";
@ -194,9 +195,6 @@ class PostgreSQLSchemaManager extends DBSchemaManager
}
if (!empty($options[self::ID])) {
$addOptions = $options[self::ID];
} elseif (!empty($options[get_class($this)])) {
Deprecation::notice('3.2', 'Use PostgreSQLSchemaManager::ID for referencing postgres-specific table creation options');
$addOptions = $options[get_class($this)];
} else {
$addOptions = null;
}
@ -211,20 +209,21 @@ class PostgreSQLSchemaManager extends DBSchemaManager
//If we have a fulltext search request, then we need to create a special column
//for GiST searches
$fulltexts = '';
$triggers = '';
$triggers = [];
if ($indexes) {
foreach ($indexes as $name => $this_index) {
if (is_array($this_index) && $this_index['type'] == 'fulltext') {
$ts_details = $this->fulltext($this_index, $table, $name);
$fulltexts .= $ts_details['fulltexts'] . ', ';
$triggers .= $ts_details['triggers'];
$triggers[] = $ts_details['triggers'];
}
}
}
$indexQueries = [];
if ($indexes) {
foreach ($indexes as $k => $v) {
$indexSchemas .= $this->getIndexSqlDefinition($table, $k, $v) . "\n";
$indexQueries[] = $this->getIndexSqlDefinition($table, $k, $v);
}
}
@ -239,14 +238,19 @@ class PostgreSQLSchemaManager extends DBSchemaManager
$tableSpace = '';
}
$this->query("CREATE TABLE \"$table\" (
$fieldSchemas
$fulltexts
primary key (\"ID\")
)$tableSpace; $indexSchemas $addOptions");
$this->query(
"CREATE TABLE \"$table\" (
$fieldSchemas
$fulltexts
primary key (\"ID\")
)$tableSpace $addOptions"
);
foreach ($indexQueries as $indexQuery) {
$this->query($indexQuery);
}
if ($triggers!='') {
$this->query($triggers);
foreach ($triggers as $trigger) {
$this->query($trigger);
}
//If we have a partitioning requirement, we do that here:
@ -256,7 +260,7 @@ class PostgreSQLSchemaManager extends DBSchemaManager
//Lastly, clustering goes here:
if ($advancedOptions && isset($advancedOptions['cluster'])) {
$this->query("CLUSTER \"$table\" USING \"{$advancedOptions['cluster']}\";");
$this->query("CLUSTER \"$table\" USING \"{$advancedOptions['cluster']}\"");
}
return $table;
@ -299,9 +303,16 @@ class PostgreSQLSchemaManager extends DBSchemaManager
return $this->buildPostgresIndexName($tableName, $triggerName, 'ts');
}
public function alterTable($table, $newFields = null, $newIndexes = null, $alteredFields = null, $alteredIndexes = null, $alteredOptions = null, $advancedOptions = null)
{
$alterList = array();
public function alterTable(
$table,
$newFields = null,
$newIndexes = null,
$alteredFields = null,
$alteredIndexes = null,
$alteredOptions = null,
$advancedOptions = null
) {
$alterList = [];
if ($newFields) {
foreach ($newFields as $fieldName => $fieldSpec) {
$alterList[] = "ADD \"$fieldName\" $fieldSpec";
@ -319,46 +330,49 @@ class PostgreSQLSchemaManager extends DBSchemaManager
//Do we need to do anything with the tablespaces?
if ($alteredOptions && isset($advancedOptions['tablespace'])) {
$this->createOrReplaceTablespace($advancedOptions['tablespace']['name'], $advancedOptions['tablespace']['location']);
$this->createOrReplaceTablespace(
$advancedOptions['tablespace']['name'],
$advancedOptions['tablespace']['location']
);
$this->query("ALTER TABLE \"$table\" SET TABLESPACE {$advancedOptions['tablespace']['name']};");
}
//DB ABSTRACTION: we need to change the constraints to be a separate 'add' command,
//see http://www.postgresql.org/docs/8.1/static/sql-altertable.html
$alterIndexList = array();
$alterIndexList = [];
//Pick up the altered indexes here:
$fieldList = $this->fieldList($table);
$fulltexts = false;
$drop_triggers = false;
$triggers = false;
$fulltexts = [];
$dropTriggers = [];
$triggers = [];
if ($alteredIndexes) {
foreach ($alteredIndexes as $indexName=>$indexSpec) {
foreach ($alteredIndexes as $indexName => $indexSpec) {
$indexNamePG = $this->buildPostgresIndexName($table, $indexName);
if ($indexSpec['type']=='fulltext') {
if ($indexSpec['type'] == 'fulltext') {
//For full text indexes, we need to drop the trigger, drop the index, AND drop the column
//Go and get the tsearch details:
$ts_details = $this->fulltext($indexSpec, $table, $indexName);
//Go and get the tsearch details:
$ts_details = $this->fulltext($indexSpec, $table, $indexName);
//Drop this column if it already exists:
//Drop this column if it already exists:
//No IF EXISTS option is available for Postgres <9.0
if (array_key_exists($ts_details['ts_name'], $fieldList)) {
$fulltexts.="ALTER TABLE \"{$table}\" DROP COLUMN \"{$ts_details['ts_name']}\";";
//No IF EXISTS option is available for Postgres <9.0
if (array_key_exists($ts_details['ts_name'], $fieldList)) {
$fulltexts[] = "ALTER TABLE \"{$table}\" DROP COLUMN \"{$ts_details['ts_name']}\";";
}
// We'll execute these later:
$triggerNamePG = $this->buildPostgresTriggerName($table, $indexName);
$dropTriggers[] = "DROP TRIGGER IF EXISTS \"$triggerNamePG\" ON \"$table\";";
$fulltexts[] = "ALTER TABLE \"{$table}\" ADD COLUMN {$ts_details['fulltexts']};";
$triggers[] = $ts_details['triggers'];
}
// We'll execute these later:
$triggerNamePG = $this->buildPostgresTriggerName($table, $indexName);
$drop_triggers.= "DROP TRIGGER IF EXISTS \"$triggerNamePG\" ON \"$table\";";
$fulltexts .= "ALTER TABLE \"{$table}\" ADD COLUMN {$ts_details['fulltexts']};";
$triggers .= $ts_details['triggers'];
}
// Create index action (including fulltext)
$alterIndexList[] = "DROP INDEX IF EXISTS \"$indexNamePG\";";
// Create index action (including fulltext)
$alterIndexList[] = "DROP INDEX IF EXISTS \"$indexNamePG\";";
$createIndex = $this->getIndexSqlDefinition($table, $indexName, $indexSpec);
if ($createIndex!==false) {
if ($createIndex) {
$alterIndexList[] = $createIndex;
}
}
@ -368,25 +382,25 @@ class PostgreSQLSchemaManager extends DBSchemaManager
if ($newIndexes) {
foreach ($newIndexes as $indexName => $indexSpec) {
$indexNamePG = $this->buildPostgresIndexName($table, $indexName);
//If we have a fulltext search request, then we need to create a special column
//for GiST searches
//Pick up the new indexes here:
if ($indexSpec['type']=='fulltext') {
$ts_details=$this->fulltext($indexSpec, $table, $indexName);
if (!isset($fieldList[$ts_details['ts_name']])) {
$fulltexts.="ALTER TABLE \"{$table}\" ADD COLUMN {$ts_details['fulltexts']};";
$triggers.=$ts_details['triggers'];
//If we have a fulltext search request, then we need to create a special column
//for GiST searches
//Pick up the new indexes here:
if ($indexSpec['type'] == 'fulltext') {
$ts_details = $this->fulltext($indexSpec, $table, $indexName);
if (!isset($fieldList[$ts_details['ts_name']])) {
$fulltexts[] = "ALTER TABLE \"{$table}\" ADD COLUMN {$ts_details['fulltexts']};";
$triggers[] = $ts_details['triggers'];
}
}
}
//Check that this index doesn't already exist:
$indexes=$this->indexList($table);
//Check that this index doesn't already exist:
$indexes = $this->indexList($table);
if (isset($indexes[$indexName])) {
$alterIndexList[] = "DROP INDEX IF EXISTS \"$indexNamePG\";";
}
$createIndex=$this->getIndexSqlDefinition($table, $indexName, $indexSpec);
if ($createIndex!==false) {
$createIndex = $this->getIndexSqlDefinition($table, $indexName, $indexSpec);
if ($createIndex) {
$alterIndexList[] = $createIndex;
}
}
@ -399,7 +413,7 @@ class PostgreSQLSchemaManager extends DBSchemaManager
//Do we need to create a tablespace for this item?
if ($advancedOptions && isset($advancedOptions['extensions']['tablespace'])) {
$extensions=$advancedOptions['extensions'];
$extensions = $advancedOptions['extensions'];
$this->createOrReplaceTablespace($extensions['tablespace']['name'], $extensions['tablespace']['location']);
}
@ -412,24 +426,19 @@ class PostgreSQLSchemaManager extends DBSchemaManager
}
//Create any fulltext columns and triggers here:
if ($fulltexts) {
$this->query($fulltexts);
foreach ($fulltexts as $fulltext) {
$this->query($fulltext);
}
if ($drop_triggers) {
$this->query($drop_triggers);
foreach ($dropTriggers as $dropTrigger) {
$this->query($dropTrigger);
}
if ($triggers) {
$this->query($triggers);
$triggerbits=explode(';', $triggers);
foreach ($triggerbits as $trigger) {
$trigger_fields=$this->triggerFieldsFromTrigger($trigger);
if ($trigger_fields) {
//We need to run a simple query to force the database to update the triggered columns
$this->query("UPDATE \"{$table}\" SET \"{$trigger_fields[0]}\"=\"$trigger_fields[0]\";");
}
foreach ($triggers as $trigger) {
$this->query($trigger);
$triggerFields = $this->triggerFieldsFromTrigger($trigger);
if ($triggerFields) {
//We need to run a simple query to force the database to update the triggered columns
$this->query("UPDATE \"{$table}\" SET \"{$triggerFields[0]}\"=\"$triggerFields[0]\";");
}
}
@ -454,11 +463,12 @@ class PostgreSQLSchemaManager extends DBSchemaManager
"SELECT relid FROM pg_stat_user_tables WHERE relname = ?;",
array($table)
)->first();
$oid=$stats['relid'];
$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("
$clustered = $this->preparedQuery(
"
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';",
@ -488,11 +498,11 @@ class PostgreSQLSchemaManager extends DBSchemaManager
$pattern = '/^([\w(\,)]+)\s?((?:not\s)?null)?\s?(default\s[\w\.\']+)?\s?(check\s[\w()\'",\s]+)?$/i';
preg_match($pattern, $colSpec, $matches);
if (sizeof($matches)==0) {
if (sizeof($matches) == 0) {
return '';
}
if ($matches[1]=='serial8') {
if ($matches[1] == 'serial8') {
return '';
}
@ -515,20 +525,20 @@ class PostgreSQLSchemaManager extends DBSchemaManager
$constraintExists = $this->constraintExists($constraintName, false);
if (isset($matches[4])) {
//Take this new constraint and see what's outstanding from the target table:
$constraint_bits=explode('(', $matches[4]);
$constraint_values=trim($constraint_bits[2], ')');
$constraint_values_bits=explode(',', $constraint_values);
$default=trim($constraint_values_bits[0], " '");
$constraint_bits = explode('(', $matches[4]);
$constraint_values = trim($constraint_bits[2], ')');
$constraint_values_bits = explode(',', $constraint_values);
$default = trim($constraint_values_bits[0], " '");
//Now go and convert anything that's not in this list to 'Page'
//We have to run this as a query, not as part of the alteration queries due to the way they are constructed.
$updateConstraint='';
$updateConstraint.="UPDATE \"{$tableName}\" SET \"$colName\"='$default' WHERE \"$colName\" NOT IN ($constraint_values);";
$updateConstraint = '';
$updateConstraint .= "UPDATE \"{$tableName}\" SET \"$colName\"='$default' WHERE \"$colName\" NOT IN ($constraint_values);";
if ($this->hasTable("{$tableName}_Live")) {
$updateConstraint.="UPDATE \"{$tableName}_Live\" SET \"$colName\"='$default' WHERE \"$colName\" NOT IN ($constraint_values);";
$updateConstraint .= "UPDATE \"{$tableName}_Live\" SET \"$colName\"='$default' WHERE \"$colName\" NOT IN ($constraint_values);";
}
if ($this->hasTable("{$tableName}_versions")) {
$updateConstraint.="UPDATE \"{$tableName}_versions\" SET \"$colName\"='$default' WHERE \"$colName\" NOT IN ($constraint_values);";
$updateConstraint .= "UPDATE \"{$tableName}_versions\" SET \"$colName\"='$default' WHERE \"$colName\" NOT IN ($constraint_values);";
}
$this->query($updateConstraint);
@ -595,13 +605,14 @@ class PostgreSQLSchemaManager extends DBSchemaManager
//This gets us more information than we need, but I've included it all for the moment....
//if(!isset(self::$cached_fieldlists[$table])){
$fields = $this->preparedQuery("
$fields = $this->preparedQuery(
"
SELECT ordinal_position, column_name, data_type, column_default,
is_nullable, character_maximum_length, numeric_precision, numeric_scale
FROM information_schema.columns WHERE table_name = ? and table_schema = ?
ORDER BY ordinal_position;",
array($table, $this->database->currentSchema())
);
array($table, $this->database->currentSchema())
);
$output = array();
if ($fields) {
@ -618,62 +629,70 @@ class PostgreSQLSchemaManager extends DBSchemaManager
//CHECK ("ClassName"::text = 'PageComment'::text)
//TODO: replace all this with a regular expression!
$value=$constraint['pg_get_constraintdef'];
$value=substr($value, strpos($value, '='));
$value=str_replace("''", "'", $value);
$value = $constraint['pg_get_constraintdef'];
$value = substr($value, strpos($value, '='));
$value = str_replace("''", "'", $value);
$in_value=false;
$constraints=array();
$current_value='';
for ($i=0; $i<strlen($value); $i++) {
$char=substr($value, $i, 1);
$in_value = false;
$constraints = array();
$current_value = '';
for ($i = 0; $i < strlen($value); $i++) {
$char = substr($value, $i, 1);
if ($in_value) {
$current_value.=$char;
$current_value .= $char;
}
if ($char=="'") {
if ($char == "'") {
if (!$in_value) {
$in_value=true;
$in_value = true;
} else {
$in_value=false;
$constraints[]=substr($current_value, 0, -1);
$current_value='';
$in_value = false;
$constraints[] = substr($current_value, 0, -1);
$current_value = '';
}
}
}
if (sizeof($constraints)>0) {
if (sizeof($constraints) > 0) {
//Get the default:
$default=trim(substr($field['column_default'], 0, strpos($field['column_default'], '::')), "'");
$output[$field['column_name']]=$this->enum(array('default'=>$default, 'name'=>$field['column_name'], 'enums'=>$constraints));
$default = trim(substr(
$field['column_default'],
0,
strpos($field['column_default'], '::')
), "'");
$output[$field['column_name']] = $this->enum(array(
'default' => $default,
'name' => $field['column_name'],
'enums' => $constraints
));
}
} else {
$output[$field['column_name']]='varchar(' . $field['character_maximum_length'] . ')';
$output[$field['column_name']] = 'varchar(' . $field['character_maximum_length'] . ')';
}
break;
case 'numeric':
$output[$field['column_name']]='decimal(' . $field['numeric_precision'] . ',' . $field['numeric_scale'] . ') default ' . floatval($field['column_default']);
$output[$field['column_name']] = 'decimal(' . $field['numeric_precision'] . ',' . $field['numeric_scale'] . ') default ' . floatval($field['column_default']);
break;
case 'integer':
$output[$field['column_name']]='integer default ' . (int)$field['column_default'];
$output[$field['column_name']] = 'integer default ' . (int)$field['column_default'];
break;
case 'timestamp without time zone':
$output[$field['column_name']]='timestamp';
$output[$field['column_name']] = 'timestamp';
break;
case 'smallint':
$output[$field['column_name']]='smallint default ' . (int)$field['column_default'];
$output[$field['column_name']] = 'smallint default ' . (int)$field['column_default'];
break;
case 'time without time zone':
$output[$field['column_name']]='time';
$output[$field['column_name']] = 'time';
break;
case 'double precision':
$output[$field['column_name']]='float';
$output[$field['column_name']] = 'float';
break;
default:
@ -690,12 +709,12 @@ class PostgreSQLSchemaManager extends DBSchemaManager
return $output;
}
public function clearCachedFieldlist($tableName=false)
public function clearCachedFieldlist($tableName = false)
{
if ($tableName) {
unset(self::$cached_fieldlists[$tableName]);
} else {
self::$cached_fieldlists=array();
self::$cached_fieldlists = array();
}
return true;
}
@ -761,7 +780,7 @@ class PostgreSQLSchemaManager extends DBSchemaManager
break;
case 'index':
//'index' is the same as default, just a normal index with the default type decided by the database.
//'index' is the same as default, just a normal index with the default type decided by the database.
default:
$spec = "create index \"$tableCol\" ON \"$tableName\" (" . $this->implodeColumnList($indexSpec['columns']) . ") $fillfactor $where";
}
@ -790,22 +809,36 @@ class PostgreSQLSchemaManager extends DBSchemaManager
* Given a trigger name attempt to determine the columns upon which it acts
*
* @param string $triggerName Postgres trigger name
* @param string $table
* @return array List of columns
*/
protected function extractTriggerColumns($triggerName)
protected function extractTriggerColumns($triggerName, $table)
{
$trigger = $this->preparedQuery(
"SELECT tgargs FROM pg_catalog.pg_trigger WHERE tgname = ?",
array($triggerName)
"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_namespace n ON n.oid = c.relnamespace
WHERE c.relname = ?
AND n.nspname = ?
AND t.tgname = ?",
[
$table,
$this->database->currentSchema(),
$triggerName
]
)->first();
// Option 1: output as a string
if (strpos($trigger['tgargs'], '\000') !== false) {
$argList = explode('\000', $trigger['tgargs']);
array_pop($argList);
// Convert stream to string
if (is_resource($trigger['tgargs'])) {
$trigger['tgargs'] = stream_get_contents($trigger['tgargs']);
}
// Option 2: hex-encoded (not sure why this happens, depends on PGSQL config)
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 = "";
@ -826,7 +859,8 @@ class PostgreSQLSchemaManager extends DBSchemaManager
public function indexList($table)
{
//Retrieve a list of indexes for the specified table
$indexes = $this->preparedQuery("
$indexes = $this->preparedQuery(
"
SELECT tablename, indexname, indexdef
FROM pg_catalog.pg_indexes
WHERE tablename = ? AND schemaname = ?;",
@ -843,12 +877,12 @@ class PostgreSQLSchemaManager extends DBSchemaManager
$type = '';
//Check for uniques:
if (substr($index['indexdef'], 0, 13)=='CREATE UNIQUE') {
if (substr($index['indexdef'], 0, 13) == 'CREATE UNIQUE') {
$type = 'unique';
}
//check for hashes, btrees etc:
if (strpos(strtolower($index['indexdef']), 'using hash ')!==false) {
if (strpos(strtolower($index['indexdef']), 'using hash ') !== false) {
$type = 'hash';
}
@ -856,7 +890,7 @@ class PostgreSQLSchemaManager extends DBSchemaManager
//if(strpos(strtolower($index['indexdef']), 'using btree ')!==false)
// $prefix='using btree ';
if (strpos(strtolower($index['indexdef']), 'using rtree ')!==false) {
if (strpos(strtolower($index['indexdef']), 'using rtree ') !== false) {
$type = 'rtree';
}
@ -865,7 +899,7 @@ class PostgreSQLSchemaManager extends DBSchemaManager
$type = 'fulltext';
// Extract trigger information from postgres
$triggerName = preg_replace('/^ix_/', 'ts_', $index['indexname']);
$columns = $this->extractTriggerColumns($triggerName);
$columns = $this->extractTriggerColumns($triggerName, $table);
$columnString = $this->implodeColumnList($columns);
} else {
$columnString = $this->quoteColumnSpecString($index['indexdef']);
@ -908,7 +942,8 @@ class PostgreSQLSchemaManager extends DBSchemaManager
protected function constraintExists($constraint, $cache = true)
{
if (!$cache || !isset(self::$cached_constraints[$constraint])) {
$value = $this->preparedQuery("
$value = $this->preparedQuery(
"
SELECT conname,pg_catalog.pg_get_constraintdef(r.oid, true)
FROM pg_catalog.pg_constraint r
INNER JOIN pg_catalog.pg_namespace n
@ -968,7 +1003,8 @@ class PostgreSQLSchemaManager extends DBSchemaManager
*/
protected function dropTrigger($triggerName, $tableName)
{
$exists = $this->preparedQuery("
$exists = $this->preparedQuery(
"
SELECT trigger_name
FROM information_schema.triggers
WHERE trigger_name = ? AND trigger_schema = ?;",
@ -988,18 +1024,18 @@ class PostgreSQLSchemaManager extends DBSchemaManager
protected function triggerFieldsFromTrigger($trigger)
{
if ($trigger) {
$tsvector='tsvector_update_trigger';
$ts_pos=strpos($trigger, $tsvector);
$details=trim(substr($trigger, $ts_pos+strlen($tsvector)), '();');
$tsvector = 'tsvector_update_trigger';
$ts_pos = strpos($trigger, $tsvector);
$details = trim(substr($trigger, $ts_pos + strlen($tsvector)), '();');
//Now split this into bits:
$bits=explode(',', $details);
$bits = explode(',', $details);
$fields=$bits[2];
$fields = $bits[2];
$field_bits=explode(',', str_replace('"', '', $fields));
$result=array();
$field_bits = explode(',', str_replace('"', '', $fields));
$result = array();
foreach ($field_bits as $field_bit) {
$result[]=trim($field_bit);
$result[] = trim($field_bit);
}
return $result;
@ -1063,7 +1099,10 @@ class PostgreSQLSchemaManager extends DBSchemaManager
public function enum($values)
{
$default = " default '{$values['default']}'";
return "varchar(255)" . $default . " check (\"" . $values['name'] . "\" in ('" . implode('\', \'', $values['enums']) . "'))";
return "varchar(255)" . $default . " check (\"" . $values['name'] . "\" in ('" . implode(
'\', \'',
$values['enums']
) . "', null))";
}
/**
@ -1250,7 +1289,7 @@ class PostgreSQLSchemaManager extends DBSchemaManager
*/
protected function enumValuesFromConstraint($constraint)
{
$constraint = substr($constraint, strpos($constraint, 'ANY (ARRAY[')+11);
$constraint = substr($constraint, strpos($constraint, 'ANY (ARRAY[') + 11);
$constraint = substr($constraint, 0, -11);
$constraints = array();
$segments = explode(',', $constraint);
@ -1314,21 +1353,25 @@ class PostgreSQLSchemaManager extends DBSchemaManager
//We need the plpgsql language to be installed for this to work:
$this->createLanguage('plpgsql');
$trigger='CREATE OR REPLACE FUNCTION ' . $tableName . '_insert_trigger() RETURNS TRIGGER AS $$ BEGIN ';
$first=true;
$trigger = 'CREATE OR REPLACE FUNCTION ' . $tableName . '_insert_trigger() RETURNS TRIGGER AS $$ BEGIN ';
$first = true;
//Do we need to create a tablespace for this item?
if ($extensions && isset($extensions['tablespace'])) {
$this->createOrReplaceTablespace($extensions['tablespace']['name'], $extensions['tablespace']['location']);
$tableSpace=' TABLESPACE ' . $extensions['tablespace']['name'];
$tableSpace = ' TABLESPACE ' . $extensions['tablespace']['name'];
} else {
$tableSpace='';
$tableSpace = '';
}
foreach ($partitions as $partition_name => $partition_value) {
//Check that this child table does not already exist:
if (!$this->hasTable($partition_name)) {
$this->query("CREATE TABLE \"$partition_name\" (CHECK (" . str_replace('NEW.', '', $partition_value) . ")) INHERITS (\"$tableName\")$tableSpace;");
$this->query("CREATE TABLE \"$partition_name\" (CHECK (" . str_replace(
'NEW.',
'',
$partition_value
) . ")) INHERITS (\"$tableName\")$tableSpace;");
} else {
//Drop the constraint, we will recreate in in the next line
$constraintName = "{$partition_name}_pkey";
@ -1342,10 +1385,10 @@ class PostgreSQLSchemaManager extends DBSchemaManager
$this->query("ALTER TABLE \"$partition_name\" ADD CONSTRAINT \"{$partition_name}_pkey\" PRIMARY KEY (\"ID\");");
if ($first) {
$trigger.='IF';
$first=false;
$trigger .= 'IF';
$first = false;
} else {
$trigger.='ELSIF';
$trigger .= 'ELSIF';
}
$trigger .= " ($partition_value) THEN INSERT INTO \"$partition_name\" VALUES (NEW.*);";
@ -1354,7 +1397,7 @@ class PostgreSQLSchemaManager extends DBSchemaManager
// We need to propogate the indexes through to the child pages.
// Some of this code is duplicated, and could be tidied up
foreach ($indexes as $name => $this_index) {
if ($this_index['type']=='fulltext') {
if ($this_index['type'] == 'fulltext') {
$fillfactor = $where = '';
if (isset($this_index['fillfactor'])) {
$fillfactor = 'WITH (FILLFACTOR = ' . $this_index['fillfactor'] . ')';
@ -1363,7 +1406,10 @@ class PostgreSQLSchemaManager extends DBSchemaManager
$where = 'WHERE ' . $this_index['where'];
}
$clusterMethod = PostgreSQLDatabase::default_fts_cluster_method();
$this->query("CREATE INDEX \"" . $this->buildPostgresIndexName($partition_name, $this_index['name']) . "\" ON \"" . $partition_name . "\" USING $clusterMethod(\"ts_" . $name . "\") $fillfactor $where");
$this->query("CREATE INDEX \"" . $this->buildPostgresIndexName(
$partition_name,
$this_index['name']
) . "\" ON \"" . $partition_name . "\" USING $clusterMethod(\"ts_" . $name . "\") $fillfactor $where");
$ts_details = $this->fulltext($this_index, $partition_name, $name);
$this->query($ts_details['triggers']);
} else {

View File

@ -1,27 +1,41 @@
{
"name": "silverstripe/postgresql",
"description": "SilverStripe now has tentative support for PostgreSQL ('Postgres')",
"type": "silverstripe-vendormodule",
"keywords": ["silverstripe", "postgresql", "database"],
"license": "BSD-3-Clause",
"authors": [
{
"name": "Sam Minnée",
"email": "sam@silverstripe.com"
}
],
"require": {
"silverstripe/framework": "^4.0@dev",
"silverstripe/vendor-plugin": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^5.7"
},
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"minimum-stability": "dev",
"prefer-stable": true
"name": "silverstripe/postgresql",
"description": "SilverStripe now has tentative support for PostgreSQL ('Postgres')",
"type": "silverstripe-vendormodule",
"keywords": [
"silverstripe",
"postgresql",
"database"
],
"license": "BSD-3-Clause",
"authors": [
{
"name": "Sam Minnée",
"email": "sam@silverstripe.com"
}
],
"require": {
"silverstripe/framework": "^4",
"silverstripe/vendor-plugin": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^5.7"
},
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
"SilverStripe\\PostgreSQL\\": "code/",
"SilverStripe\\PostgreSQL\\Tests\\": "tests/"
}
},
"scripts": {
"lint": "phpcs code/ tests/",
"lint-clean": "phpcbf code/ tests/"
},
"minimum-stability": "dev",
"prefer-stable": true
}

30
phpcs.xml.dist Normal file
View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="SilverStripe">
<description>CodeSniffer ruleset for SilverStripe coding conventions.</description>
<!-- base rules are PSR-2 -->
<rule ref="PSR2" >
<!-- Current exclusions -->
<exclude name="PSR1.Methods.CamelCapsMethodName" />
<exclude name="PSR1.Files.SideEffects.FoundWithSymbols" />
<exclude name="PSR2.Classes.PropertyDeclaration" />
<exclude name="PSR2.ControlStructures.SwitchDeclaration" /> <!-- causes php notice while linting -->
<exclude name="PSR2.ControlStructures.SwitchDeclaration.WrongOpenercase" />
<exclude name="PSR2.ControlStructures.SwitchDeclaration.WrongOpenerdefault" />
<exclude name="PSR2.ControlStructures.SwitchDeclaration.TerminatingComment" />
<exclude name="PSR2.Methods.MethodDeclaration.Underscore" />
<exclude name="Squiz.Scope.MethodScope" />
<exclude name="Squiz.Classes.ValidClassName.NotCamelCaps" />
<exclude name="Generic.Files.LineLength.TooLong" />
<exclude name="PEAR.Functions.ValidDefaultValue.NotAtEnd" />
</rule>
<!-- include php files only -->
<arg name="extensions" value="php,lib,inc,php5"/>
<!-- PHP-PEG generated file not intended for human consumption -->
<exclude-pattern>*/SSTemplateParser.php$</exclude-pattern>
<exclude-pattern>*/_fakewebroot/*</exclude-pattern>
<exclude-pattern>*/fixtures/*</exclude-pattern>
</ruleset>

View File

@ -1,4 +1,4 @@
<phpunit bootstrap="cms/tests/bootstrap.php" colors="true">
<phpunit bootstrap="vendor/silverstripe/framework/tests/bootstrap.php" colors="true">
<testsuite name="Default">
<directory>tests</directory>

View File

@ -1,5 +1,7 @@
<?php
namespace SilverStripe\PostgreSQL\Tests;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\PostgreSQL\PostgreSQLConnector;
@ -23,7 +25,7 @@ class PostgreSQLConnectorTest extends SapphireTest
// Ignoring question mark placeholders within string literals
$this->assertEquals(
"SELECT * FROM Table WHERE ID = $1 AND Name = $2 AND Content = '<p>What is love?</p>'",
"SELECT * FROM Table WHERE ID = $1 AND Name = $2 AND Content = '<p>What is love?</p>'",
$connector->replacePlaceholders(
"SELECT * FROM Table WHERE ID = ? AND Name = ? AND Content = '<p>What is love?</p>'"
)
@ -31,7 +33,7 @@ class PostgreSQLConnectorTest extends SapphireTest
// Ignoring question mark placeholders within string literals with escaped slashes
$this->assertEquals(
"SELECT * FROM Table WHERE ID = $1 AND Title = '\\'' AND Content = '<p>What is love?</p>' AND Name = $2",
"SELECT * FROM Table WHERE ID = $1 AND Title = '\\'' AND Content = '<p>What is love?</p>' AND Name = $2",
$connector->replacePlaceholders(
"SELECT * FROM Table WHERE ID = ? AND Title = '\\'' AND Content = '<p>What is love?</p>' AND Name = ?"
)
@ -39,7 +41,7 @@ class PostgreSQLConnectorTest extends SapphireTest
// same as above, but use double single quote escape syntax
$this->assertEquals(
"SELECT * FROM Table WHERE ID = $1 AND Title = '''' AND Content = '<p>What is love?</p>' AND Name = $2",
"SELECT * FROM Table WHERE ID = $1 AND Title = '''' AND Content = '<p>What is love?</p>' AND Name = $2",
$connector->replacePlaceholders(
"SELECT * FROM Table WHERE ID = ? AND Title = '''' AND Content = '<p>What is love?</p>' AND Name = ?"
)

View File

@ -1,5 +1,9 @@
<?php
namespace SilverStripe\PostgreSQL\Tests;
use Exception;
use Page;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
@ -15,19 +19,18 @@ class PostgreSQLDatabaseTest extends SapphireTest
public function testReadOnlyTransaction()
{
if (
DB::get_conn()->supportsTransactions() == true
if (DB::get_conn()->supportsTransactions() == true
&& DB::get_conn() instanceof PostgreSQLDatabase
) {
$page=new Page();
$page->Title='Read only success';
$page = new Page();
$page->Title = 'Read only success';
$page->write();
DB::get_conn()->transactionStart('READ ONLY');
try {
$page=new Page();
$page->Title='Read only page failed';
$page = new Page();
$page->Title = 'Read only page failed';
$page->write();
} catch (Exception $e) {
//could not write this record
@ -39,8 +42,8 @@ class PostgreSQLDatabaseTest extends SapphireTest
DataObject::flush_and_destroy_cache();
$success=DataObject::get('Page', "\"Title\"='Read only success'");
$fail=DataObject::get('Page', "\"Title\"='Read only page failed'");
$success = DataObject::get('Page', "\"Title\"='Read only success'");
$fail = DataObject::get('Page', "\"Title\"='Read only page failed'");
//This page should be in the system
$this->assertTrue(is_object($success) && $success->exists());

View File

@ -1,5 +1,6 @@
<?php
namespace SilverStripe\PostgreSQL\Tests;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\Queries\SQLSelect;