diff --git a/.editorconfig b/.editorconfig index 7d56e29..3ef7d71 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 diff --git a/.scrutinizer.yml b/.scrutinizer.yml index d1ebd80..4625fbf 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -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/*] diff --git a/.travis.yml b/.travis.yml index 7a1e097..249895b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/code/PostgreSQLDatabase.php b/code/PostgreSQLDatabase.php index 8c44227..678a960 100644 --- a/code/PostgreSQLDatabase.php +++ b/code/PostgreSQLDatabase.php @@ -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; } } diff --git a/code/PostgreSQLSchemaManager.php b/code/PostgreSQLSchemaManager.php index 60f40f6..37f3563 100644 --- a/code/PostgreSQLSchemaManager.php +++ b/code/PostgreSQLSchemaManager.php @@ -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; $i0) { + 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 { diff --git a/composer.json b/composer.json index db29aea..f30948e 100644 --- a/composer.json +++ b/composer.json @@ -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 } diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..b2ed508 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,30 @@ + + + CodeSniffer ruleset for SilverStripe coding conventions. + + + + + + + + + + + + + + + + + + + + + + + */SSTemplateParser.php$ + */_fakewebroot/* + */fixtures/* + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 2266f57..90da05c 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,4 +1,4 @@ - + tests diff --git a/tests/PostgreSQLConnectorTest.php b/tests/PostgreSQLConnectorTest.php index dd97262..2f0566b 100644 --- a/tests/PostgreSQLConnectorTest.php +++ b/tests/PostgreSQLConnectorTest.php @@ -1,5 +1,7 @@ assertEquals( - "SELECT * FROM Table WHERE ID = $1 AND Name = $2 AND Content = '

What is love?

'", + "SELECT * FROM Table WHERE ID = $1 AND Name = $2 AND Content = '

What is love?

'", $connector->replacePlaceholders( "SELECT * FROM Table WHERE ID = ? AND Name = ? AND Content = '

What is love?

'" ) @@ -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 = '

What is love?

' AND Name = $2", + "SELECT * FROM Table WHERE ID = $1 AND Title = '\\'' AND Content = '

What is love?

' AND Name = $2", $connector->replacePlaceholders( "SELECT * FROM Table WHERE ID = ? AND Title = '\\'' AND Content = '

What is love?

' 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 = '

What is love?

' AND Name = $2", + "SELECT * FROM Table WHERE ID = $1 AND Title = '''' AND Content = '

What is love?

' AND Name = $2", $connector->replacePlaceholders( "SELECT * FROM Table WHERE ID = ? AND Title = '''' AND Content = '

What is love?

' AND Name = ?" ) diff --git a/tests/PostgreSQLDatabaseTest.php b/tests/PostgreSQLDatabaseTest.php index fe023b7..2c9ebe3 100644 --- a/tests/PostgreSQLDatabaseTest.php +++ b/tests/PostgreSQLDatabaseTest.php @@ -1,5 +1,9 @@ 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()); diff --git a/tests/PostgreSQLQueryBuilderTest.php b/tests/PostgreSQLQueryBuilderTest.php index f480eeb..67a9853 100644 --- a/tests/PostgreSQLQueryBuilderTest.php +++ b/tests/PostgreSQLQueryBuilderTest.php @@ -1,5 +1,6 @@