From 605ba3eeff96ac003d0b8f338447c6287b6b27c1 Mon Sep 17 00:00:00 2001 From: helpfulrobot Date: Fri, 18 Dec 2015 07:18:01 +1300 Subject: [PATCH] Converted to PSR-2 --- code/PostgreSQLConnector.php | 439 +-- code/PostgreSQLDatabase.php | 1137 ++++---- .../PostgreSQLDatabaseConfigurationHelper.php | 359 +-- code/PostgreSQLQuery.php | 60 +- code/PostgreSQLQueryBuilder.php | 73 +- code/PostgreSQLSchemaManager.php | 2574 +++++++++-------- tests/PostgreSQLConnectorTest.php | 72 +- tests/PostgreSQLDatabaseTest.php | 68 +- 8 files changed, 2485 insertions(+), 2297 deletions(-) diff --git a/code/PostgreSQLConnector.php b/code/PostgreSQLConnector.php index 9d02275..27d1434 100644 --- a/code/PostgreSQLConnector.php +++ b/code/PostgreSQLConnector.php @@ -10,243 +10,270 @@ * @package sapphire * @subpackage model */ -class PostgreSQLConnector extends DBConnector { +class PostgreSQLConnector extends DBConnector +{ - /** - * Connection to the PG Database database - * - * @var resource - */ - protected $dbConn = null; + /** + * Connection to the PG Database database + * + * @var resource + */ + protected $dbConn = null; - /** - * Name of the currently selected database - * - * @var string - */ - protected $databaseName = null; + /** + * Name of the currently selected database + * + * @var string + */ + protected $databaseName = null; - /** - * Reference to the last query result (for pg_affected_rows) - * - * @var resource - */ - protected $lastQuery = null; + /** + * Reference to the last query result (for pg_affected_rows) + * + * @var resource + */ + protected $lastQuery = null; - /** - * Last parameters used to connect - * - * @var array - */ - protected $lastParameters = null; + /** + * Last parameters used to connect + * + * @var array + */ + protected $lastParameters = null; - protected $lastRows = 0; + protected $lastRows = 0; - /** - * Escape a parameter to be used in the connection string - * - * @param array $parameters All parameters - * @param string $key The key in $parameters to pull from - * @param string $name The connection string parameter name - * @param mixed $default The default value, or null if optional - * @return string The completed fragment in the form name=value - */ - protected function escapeParameter($parameters, $key, $name, $default = null) { - if(empty($parameters[$key])) { - if($default === null) return ''; - $value = $default; - } else { - $value = $parameters[$key]; - } - return "$name='" . addslashes($value) . "'"; - } + /** + * Escape a parameter to be used in the connection string + * + * @param array $parameters All parameters + * @param string $key The key in $parameters to pull from + * @param string $name The connection string parameter name + * @param mixed $default The default value, or null if optional + * @return string The completed fragment in the form name=value + */ + protected function escapeParameter($parameters, $key, $name, $default = null) + { + if (empty($parameters[$key])) { + if ($default === null) { + return ''; + } + $value = $default; + } else { + $value = $parameters[$key]; + } + return "$name='" . addslashes($value) . "'"; + } - public function connect($parameters, $selectDB = false) { - $this->lastParameters = $parameters; + public function connect($parameters, $selectDB = false) + { + $this->lastParameters = $parameters; - // Note: Postgres always behaves as though $selectDB = true, ignoring - // any value actually passed in. The controller passes in true for other - // connectors such as PDOConnector. + // Note: Postgres always behaves as though $selectDB = true, ignoring + // any value actually passed in. The controller passes in true for other + // connectors such as PDOConnector. - // Escape parameters - $arguments = array( - $this->escapeParameter($parameters, 'server', 'host', 'localhost'), - $this->escapeParameter($parameters, 'port', 'port', 5432), - $this->escapeParameter($parameters, 'database', 'dbname', 'postgres'), - $this->escapeParameter($parameters, 'username', 'user'), - $this->escapeParameter($parameters, 'password', 'password') - ); + // Escape parameters + $arguments = array( + $this->escapeParameter($parameters, 'server', 'host', 'localhost'), + $this->escapeParameter($parameters, 'port', 'port', 5432), + $this->escapeParameter($parameters, 'database', 'dbname', 'postgres'), + $this->escapeParameter($parameters, 'username', 'user'), + $this->escapeParameter($parameters, 'password', 'password') + ); - // Close the old connection - if($this->dbConn) pg_close($this->dbConn); - - // Connect - $this->dbConn = @pg_connect(implode(' ', $arguments)); - if($this->dbConn === false) { - // Extract error details from PHP error handling - $error = error_get_last(); - if($error && preg_match('/function\\.pg-connect\\<\\/a\\>\\]\\: (?.*)/', $error['message'], $matches)) { - $this->databaseError(html_entity_decode($matches['message'])); - } else { - $this->databaseError("Couldn't connect to PostgreSQL database."); - } - } elseif(pg_connection_status($this->dbConn) != PGSQL_CONNECTION_OK) { - throw new ErrorException($this->getLastError()); - } + // Close the old connection + if ($this->dbConn) { + pg_close($this->dbConn); + } + + // Connect + $this->dbConn = @pg_connect(implode(' ', $arguments)); + if ($this->dbConn === false) { + // Extract error details from PHP error handling + $error = error_get_last(); + if ($error && preg_match('/function\\.pg-connect\\<\\/a\\>\\]\\: (?.*)/', $error['message'], $matches)) { + $this->databaseError(html_entity_decode($matches['message'])); + } else { + $this->databaseError("Couldn't connect to PostgreSQL database."); + } + } elseif (pg_connection_status($this->dbConn) != PGSQL_CONNECTION_OK) { + throw new ErrorException($this->getLastError()); + } - //By virtue of getting here, the connection is active: - $this->databaseName = empty($parameters['database']) ? PostgreSQLDatabase::MASTER_DATABASE : $parameters['database']; - } + //By virtue of getting here, the connection is active: + $this->databaseName = empty($parameters['database']) ? PostgreSQLDatabase::MASTER_DATABASE : $parameters['database']; + } - public function affectedRows() { - return $this->lastRows; - } + public function affectedRows() + { + return $this->lastRows; + } - public function getGeneratedID($table) { - $result = $this->query("SELECT last_value FROM \"{$table}_ID_seq\";")->first(); - return $result['last_value']; - } + public function getGeneratedID($table) + { + $result = $this->query("SELECT last_value FROM \"{$table}_ID_seq\";")->first(); + return $result['last_value']; + } - public function getLastError() { - return pg_last_error($this->dbConn); - } + public function getLastError() + { + return pg_last_error($this->dbConn); + } - public function getSelectedDatabase() { - return $this->databaseName; - } + public function getSelectedDatabase() + { + return $this->databaseName; + } - public function getVersion() { - $version = pg_version($this->dbConn); - if(isset($version['server'])) return $version['server']; - else return false; - } + public function getVersion() + { + $version = pg_version($this->dbConn); + if (isset($version['server'])) { + return $version['server']; + } else { + return false; + } + } - public function isActive() { - return $this->databaseName && $this->dbConn; - } + public function isActive() + { + return $this->databaseName && $this->dbConn; + } - /** - * Determines if the SQL fragment either breaks into or out of a string literal - * by counting single quotes - * - * Handles double-quote escaped quotes as well as slash escaped quotes - * - * @todo Test this! - * - * @see http://www.postgresql.org/docs/8.3/interactive/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS - * - * @param string $input The SQL fragment - * @return boolean True if the string breaks into or out of a string literal - */ - public function checkStringTogglesLiteral($input) { - // Remove escaped backslashes, count them! - $input = preg_replace('/\\\\\\\\/', '', $input); + /** + * Determines if the SQL fragment either breaks into or out of a string literal + * by counting single quotes + * + * Handles double-quote escaped quotes as well as slash escaped quotes + * + * @todo Test this! + * + * @see http://www.postgresql.org/docs/8.3/interactive/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS + * + * @param string $input The SQL fragment + * @return boolean True if the string breaks into or out of a string literal + */ + public function checkStringTogglesLiteral($input) + { + // Remove escaped backslashes, count them! + $input = preg_replace('/\\\\\\\\/', '', $input); - // Count quotes - $totalQuotes = substr_count($input, "'"); // Includes double quote escaped quotes - $escapedQuotes = substr_count($input, "\\'"); - return (($totalQuotes - $escapedQuotes) % 2) !== 0; - } + // Count quotes + $totalQuotes = substr_count($input, "'"); // Includes double quote escaped quotes + $escapedQuotes = substr_count($input, "\\'"); + return (($totalQuotes - $escapedQuotes) % 2) !== 0; + } - /** - * Iteratively replaces all question marks with numerical placeholders - * E.g. "Title = ? AND Name = ?" becomes "Title = $1 AND Name = $2" - * - * @todo Better consider question marks in string literals - * - * @param string $sql Paramaterised query using question mark placeholders - * @return string Paramaterised query using numeric placeholders - */ - public function replacePlaceholders($sql) { - $segments = preg_split('/\?/', $sql); - $joined = ''; - $inString = false; - $num = 0; - for($i = 0; $i < count($segments); $i++) { - // Append next segment - $joined .= $segments[$i]; + /** + * Iteratively replaces all question marks with numerical placeholders + * E.g. "Title = ? AND Name = ?" becomes "Title = $1 AND Name = $2" + * + * @todo Better consider question marks in string literals + * + * @param string $sql Paramaterised query using question mark placeholders + * @return string Paramaterised query using numeric placeholders + */ + public function replacePlaceholders($sql) + { + $segments = preg_split('/\?/', $sql); + $joined = ''; + $inString = false; + $num = 0; + for ($i = 0; $i < count($segments); $i++) { + // Append next segment + $joined .= $segments[$i]; - // Don't add placeholder after last segment - if($i === count($segments) - 1) break; + // Don't add placeholder after last segment + if ($i === count($segments) - 1) { + break; + } - // check string escape on previous fragment - if($this->checkStringTogglesLiteral($segments[$i])) { - $inString = !$inString; - } + // check string escape on previous fragment + if ($this->checkStringTogglesLiteral($segments[$i])) { + $inString = !$inString; + } - // Append placeholder replacement - if($inString) { - $joined .= "?"; - } else { - $joined .= '$' . ++$num; - } - } - return $joined; - } + // Append placeholder replacement + if ($inString) { + $joined .= "?"; + } else { + $joined .= '$' . ++$num; + } + } + return $joined; + } - public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) { - // Reset state - $this->lastQuery = null; - $this->lastRows = 0; + public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) + { + // Reset state + $this->lastQuery = null; + $this->lastRows = 0; - // Replace question mark placeholders with numeric placeholders - if(!empty($parameters)) { - $sql = $this->replacePlaceholders($sql); - $parameters = $this->parameterValues($parameters); - } + // Replace question mark placeholders with numeric placeholders + if (!empty($parameters)) { + $sql = $this->replacePlaceholders($sql); + $parameters = $this->parameterValues($parameters); + } - // Execute query - if(!empty($parameters)) { - $result = pg_query_params($this->dbConn, $sql, $parameters); - } else { - $result = pg_query($this->dbConn, $sql); - } - - // Handle error - if ($result === false) { - $this->databaseError($this->getLastError(), $errorLevel, $sql, $parameters); - return null; - } - - // Save and return results - $this->lastQuery = $result; - $this->lastRows = pg_affected_rows($result); - return new PostgreSQLQuery($result); - } + // Execute query + if (!empty($parameters)) { + $result = pg_query_params($this->dbConn, $sql, $parameters); + } else { + $result = pg_query($this->dbConn, $sql); + } + + // Handle error + if ($result === false) { + $this->databaseError($this->getLastError(), $errorLevel, $sql, $parameters); + return null; + } + + // Save and return results + $this->lastQuery = $result; + $this->lastRows = pg_affected_rows($result); + return new PostgreSQLQuery($result); + } - public function query($sql, $errorLevel = E_USER_ERROR) { - return $this->preparedQuery($sql, array(), $errorLevel); - } + public function query($sql, $errorLevel = E_USER_ERROR) + { + return $this->preparedQuery($sql, array(), $errorLevel); + } - public function quoteString($value) { - if(function_exists('pg_escape_literal')) { - return pg_escape_literal($this->dbConn, $value); - } else { - return "'" . $this->escapeString($value) . "'"; - } - } + public function quoteString($value) + { + if (function_exists('pg_escape_literal')) { + return pg_escape_literal($this->dbConn, $value); + } else { + return "'" . $this->escapeString($value) . "'"; + } + } - public function escapeString($value) { - return pg_escape_string($this->dbConn, $value); - } + public function escapeString($value) + { + return pg_escape_string($this->dbConn, $value); + } - public function escapeIdentifier($value, $separator = '.') { - if(empty($separator) && function_exists('pg_escape_identifier')) { - return pg_escape_identifier($this->dbConn, $value); - } + public function escapeIdentifier($value, $separator = '.') + { + if (empty($separator) && function_exists('pg_escape_identifier')) { + return pg_escape_identifier($this->dbConn, $value); + } - // Let parent function handle recursive calls - return parent::escapeIdentifier ($value, $separator); - } + // Let parent function handle recursive calls + return parent::escapeIdentifier($value, $separator); + } - public function selectDatabase($name) { - if($name !== $this->databaseName) { - user_error("PostgreSQLConnector can't change databases. Please create a new database connection", E_USER_ERROR); - } - return true; - } + public function selectDatabase($name) + { + if ($name !== $this->databaseName) { + user_error("PostgreSQLConnector can't change databases. Please create a new database connection", E_USER_ERROR); + } + return true; + } - public function unloadDatabase() { - $this->databaseName = null; - } + public function unloadDatabase() + { + $this->databaseName = null; + } } diff --git a/code/PostgreSQLDatabase.php b/code/PostgreSQLDatabase.php index cf364c3..efa8752 100644 --- a/code/PostgreSQLDatabase.php +++ b/code/PostgreSQLDatabase.php @@ -6,627 +6,682 @@ * @package sapphire * @subpackage model */ -class PostgreSQLDatabase extends SS_Database { +class PostgreSQLDatabase extends SS_Database +{ - /** - * Database schema manager object - * - * @var PostgreSQLSchemaManager - */ - protected $schemaManager; + /** + * Database schema manager object + * + * @var PostgreSQLSchemaManager + */ + protected $schemaManager; - /** - * The currently selected database schema name. - * - * @var string - */ - protected $schema; + /** + * The currently selected database schema name. + * + * @var string + */ + protected $schema; - protected $supportsTransactions = true; + protected $supportsTransactions = true; - const MASTER_DATABASE = 'postgres'; + const MASTER_DATABASE = 'postgres'; - const MASTER_SCHEMA = 'public'; + const MASTER_SCHEMA = 'public'; - /** - * Full text cluster method. (e.g. GIN or GiST) - * - * @return string - */ - public static function default_fts_cluster_method() { - return Config::inst()->get('PostgreSQLDatabase', 'default_fts_cluster_method'); - } + /** + * Full text cluster method. (e.g. GIN or GiST) + * + * @return string + */ + public static function default_fts_cluster_method() + { + return Config::inst()->get('PostgreSQLDatabase', 'default_fts_cluster_method'); + } - /** - * Full text search method. - * - * @return string - */ - public static function default_fts_search_method() { - return Config::inst()->get('PostgreSQLDatabase', 'default_fts_search_method'); - } + /** + * Full text search method. + * + * @return string + */ + public static function default_fts_search_method() + { + return Config::inst()->get('PostgreSQLDatabase', 'default_fts_search_method'); + } - /** - * Determines whether to check a database exists on the host by - * querying the 'postgres' database and running createDatabase. - * - * Some locked down systems prevent access to the 'postgres' table in - * which case you need to set this to false. - * - * If allow_query_master_postgres is false, and model_schema_as_database is also false, - * then attempts to create or check databases beyond the initial connection will - * result in a runtime error. - */ - public static function allow_query_master_postgres() { - return Config::inst()->get('PostgreSQLDatabase', 'allow_query_master_postgres'); - } + /** + * Determines whether to check a database exists on the host by + * querying the 'postgres' database and running createDatabase. + * + * Some locked down systems prevent access to the 'postgres' table in + * which case you need to set this to false. + * + * If allow_query_master_postgres is false, and model_schema_as_database is also false, + * then attempts to create or check databases beyond the initial connection will + * result in a runtime error. + */ + public static function allow_query_master_postgres() + { + return Config::inst()->get('PostgreSQLDatabase', 'allow_query_master_postgres'); + } - /** - * For instances where multiple databases are used beyond the initial connection - * you may set this option to true to force database switches to switch schemas - * instead of using databases. This may be useful if the database user does not - * have cross-database permissions, and in cases where multiple databases are used - * (such as in running test cases). - * - * If this is true then the database will only be set during the initial connection, - * and attempts to change to this database will use the 'public' schema instead - */ - public static function model_schema_as_database() { - return Config::inst()->get('PostgreSQLDatabase', 'model_schema_as_database'); - } + /** + * For instances where multiple databases are used beyond the initial connection + * you may set this option to true to force database switches to switch schemas + * instead of using databases. This may be useful if the database user does not + * have cross-database permissions, and in cases where multiple databases are used + * (such as in running test cases). + * + * If this is true then the database will only be set during the initial connection, + * and attempts to change to this database will use the 'public' schema instead + */ + public static function model_schema_as_database() + { + return Config::inst()->get('PostgreSQLDatabase', 'model_schema_as_database'); + } - /** - * Override the language that tsearch uses. By default it is 'english, but - * could be any of the supported languages that can be found in the - * pg_catalog.pg_ts_config table. - * - * @var string - */ - public static function search_language() { - return Config::inst()->get('PostgreSQLDatabase', 'search_language'); - } + /** + * Override the language that tsearch uses. By default it is 'english, but + * could be any of the supported languages that can be found in the + * pg_catalog.pg_ts_config table. + * + * @var string + */ + public static function search_language() + { + return Config::inst()->get('PostgreSQLDatabase', 'search_language'); + } - /** - * The database name specified at initial connection - * - * @var string - */ - protected $databaseOriginal = ''; + /** + * The database name specified at initial connection + * + * @var string + */ + protected $databaseOriginal = ''; - /** - * The schema name specified at initial construction. When model_schema_as_database - * is set to true selecting the $databaseOriginal database will instead reset - * the schema to this - * - * @var string - */ - protected $schemaOriginal = ''; + /** + * The schema name specified at initial construction. When model_schema_as_database + * is set to true selecting the $databaseOriginal database will instead reset + * the schema to this + * + * @var string + */ + protected $schemaOriginal = ''; - /** - * Connection parameters specified at inital connection - * - * @var array - */ - protected $parameters = array(); + /** + * Connection parameters specified at inital connection + * + * @var array + */ + protected $parameters = array(); - public function connect($parameters) { - // Check database name - if(empty($parameters['database'])) { - // Check if we can use the master database - if(!self::allow_query_master_postgres()) { - throw new ErrorException('PostegreSQLDatabase::connect called without a database name specified'); - } - // Fallback to master database connection if permission allows - $parameters['database'] = self::MASTER_DATABASE; - } - $this->databaseOriginal = $parameters['database']; + public function connect($parameters) + { + // Check database name + if (empty($parameters['database'])) { + // Check if we can use the master database + if (!self::allow_query_master_postgres()) { + throw new ErrorException('PostegreSQLDatabase::connect called without a database name specified'); + } + // Fallback to master database connection if permission allows + $parameters['database'] = self::MASTER_DATABASE; + } + $this->databaseOriginal = $parameters['database']; - // check schema name - if(empty($parameters['schema'])) { - $parameters['schema'] = self::MASTER_SCHEMA; - } - $this->schemaOriginal = $parameters['schema']; + // check schema name + if (empty($parameters['schema'])) { + $parameters['schema'] = self::MASTER_SCHEMA; + } + $this->schemaOriginal = $parameters['schema']; - // Ensure that driver is available (required by PDO) - if(empty($parameters['driver'])) { - $parameters['driver'] = $this->getDatabaseServer(); - } + // Ensure that driver is available (required by PDO) + if (empty($parameters['driver'])) { + $parameters['driver'] = $this->getDatabaseServer(); + } - // Ensure port number is set (required by postgres) - if(empty($parameters['port'])) { - $parameters['port'] = 5432; - } + // Ensure port number is set (required by postgres) + if (empty($parameters['port'])) { + $parameters['port'] = 5432; + } - $this->parameters = $parameters; + $this->parameters = $parameters; - // If allowed, check that the database exists. Otherwise naively assume - // that the original database exists - if(self::allow_query_master_postgres()) { - // Use master connection to setup initial schema - $this->connectMaster(); - if(!$this->schemaManager->postgresDatabaseExists($this->databaseOriginal)) { - $this->schemaManager->createPostgresDatabase($this->databaseOriginal); - } - } + // If allowed, check that the database exists. Otherwise naively assume + // that the original database exists + if (self::allow_query_master_postgres()) { + // Use master connection to setup initial schema + $this->connectMaster(); + if (!$this->schemaManager->postgresDatabaseExists($this->databaseOriginal)) { + $this->schemaManager->createPostgresDatabase($this->databaseOriginal); + } + } - // Connect to the actual database we're requesting - $this->connectDefault(); + // Connect to the actual database we're requesting + $this->connectDefault(); - // Set up the schema if required - $this->setSchema($this->schemaOriginal, true); + // Set up the schema if required + $this->setSchema($this->schemaOriginal, true); - // Set the timezone if required. - if (isset($parameters['timezone'])) { - $this->selectTimezone($parameters['timezone']); - } - } + // Set the timezone if required. + if (isset($parameters['timezone'])) { + $this->selectTimezone($parameters['timezone']); + } + } - protected function connectMaster() { - $parameters = $this->parameters; - $parameters['database'] = self::MASTER_DATABASE; - $this->connector->connect($parameters, true); - } + protected function connectMaster() + { + $parameters = $this->parameters; + $parameters['database'] = self::MASTER_DATABASE; + $this->connector->connect($parameters, true); + } - protected function connectDefault() { - $parameters = $this->parameters; - $parameters['database'] = $this->databaseOriginal; - $this->connector->connect($parameters, true); - } + protected function connectDefault() + { + $parameters = $this->parameters; + $parameters['database'] = $this->databaseOriginal; + $this->connector->connect($parameters, true); + } - /** - * Sets the system timezone for the database connection - * - * @param string $timezone - */ - public function selectTimezone($timezone) { - if (empty($timezone)) return; - $this->query("SET SESSION TIME ZONE '$timezone';"); - } + /** + * Sets the system timezone for the database connection + * + * @param string $timezone + */ + public function selectTimezone($timezone) + { + if (empty($timezone)) { + return; + } + $this->query("SET SESSION TIME ZONE '$timezone';"); + } - public function supportsCollations() { - return true; - } + public function supportsCollations() + { + return true; + } - public function supportsTimezoneOverride() { - return true; - } + public function supportsTimezoneOverride() + { + return true; + } - public function getDatabaseServer() { - return "postgresql"; - } + public function getDatabaseServer() + { + return "postgresql"; + } - /** - * Returns the name of the current schema in use - * - * @return string Name of current schema - */ - public function currentSchema() { - return $this->schema; - } + /** + * Returns the name of the current schema in use + * + * @return string Name of current schema + */ + public function currentSchema() + { + return $this->schema; + } - /** - * Utility method to manually set the schema to an alternative - * Check existance & sets search path to the supplied schema name - * - * @param string $name Name of the schema - * @param boolean $create Flag indicating whether the schema should be created - * if it doesn't exist. If $create is false and the schema doesn't exist - * then an error will be raised - * @param int|boolean $errorLevel The level of error reporting to enable for - * the query, or false if no error should be raised - * @return boolean Flag indicating success - */ - public function setSchema($schema, $create = false, $errorLevel = E_USER_ERROR) { - if(!$this->schemaManager->schemaExists($schema)) { - // Check DB creation permisson - if (!$create) { - if ($errorLevel !== false) { - user_error("Schema $schema does not exist", $errorLevel); - } - $this->schema = null; - return false; - } - $this->schemaManager->createSchema($schema); - } - $this->setSchemaSearchPath($schema); - $this->schema = $schema; - return true; - } + /** + * Utility method to manually set the schema to an alternative + * Check existance & sets search path to the supplied schema name + * + * @param string $name Name of the schema + * @param boolean $create Flag indicating whether the schema should be created + * if it doesn't exist. If $create is false and the schema doesn't exist + * then an error will be raised + * @param int|boolean $errorLevel The level of error reporting to enable for + * the query, or false if no error should be raised + * @return boolean Flag indicating success + */ + public function setSchema($schema, $create = false, $errorLevel = E_USER_ERROR) + { + if (!$this->schemaManager->schemaExists($schema)) { + // Check DB creation permisson + if (!$create) { + if ($errorLevel !== false) { + user_error("Schema $schema does not exist", $errorLevel); + } + $this->schema = null; + return false; + } + $this->schemaManager->createSchema($schema); + } + $this->setSchemaSearchPath($schema); + $this->schema = $schema; + return true; + } - /** - * Override the schema search path. Search using the arguments supplied. - * NOTE: The search path is normally set through setSchema() and only - * one schema is selected. The facility to add more than one schema to - * the search path is provided as an advanced PostgreSQL feature for raw - * SQL queries. Sapphire cannot search for datamodel tables in alternate - * schemas, so be wary of using alternate schemas within the ORM environment. - * - * @param string $arg1 First schema to use - * @param string $arg2 Second schema to use - * @param string $argN Nth schema to use - */ - public function setSchemaSearchPath() { - if(func_num_args() == 0) { - user_error('At least one Schema must be supplied to set a search path.', E_USER_ERROR); - } - $schemas = array_values(func_get_args()); - $this->query("SET search_path TO \"" . implode("\",\"", $schemas) . "\""); - } + /** + * Override the schema search path. Search using the arguments supplied. + * NOTE: The search path is normally set through setSchema() and only + * one schema is selected. The facility to add more than one schema to + * the search path is provided as an advanced PostgreSQL feature for raw + * SQL queries. Sapphire cannot search for datamodel tables in alternate + * schemas, so be wary of using alternate schemas within the ORM environment. + * + * @param string $arg1 First schema to use + * @param string $arg2 Second schema to use + * @param string $argN Nth schema to use + */ + public function setSchemaSearchPath() + { + if (func_num_args() == 0) { + user_error('At least one Schema must be supplied to set a search path.', E_USER_ERROR); + } + $schemas = array_values(func_get_args()); + $this->query("SET search_path TO \"" . implode("\",\"", $schemas) . "\""); + } - /** - * The core search engine configuration. - * @todo Properly extract the search functions out of the core. - * - * @param string $keywords Keywords as a space separated string - * @return object DataObjectSet of result pages - */ - public function searchEngine($classesToSearch, $keywords, $start, $pageLength, $sortBy = "ts_rank DESC", $extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false) { - //Fix the keywords to be ts_query compatitble: - //Spaces must have pipes - //@TODO: properly handle boolean operators here. - $keywords= trim($keywords); - $keywords= str_replace(' ', ' | ', $keywords); - $keywords= str_replace('"', "'", $keywords); + /** + * The core search engine configuration. + * @todo Properly extract the search functions out of the core. + * + * @param string $keywords Keywords as a space separated string + * @return object DataObjectSet of result pages + */ + public function searchEngine($classesToSearch, $keywords, $start, $pageLength, $sortBy = "ts_rank DESC", $extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false) + { + //Fix the keywords to be ts_query compatitble: + //Spaces must have pipes + //@TODO: properly handle boolean operators here. + $keywords= trim($keywords); + $keywords= str_replace(' ', ' | ', $keywords); + $keywords= str_replace('"', "'", $keywords); - $keywords = $this->quoteString(trim($keywords)); + $keywords = $this->quoteString(trim($keywords)); - //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); - $result = $this->preparedQuery(" + //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); + $result = $this->preparedQuery(" SELECT table_name, column_name, data_type FROM information_schema.columns WHERE data_type='tsvector' AND table_name in ($classesPlaceholders);", - $classesToSearch - ); - if (!$result->numRecords()) throw new Exception('there are no full text columns to search'); + $classesToSearch + ); + if (!$result->numRecords()) { + throw new Exception('there are no full text columns to search'); + } - $tables = array(); - $tableParameters = array(); + $tables = array(); + $tableParameters = array(); - // Make column selection lists - $select = array( - 'SiteTree' => array( - '"ClassName"', - '"SiteTree"."ID"', - '"ParentID"', - '"Title"', - '"URLSegment"', - '"Content"', - '"LastEdited"', - '"Created"', - 'NULL AS "Name"', - '"CanViewType"' - ), - 'File' => array( - '"ClassName"', - '"File"."ID"', - '0 AS "ParentID"', - '"Title"', - 'NULL AS "URLSegment"', - 'NULL AS "Content"', - '"LastEdited"', - '"Created"', - '"Name"', - 'NULL AS "CanViewType"' - ) - ); + // Make column selection lists + $select = array( + 'SiteTree' => array( + '"ClassName"', + '"SiteTree"."ID"', + '"ParentID"', + '"Title"', + '"URLSegment"', + '"Content"', + '"LastEdited"', + '"Created"', + 'NULL AS "Name"', + '"CanViewType"' + ), + 'File' => array( + '"ClassName"', + '"File"."ID"', + '0 AS "ParentID"', + '"Title"', + 'NULL AS "URLSegment"', + 'NULL AS "Content"', + '"LastEdited"', + '"Created"', + '"Name"', + 'NULL AS "CanViewType"' + ) + ); - foreach($result as $row){ - $conditions = array(); - if($row['table_name'] === 'SiteTree' || $row['table_name'] === 'File') { - $conditions[] = array('"ShowInSearch"' => 1); - } + foreach ($result as $row) { + $conditions = array(); + if ($row['table_name'] === 'SiteTree' || $row['table_name'] === 'File') { + $conditions[] = array('"ShowInSearch"' => 1); + } - $method = self::default_fts_search_method(); - $conditions[] = "\"{$row['table_name']}\".\"{$row['column_name']}\" $method q "; - $query = DataObject::get($row['table_name'], $conditions)->dataQuery()->query(); + $method = self::default_fts_search_method(); + $conditions[] = "\"{$row['table_name']}\".\"{$row['column_name']}\" $method q "; + $query = DataObject::get($row['table_name'], $conditions)->dataQuery()->query(); - // Could parameterise this, but convention is only to to so for where conditions - $query->addFrom(array( - 'tsearch' => ", to_tsquery('" . self::search_language() . "', $keywords) AS q" - )); - $query->setSelect(array()); + // Could parameterise this, but convention is only to to so for where conditions + $query->addFrom(array( + 'tsearch' => ", to_tsquery('" . self::search_language() . "', $keywords) AS q" + )); + $query->setSelect(array()); - foreach($select[$row['table_name']] as $clause) { - if(preg_match('/^(.*) +AS +"?([^"]*)"?/i', $clause, $matches)) { - $query->selectField($matches[1], $matches[2]); - } else { - $query->selectField($clause); - } - } + foreach ($select[$row['table_name']] as $clause) { + if (preg_match('/^(.*) +AS +"?([^"]*)"?/i', $clause, $matches)) { + $query->selectField($matches[1], $matches[2]); + } else { + $query->selectField($clause); + } + } - $query->selectField("ts_rank(\"{$row['table_name']}\".\"{$row['column_name']}\", q)", 'Relevance'); - $query->setOrderBy(array()); + $query->selectField("ts_rank(\"{$row['table_name']}\".\"{$row['column_name']}\", q)", 'Relevance'); + $query->setOrderBy(array()); - //Add this query to the collection - $tables[] = $query->sql($parameters); - $tableParameters = array_merge($tableParameters, $parameters); - } + //Add this query to the collection + $tables[] = $query->sql($parameters); + $tableParameters = array_merge($tableParameters, $parameters); + } - $limit = $pageLength; - $offset = $start; + $limit = $pageLength; + $offset = $start; - if($keywords) $orderBy = " ORDER BY $sortBy"; - else $orderBy=''; + if ($keywords) { + $orderBy = " ORDER BY $sortBy"; + } else { + $orderBy=''; + } - $fullQuery = "SELECT * FROM (" . implode(" UNION ", $tables) . ") AS q1 $orderBy LIMIT $limit OFFSET $offset"; + $fullQuery = "SELECT * FROM (" . implode(" UNION ", $tables) . ") AS q1 $orderBy LIMIT $limit OFFSET $offset"; - // Get records - $records = $this->preparedQuery($fullQuery, $tableParameters); - $totalCount=0; - foreach($records as $record){ - $objects[] = new $record['ClassName']($record); - $totalCount++; - } + // Get records + $records = $this->preparedQuery($fullQuery, $tableParameters); + $totalCount=0; + foreach ($records as $record) { + $objects[] = new $record['ClassName']($record); + $totalCount++; + } - if(isset($objects)) $results = new ArrayList($objects); - else $results = new ArrayList(); - $list = new PaginatedList($results); - $list->setLimitItems(false); - $list->setPageStart($start); - $list->setPageLength($pageLength); - $list->setTotalItems($totalCount); - return $list; - } + if (isset($objects)) { + $results = new ArrayList($objects); + } else { + $results = new ArrayList(); + } + $list = new PaginatedList($results); + $list->setLimitItems(false); + $list->setPageStart($start); + $list->setPageLength($pageLength); + $list->setTotalItems($totalCount); + return $list; + } - public function supportsTransactions() { - return $this->supportsTransactions; - } + public function supportsTransactions() + { + return $this->supportsTransactions; + } - /* - * This is a quick lookup to discover if the database supports particular extensions - */ - public function supportsExtensions($extensions=Array('partitions', 'tablespaces', 'clustering')){ - if(isset($extensions['partitions'])) return true; - elseif(isset($extensions['tablespaces'])) return true; - elseif(isset($extensions['clustering'])) return true; - else return false; - } + /* + * This is a quick lookup to discover if the database supports particular extensions + */ + public function supportsExtensions($extensions=array('partitions', 'tablespaces', 'clustering')) + { + if (isset($extensions['partitions'])) { + return true; + } elseif (isset($extensions['tablespaces'])) { + return true; + } elseif (isset($extensions['clustering'])) { + return true; + } else { + return false; + } + } - public function transactionStart($transaction_mode = false, $session_characteristics = false){ - $this->query('BEGIN;'); + public function transactionStart($transaction_mode = false, $session_characteristics = false) + { + $this->query('BEGIN;'); - if($transaction_mode) { - $this->query("SET TRANSACTION {$transaction_mode};"); - } + if ($transaction_mode) { + $this->query("SET TRANSACTION {$transaction_mode};"); + } - if($session_characteristics) { - $this->query("SET SESSION CHARACTERISTICS AS TRANSACTION {$session_characteristics};"); - } - } + if ($session_characteristics) { + $this->query("SET SESSION CHARACTERISTICS AS TRANSACTION {$session_characteristics};"); + } + } - public function transactionSavepoint($savepoint){ - $this->query("SAVEPOINT {$savepoint};"); - } + public function transactionSavepoint($savepoint) + { + $this->query("SAVEPOINT {$savepoint};"); + } - public function transactionRollback($savepoint = false){ - if($savepoint) { - $this->query("ROLLBACK TO {$savepoint};"); - } else { - $this->query('ROLLBACK;'); - } - } + public function transactionRollback($savepoint = false) + { + if ($savepoint) { + $this->query("ROLLBACK TO {$savepoint};"); + } else { + $this->query('ROLLBACK;'); + } + } - public function transactionEnd($chain = false){ - $this->query('COMMIT;'); - } + public function transactionEnd($chain = false) + { + $this->query('COMMIT;'); + } - public function comparisonClause($field, $value, $exact = false, $negate = false, $caseSensitive = null, $parameterised = false) { - if($exact && $caseSensitive === null) { - $comp = ($negate) ? '!=' : '='; - } else { - $comp = ($caseSensitive === true) ? 'LIKE' : 'ILIKE'; - if($negate) $comp = 'NOT ' . $comp; - $field.='::text'; - } + public function comparisonClause($field, $value, $exact = false, $negate = false, $caseSensitive = null, $parameterised = false) + { + if ($exact && $caseSensitive === null) { + $comp = ($negate) ? '!=' : '='; + } else { + $comp = ($caseSensitive === true) ? 'LIKE' : 'ILIKE'; + if ($negate) { + $comp = 'NOT ' . $comp; + } + $field.='::text'; + } - if($parameterised) { - return sprintf("%s %s ?", $field, $comp); - } else { - return sprintf("%s %s '%s'", $field, $comp, $value); - } - } + if ($parameterised) { + return sprintf("%s %s ?", $field, $comp); + } else { + return sprintf("%s %s '%s'", $field, $comp, $value); + } + } - /** - * Function to return an SQL datetime expression that can be used with Postgres - * used for querying a datetime in a certain format - * @param string $date to be formated, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"' - * @param string $format to be used, supported specifiers: - * %Y = Year (four digits) - * %m = Month (01..12) - * %d = Day (01..31) - * %H = Hour (00..23) - * %i = Minutes (00..59) - * %s = Seconds (00..59) - * %U = unix timestamp, can only be used on it's own - * @return string SQL datetime expression to query for a formatted datetime - */ - public function formattedDatetimeClause($date, $format) { - preg_match_all('/%(.)/', $format, $matches); - foreach($matches[1] as $match) { - if(array_search($match, array('Y','m','d','H','i','s','U')) === false) { - user_error('formattedDatetimeClause(): unsupported format character %' . $match, E_USER_WARNING); - } - } + /** + * Function to return an SQL datetime expression that can be used with Postgres + * used for querying a datetime in a certain format + * @param string $date to be formated, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"' + * @param string $format to be used, supported specifiers: + * %Y = Year (four digits) + * %m = Month (01..12) + * %d = Day (01..31) + * %H = Hour (00..23) + * %i = Minutes (00..59) + * %s = Seconds (00..59) + * %U = unix timestamp, can only be used on it's own + * @return string SQL datetime expression to query for a formatted datetime + */ + public function formattedDatetimeClause($date, $format) + { + preg_match_all('/%(.)/', $format, $matches); + foreach ($matches[1] as $match) { + if (array_search($match, array('Y', 'm', 'd', 'H', 'i', 's', 'U')) === false) { + user_error('formattedDatetimeClause(): unsupported format character %' . $match, E_USER_WARNING); + } + } - $translate = array( - '/%Y/' => 'YYYY', - '/%m/' => 'MM', - '/%d/' => 'DD', - '/%H/' => 'HH24', - '/%i/' => 'MI', - '/%s/' => 'SS', - ); - $format = preg_replace(array_keys($translate), array_values($translate), $format); + $translate = array( + '/%Y/' => 'YYYY', + '/%m/' => 'MM', + '/%d/' => 'DD', + '/%H/' => 'HH24', + '/%i/' => 'MI', + '/%s/' => 'SS', + ); + $format = preg_replace(array_keys($translate), array_values($translate), $format); - if(preg_match('/^now$/i', $date)) { - $date = "NOW()"; - } else if(preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) { - $date = "TIMESTAMP '$date'"; - } + if (preg_match('/^now$/i', $date)) { + $date = "NOW()"; + } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) { + $date = "TIMESTAMP '$date'"; + } - if($format == '%U') return "FLOOR(EXTRACT(epoch FROM $date))"; + if ($format == '%U') { + return "FLOOR(EXTRACT(epoch FROM $date))"; + } - return "to_char($date, TEXT '$format')"; + return "to_char($date, TEXT '$format')"; + } - } + /** + * Function to return an SQL datetime expression that can be used with Postgres + * used for querying a datetime addition + * @param string $date, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"' + * @param string $interval to be added, use the format [sign][integer] [qualifier], e.g. -1 Day, +15 minutes, +1 YEAR + * supported qualifiers: + * - years + * - months + * - days + * - hours + * - minutes + * - seconds + * This includes the singular forms as well + * @return string SQL datetime expression to query for a datetime (YYYY-MM-DD hh:mm:ss) which is the result of the addition + */ + public function datetimeIntervalClause($date, $interval) + { + if (preg_match('/^now$/i', $date)) { + $date = "NOW()"; + } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) { + $date = "TIMESTAMP '$date'"; + } - /** - * Function to return an SQL datetime expression that can be used with Postgres - * used for querying a datetime addition - * @param string $date, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"' - * @param string $interval to be added, use the format [sign][integer] [qualifier], e.g. -1 Day, +15 minutes, +1 YEAR - * supported qualifiers: - * - years - * - months - * - days - * - hours - * - minutes - * - seconds - * This includes the singular forms as well - * @return string SQL datetime expression to query for a datetime (YYYY-MM-DD hh:mm:ss) which is the result of the addition - */ - public function datetimeIntervalClause($date, $interval) { - if(preg_match('/^now$/i', $date)) { - $date = "NOW()"; - } else if(preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) { - $date = "TIMESTAMP '$date'"; - } + // ... when being too precise becomes a pain. we need to cut of the fractions. + // TIMESTAMP(0) doesn't work because it rounds instead flooring + return "CAST(SUBSTRING(CAST($date + INTERVAL '$interval' AS VARCHAR) FROM 1 FOR 19) AS TIMESTAMP)"; + } - // ... when being too precise becomes a pain. we need to cut of the fractions. - // TIMESTAMP(0) doesn't work because it rounds instead flooring - return "CAST(SUBSTRING(CAST($date + INTERVAL '$interval' AS VARCHAR) FROM 1 FOR 19) AS TIMESTAMP)"; - } + /** + * Function to return an SQL datetime expression that can be used with Postgres + * used for querying a datetime substraction + * @param string $date1, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"' + * @param string $date2 to be substracted of $date1, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"' + * @return string SQL datetime expression to query for the interval between $date1 and $date2 in seconds which is the result of the substraction + */ + public function datetimeDifferenceClause($date1, $date2) + { + if (preg_match('/^now$/i', $date1)) { + $date1 = "NOW()"; + } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date1)) { + $date1 = "TIMESTAMP '$date1'"; + } - /** - * Function to return an SQL datetime expression that can be used with Postgres - * used for querying a datetime substraction - * @param string $date1, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"' - * @param string $date2 to be substracted of $date1, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"' - * @return string SQL datetime expression to query for the interval between $date1 and $date2 in seconds which is the result of the substraction - */ - public function datetimeDifferenceClause($date1, $date2) { - if(preg_match('/^now$/i', $date1)) { - $date1 = "NOW()"; - } else if(preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date1)) { - $date1 = "TIMESTAMP '$date1'"; - } + if (preg_match('/^now$/i', $date2)) { + $date2 = "NOW()"; + } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date2)) { + $date2 = "TIMESTAMP '$date2'"; + } - if(preg_match('/^now$/i', $date2)) { - $date2 = "NOW()"; - } else if(preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date2)) { - $date2 = "TIMESTAMP '$date2'"; - } + return "(FLOOR(EXTRACT(epoch FROM $date1)) - FLOOR(EXTRACT(epoch from $date2)))"; + } - return "(FLOOR(EXTRACT(epoch FROM $date1)) - FLOOR(EXTRACT(epoch from $date2)))"; - } + public function now() + { + return 'NOW()'; + } - function now(){ - return 'NOW()'; - } + public function random() + { + return 'RANDOM()'; + } - function random(){ - return 'RANDOM()'; - } + /** + * Determines the name of the current database to be reported externally + * by substituting the schema name for the database name. + * Should only be used when model_schema_as_database is true + * + * @param string $schema Name of the schema + * @return string Name of the database to report + */ + public function schemaToDatabaseName($schema) + { + switch ($schema) { + case $this->schemaOriginal: return $this->databaseOriginal; + default: return $schema; + } + } - /** - * Determines the name of the current database to be reported externally - * by substituting the schema name for the database name. - * Should only be used when model_schema_as_database is true - * - * @param string $schema Name of the schema - * @return string Name of the database to report - */ - public function schemaToDatabaseName($schema) { - switch($schema) { - case $this->schemaOriginal: return $this->databaseOriginal; - default: return $schema; - } - } + /** + * Translates a requested database name to a schema name to substitute internally. + * Should only be used when model_schema_as_database is true + * + * @param string $database Name of the database + * @return string Name of the schema to use for this database internally + */ + public function databaseToSchemaName($database) + { + switch ($database) { + case $this->databaseOriginal: return $this->schemaOriginal; + default: return $database; + } + } - /** - * Translates a requested database name to a schema name to substitute internally. - * Should only be used when model_schema_as_database is true - * - * @param string $database Name of the database - * @return string Name of the schema to use for this database internally - */ - public function databaseToSchemaName($database) { - switch($database) { - case $this->databaseOriginal: return $this->schemaOriginal; - default: return $database; - } - } + public function dropSelectedDatabase() + { + if (self::model_schema_as_database()) { + // Check current schema is valid + $oldSchema = $this->schema; + if (empty($oldSchema)) { + return true; + } // Nothing selected to drop - public function dropSelectedDatabase() { - if(self::model_schema_as_database()) { - // Check current schema is valid - $oldSchema = $this->schema; - if(empty($oldSchema)) return true; // Nothing selected to drop + // Select another schema + if ($oldSchema !== $this->schemaOriginal) { + $this->setSchema($this->schemaOriginal); + } elseif ($oldSchema !== self::MASTER_SCHEMA) { + $this->setSchema(self::MASTER_SCHEMA); + } else { + $this->schema = null; + } - // Select another schema - if($oldSchema !== $this->schemaOriginal) { - $this->setSchema($this->schemaOriginal); - } elseif($oldSchema !== self::MASTER_SCHEMA) { - $this->setSchema(self::MASTER_SCHEMA); - } else { - $this->schema = null; - } + // Remove this schema + $this->schemaManager->dropSchema($oldSchema); + } else { + parent::dropSelectedDatabase(); + } + } - // Remove this schema - $this->schemaManager->dropSchema($oldSchema); - } else { - parent::dropSelectedDatabase(); - } - } + public function getSelectedDatabase() + { + if (self::model_schema_as_database()) { + return $this->schemaToDatabaseName($this->schema); + } + return parent::getSelectedDatabase(); + } - public function getSelectedDatabase() { - if(self::model_schema_as_database()) { - return $this->schemaToDatabaseName($this->schema); - } - return parent::getSelectedDatabase(); - } + public function selectDatabase($name, $create = false, $errorLevel = E_USER_ERROR) + { + // Substitute schema here as appropriate + if (self::model_schema_as_database()) { + // Selecting the database itself should be treated as selecting the public schema + $schemaName = $this->databaseToSchemaName($name); + return $this->setSchema($schemaName, $create, $errorLevel); + } - public function selectDatabase($name, $create = false, $errorLevel = E_USER_ERROR) { - // Substitute schema here as appropriate - if(self::model_schema_as_database()) { - // Selecting the database itself should be treated as selecting the public schema - $schemaName = $this->databaseToSchemaName($name); - return $this->setSchema($schemaName, $create, $errorLevel); - } + // Database selection requires that a new connection is established. + // This is not ideal postgres practise + if (!$this->schemaManager->databaseExists($name)) { + // Check DB creation permisson + if (!$create) { + if ($errorLevel !== false) { + user_error("Attempted to connect to non-existing database \"$name\"", $errorLevel); + } + // Unselect database + $this->connector->unloadDatabase(); + return false; + } + $this->schemaManager->createDatabase($name); + } - // Database selection requires that a new connection is established. - // This is not ideal postgres practise - if (!$this->schemaManager->databaseExists($name)) { - // Check DB creation permisson - if (!$create) { - if ($errorLevel !== false) { - user_error("Attempted to connect to non-existing database \"$name\"", $errorLevel); - } - // Unselect database - $this->connector->unloadDatabase(); - return false; - } - $this->schemaManager->createDatabase($name); - } + // New connection made here, treating the new database name as the new original + $this->databaseOriginal = $name; + $this->connectDefault(); + } - // New connection made here, treating the new database name as the new original - $this->databaseOriginal = $name; - $this->connectDefault(); - } - - /** - * Delete all entries from the table instead of truncating it. - * - * This gives a massive speed improvement compared to using TRUNCATE, with - * the caveat that primary keys are not reset etc. - * - * @see DatabaseAdmin::clearAllData() - * - * @param string $table - */ - public function clearTable($table) { - $this->query('DELETE FROM "'.$table.'";'); - } + /** + * Delete all entries from the table instead of truncating it. + * + * This gives a massive speed improvement compared to using TRUNCATE, with + * the caveat that primary keys are not reset etc. + * + * @see DatabaseAdmin::clearAllData() + * + * @param string $table + */ + public function clearTable($table) + { + $this->query('DELETE FROM "'.$table.'";'); + } } diff --git a/code/PostgreSQLDatabaseConfigurationHelper.php b/code/PostgreSQLDatabaseConfigurationHelper.php index 03ba04c..033ed2f 100644 --- a/code/PostgreSQLDatabaseConfigurationHelper.php +++ b/code/PostgreSQLDatabaseConfigurationHelper.php @@ -7,187 +7,198 @@ * * @package postgresql */ -class PostgreSQLDatabaseConfigurationHelper implements DatabaseConfigurationHelper { - - /** - * Create a connection of the appropriate type - * - * @param array $databaseConfig - * @param string $error Error message passed by value - * @return mixed|null Either the connection object, or null if error - */ - protected function createConnection($databaseConfig, &$error) { - $error = null; - $username = empty($databaseConfig['username']) ? '' : $databaseConfig['username']; - $password = empty($databaseConfig['password']) ? '' : $databaseConfig['password']; - $server = $databaseConfig['server']; - - try { - switch($databaseConfig['type']) { - case 'PostgreSQLDatabase': - $userPart = $username ? " user=$username" : ''; - $passwordPart = $password ? " password=$password" : ''; - $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'; - return null; - } - } catch(Exception $ex) { - $error = $ex->getMessage(); - return null; - } - if($conn) { - return $conn; - } else { - $error = 'PostgreSQL requires a valid username and password to determine if the server exists.'; - return null; - } - } +class PostgreSQLDatabaseConfigurationHelper implements DatabaseConfigurationHelper +{ + + /** + * Create a connection of the appropriate type + * + * @param array $databaseConfig + * @param string $error Error message passed by value + * @return mixed|null Either the connection object, or null if error + */ + protected function createConnection($databaseConfig, &$error) + { + $error = null; + $username = empty($databaseConfig['username']) ? '' : $databaseConfig['username']; + $password = empty($databaseConfig['password']) ? '' : $databaseConfig['password']; + $server = $databaseConfig['server']; + + try { + switch ($databaseConfig['type']) { + case 'PostgreSQLDatabase': + $userPart = $username ? " user=$username" : ''; + $passwordPart = $password ? " password=$password" : ''; + $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'; + return null; + } + } catch (Exception $ex) { + $error = $ex->getMessage(); + return null; + } + if ($conn) { + return $conn; + } else { + $error = 'PostgreSQL requires a valid username and password to determine if the server exists.'; + return null; + } + } - public function requireDatabaseFunctions($databaseConfig) { - $data = DatabaseAdapterRegistry::get_adapter($databaseConfig['type']); - return !empty($data['supported']); - } + public function requireDatabaseFunctions($databaseConfig) + { + $data = DatabaseAdapterRegistry::get_adapter($databaseConfig['type']); + return !empty($data['supported']); + } - public function requireDatabaseServer($databaseConfig) { - $conn = $this->createConnection($databaseConfig, $error); - $success = !empty($conn); - return array( - 'success' => $success, - 'error' => $error - ); - } + public function requireDatabaseServer($databaseConfig) + { + $conn = $this->createConnection($databaseConfig, $error); + $success = !empty($conn); + return array( + 'success' => $success, + 'error' => $error + ); + } - public function requireDatabaseConnection($databaseConfig) { - $conn = $this->createConnection($databaseConfig, $error); - $success = !empty($conn); - return array( - 'success' => $success, - 'connection' => $conn, - 'error' => $error - ); - } + public function requireDatabaseConnection($databaseConfig) + { + $conn = $this->createConnection($databaseConfig, $error); + $success = !empty($conn); + return array( + 'success' => $success, + 'connection' => $conn, + 'error' => $error + ); + } - public function getDatabaseVersion($databaseConfig) { - $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']; - } else { - return false; - } - } + public function getDatabaseVersion($databaseConfig) + { + $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']; + } else { + return false; + } + } - /** - * Ensure that the PostgreSQL version is at least 8.3. - * - * @param array $databaseConfig Associative array of db configuration, e.g. "server", "username" etc - * @return array Result - e.g. array('success' => true, 'error' => 'details of error') - */ - public function requireDatabaseVersion($databaseConfig) { - $success = false; - $error = ''; - $version = $this->getDatabaseVersion($databaseConfig); + /** + * Ensure that the PostgreSQL version is at least 8.3. + * + * @param array $databaseConfig Associative array of db configuration, e.g. "server", "username" etc + * @return array Result - e.g. array('success' => true, 'error' => 'details of error') + */ + public function requireDatabaseVersion($databaseConfig) + { + $success = false; + $error = ''; + $version = $this->getDatabaseVersion($databaseConfig); - if($version) { - $success = version_compare($version, '8.3', '>='); - if(!$success) { - $error = "Your PostgreSQL version is $version. It's recommended you use at least 8.3."; - } - } else { - $error = "Your PostgreSQL version could not be determined."; - } + if ($version) { + $success = version_compare($version, '8.3', '>='); + if (!$success) { + $error = "Your PostgreSQL version is $version. It's recommended you use at least 8.3."; + } + } else { + $error = "Your PostgreSQL version could not be determined."; + } - return array( - 'success' => $success, - 'error' => $error - ); - } - - /** - * Helper function to quote a string value - * - * @param mixed $conn Connection object/resource - * @param string $value Value to quote - * @return string Quoted strieng - */ - protected function quote($conn, $value) { - if($conn instanceof PDO) { - return $conn->quote($value); - } elseif(is_resource($conn)) { - return "'".pg_escape_string($conn, $value)."'"; - } else { - user_error('Invalid database connection', E_USER_ERROR); - } - } - - /** - * Helper function to execute a query - * - * @param mixed $conn Connection object/resource - * @param string $sql SQL string to execute - * @return array List of first value from each resulting row - */ - protected function query($conn, $sql) { - $items = array(); - 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]; - } - } - return $items; - } + return array( + 'success' => $success, + 'error' => $error + ); + } + + /** + * Helper function to quote a string value + * + * @param mixed $conn Connection object/resource + * @param string $value Value to quote + * @return string Quoted strieng + */ + protected function quote($conn, $value) + { + if ($conn instanceof PDO) { + return $conn->quote($value); + } elseif (is_resource($conn)) { + return "'".pg_escape_string($conn, $value)."'"; + } else { + user_error('Invalid database connection', E_USER_ERROR); + } + } + + /** + * Helper function to execute a query + * + * @param mixed $conn Connection object/resource + * @param string $sql SQL string to execute + * @return array List of first value from each resulting row + */ + protected function query($conn, $sql) + { + $items = array(); + 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]; + } + } + return $items; + } - public function requireDatabaseOrCreatePermissions($databaseConfig) { - $success = false; - $alreadyExists = false; - $conn = $this->createConnection($databaseConfig, $error); - if($conn) { - // Check if db already exists - $existingDatabases = $this->query($conn, "SELECT datname FROM pg_database"); - $alreadyExists = in_array($databaseConfig['database'], $existingDatabases); - if($alreadyExists) { - $success = true; - } else { - // Check if this user has create privileges - $allowedUsers = $this->query($conn, "select rolname from pg_authid where rolcreatedb = true;"); - $success = in_array($databaseConfig['username'], $allowedUsers); - } - } - - return array( - 'success' => $success, - 'alreadyExists' => $alreadyExists - ); - } + public function requireDatabaseOrCreatePermissions($databaseConfig) + { + $success = false; + $alreadyExists = false; + $conn = $this->createConnection($databaseConfig, $error); + if ($conn) { + // Check if db already exists + $existingDatabases = $this->query($conn, "SELECT datname FROM pg_database"); + $alreadyExists = in_array($databaseConfig['database'], $existingDatabases); + if ($alreadyExists) { + $success = true; + } else { + // Check if this user has create privileges + $allowedUsers = $this->query($conn, "select rolname from pg_authid where rolcreatedb = true;"); + $success = in_array($databaseConfig['username'], $allowedUsers); + } + } + + return array( + 'success' => $success, + 'alreadyExists' => $alreadyExists + ); + } - public function requireDatabaseAlterPermissions($databaseConfig) { - $conn = $this->createConnection($databaseConfig, $error); - if($conn) { - // if the account can even log in, it can alter tables - return array( - 'success' => true, - 'applies' => true - ); - } - return array( - 'success' => false, - 'applies' => true - ); - } + public function requireDatabaseAlterPermissions($databaseConfig) + { + $conn = $this->createConnection($databaseConfig, $error); + if ($conn) { + // if the account can even log in, it can alter tables + return array( + 'success' => true, + 'applies' => true + ); + } + return array( + 'success' => false, + 'applies' => true + ); + } } diff --git a/code/PostgreSQLQuery.php b/code/PostgreSQLQuery.php index f51d686..278f9a5 100644 --- a/code/PostgreSQLQuery.php +++ b/code/PostgreSQLQuery.php @@ -6,36 +6,44 @@ * @package sapphire * @subpackage model */ -class PostgreSQLQuery extends SS_Query { +class PostgreSQLQuery extends SS_Query +{ - /** - * The internal Postgres handle that points to the result set. - * @var resource - */ - private $handle; + /** + * The internal Postgres handle that points to the result set. + * @var resource + */ + private $handle; - /** - * Hook the result-set given into a Query class, suitable for use by sapphire. - * @param database The database object that created this query. - * @param handle the internal Postgres handle that is points to the resultset. - */ - public function __construct($handle) { - $this->handle = $handle; - } + /** + * Hook the result-set given into a Query class, suitable for use by sapphire. + * @param database The database object that created this query. + * @param handle the internal Postgres handle that is points to the resultset. + */ + public function __construct($handle) + { + $this->handle = $handle; + } - public function __destruct() { - if(is_resource($this->handle)) pg_free_result($this->handle); - } + public function __destruct() + { + if (is_resource($this->handle)) { + pg_free_result($this->handle); + } + } - public function seek($row) { - return pg_result_seek($this->handle, $row); - } + public function seek($row) + { + return pg_result_seek($this->handle, $row); + } - public function numRecords() { - return pg_num_rows($this->handle); - } + public function numRecords() + { + return pg_num_rows($this->handle); + } - public function nextRecord() { - return pg_fetch_assoc($this->handle); - } + public function nextRecord() + { + return pg_fetch_assoc($this->handle); + } } diff --git a/code/PostgreSQLQueryBuilder.php b/code/PostgreSQLQueryBuilder.php index 1ea071b..36b07d4 100644 --- a/code/PostgreSQLQueryBuilder.php +++ b/code/PostgreSQLQueryBuilder.php @@ -1,42 +1,45 @@ getSeparator(); + /** + * Return the LIMIT clause ready for inserting into a query. + * + * @param SQLSelect $query The expression object to build from + * @param array $parameters Out parameter for the resulting query parameters + * @return string The finalised limit SQL fragment + */ + public function buildLimitFragment(SQLSelect $query, array &$parameters) + { + $nl = $this->getSeparator(); - // Ensure limit is given - $limit = $query->getLimit(); - if(empty($limit)) return ''; - - // For literal values return this as the limit SQL - if( ! is_array($limit)) { - return "{$nl}LIMIT $limit"; - } + // Ensure limit is given + $limit = $query->getLimit(); + if (empty($limit)) { + return ''; + } + + // For literal values return this as the limit SQL + if (! is_array($limit)) { + return "{$nl}LIMIT $limit"; + } - // Assert that the array version provides the 'limit' key - if( ! array_key_exists('limit', $limit) || ($limit['limit'] !== null && ! is_numeric($limit['limit']))) { - throw new InvalidArgumentException( - 'DBQueryBuilder::buildLimitSQL(): Wrong format for $limit: '. var_export($limit, true) - ); - } - - if($limit['limit'] === null) { - $limit['limit'] = 'ALL'; - } - - $clause = "{$nl}LIMIT {$limit['limit']}"; - if(isset($limit['start']) && is_numeric($limit['start']) && $limit['start'] !== 0) { - $clause .= " OFFSET {$limit['start']}"; - } - return $clause; - } + // Assert that the array version provides the 'limit' key + if (! array_key_exists('limit', $limit) || ($limit['limit'] !== null && ! is_numeric($limit['limit']))) { + throw new InvalidArgumentException( + 'DBQueryBuilder::buildLimitSQL(): Wrong format for $limit: '. var_export($limit, true) + ); + } + if ($limit['limit'] === null) { + $limit['limit'] = 'ALL'; + } + + $clause = "{$nl}LIMIT {$limit['limit']}"; + if (isset($limit['start']) && is_numeric($limit['start']) && $limit['start'] !== 0) { + $clause .= " OFFSET {$limit['start']}"; + } + return $clause; + } } diff --git a/code/PostgreSQLSchemaManager.php b/code/PostgreSQLSchemaManager.php index abe0ff4..2a9aed8 100644 --- a/code/PostgreSQLSchemaManager.php +++ b/code/PostgreSQLSchemaManager.php @@ -6,895 +6,958 @@ * @package sapphire * @subpackage model */ -class PostgreSQLSchemaManager extends DBSchemaManager { +class PostgreSQLSchemaManager extends DBSchemaManager +{ - /** - * Identifier for this schema, used for configuring schema-specific table - * creation options - */ - const ID = 'PostgreSQL'; + /** + * Identifier for this schema, used for configuring schema-specific table + * creation options + */ + const ID = 'PostgreSQL'; - /** - * Instance of the database controller this schema belongs to - * - * @var PostgreSQLDatabase - */ - protected $database = null; + /** + * Instance of the database controller this schema belongs to + * + * @var PostgreSQLDatabase + */ + protected $database = null; - /** - * This holds a copy of all the constraint results that are returned - * via the function constraintExists(). This is a bit faster than - * repeatedly querying this column, and should allow the database - * to use it's built-in caching features for better queries. - * - * @var array - */ - protected static $cached_constraints = array(); + /** + * This holds a copy of all the constraint results that are returned + * via the function constraintExists(). This is a bit faster than + * repeatedly querying this column, and should allow the database + * to use it's built-in caching features for better queries. + * + * @var array + */ + protected static $cached_constraints = array(); - /** - * - * This holds a copy of all the queries that run through the function fieldList() - * This is one of the most-often called functions, and repeats itself a great deal in the unit tests. - * - * @var array - */ - protected static $cached_fieldlists = array(); + /** + * + * This holds a copy of all the queries that run through the function fieldList() + * This is one of the most-often called functions, and repeats itself a great deal in the unit tests. + * + * @var array + */ + protected static $cached_fieldlists = array(); - protected function indexKey($table, $index, $spec) { - return $this->buildPostgresIndexName($table, $index); - } + protected function indexKey($table, $index, $spec) + { + return $this->buildPostgresIndexName($table, $index); + } - /** - * Creates a postgres database, ignoring model_schema_as_database - * - * @param string $name - */ - public function createPostgresDatabase($name) { - $this->query("CREATE DATABASE \"$name\";"); - } + /** + * Creates a postgres database, ignoring model_schema_as_database + * + * @param string $name + */ + public function createPostgresDatabase($name) + { + $this->query("CREATE DATABASE \"$name\";"); + } - public function createDatabase($name) { - if(PostgreSQLDatabase::model_schema_as_database()) { - $schemaName = $this->database->databaseToSchemaName($name); - return $this->createSchema($schemaName); - } - return $this->createPostgresDatabase($name); - } + public function createDatabase($name) + { + if (PostgreSQLDatabase::model_schema_as_database()) { + $schemaName = $this->database->databaseToSchemaName($name); + return $this->createSchema($schemaName); + } + return $this->createPostgresDatabase($name); + } - /** - * Determines if a postgres database exists, ignoring model_schema_as_database - * - * @param string $name - * @return boolean - */ - public function postgresDatabaseExists($name) { - $result = $this->preparedQuery("SELECT datname FROM pg_database WHERE datname = ?;", array($name)); - return $result->first() ? true : false; - } + /** + * Determines if a postgres database exists, ignoring model_schema_as_database + * + * @param string $name + * @return boolean + */ + public function postgresDatabaseExists($name) + { + $result = $this->preparedQuery("SELECT datname FROM pg_database WHERE datname = ?;", array($name)); + return $result->first() ? true : false; + } - public function databaseExists($name) { - if(PostgreSQLDatabase::model_schema_as_database()) { - $schemaName = $this->database->databaseToSchemaName($name); - return $this->schemaExists($schemaName); - } - return $this->postgresDatabaseExists($name); - } + public function databaseExists($name) + { + if (PostgreSQLDatabase::model_schema_as_database()) { + $schemaName = $this->database->databaseToSchemaName($name); + return $this->schemaExists($schemaName); + } + return $this->postgresDatabaseExists($name); + } - /** - * Determines the list of all postgres databases, ignoring model_schema_as_database - * - * @return array - */ - public function postgresDatabaseList() { - return $this->query("SELECT datname FROM pg_database WHERE datistemplate=false;")->column(); - } + /** + * Determines the list of all postgres databases, ignoring model_schema_as_database + * + * @return array + */ + public function postgresDatabaseList() + { + return $this->query("SELECT datname FROM pg_database WHERE datistemplate=false;")->column(); + } - public function databaseList() { - if(PostgreSQLDatabase::model_schema_as_database()) { - $schemas = $this->schemaList(); - $names = array(); - foreach($schemas as $schema) { - $names[] = $this->database->schemaToDatabaseName($schema); - } - return array_unique($names); - } - return $this->postgresDatabaseList(); - } - /** - * Drops a postgres database, ignoring model_schema_as_database - * - * @param string $name - */ - public function dropPostgresDatabase($name) { - $nameSQL = $this->database->escapeIdentifier($name); - $this->query("DROP DATABASE $nameSQL;"); - } + public function databaseList() + { + if (PostgreSQLDatabase::model_schema_as_database()) { + $schemas = $this->schemaList(); + $names = array(); + foreach ($schemas as $schema) { + $names[] = $this->database->schemaToDatabaseName($schema); + } + return array_unique($names); + } + return $this->postgresDatabaseList(); + } + /** + * Drops a postgres database, ignoring model_schema_as_database + * + * @param string $name + */ + public function dropPostgresDatabase($name) + { + $nameSQL = $this->database->escapeIdentifier($name); + $this->query("DROP DATABASE $nameSQL;"); + } - public function dropDatabase($name) { - if(PostgreSQLDatabase::model_schema_as_database()) { - $schemaName = $this->database->databaseToSchemaName($name); - return $this->dropSchema($schemaName); - } - $this->dropPostgresDatabase($name); - } + public function dropDatabase($name) + { + if (PostgreSQLDatabase::model_schema_as_database()) { + $schemaName = $this->database->databaseToSchemaName($name); + return $this->dropSchema($schemaName); + } + $this->dropPostgresDatabase($name); + } - /** - * Returns true if the schema exists in the current database - * - * @param string $name - * @return boolean - */ - public function schemaExists($name) { - return $this->preparedQuery( - "SELECT nspname FROM pg_catalog.pg_namespace WHERE nspname = ?;", - array($name) - )->first() ? true : false; - } + /** + * Returns true if the schema exists in the current database + * + * @param string $name + * @return boolean + */ + public function schemaExists($name) + { + return $this->preparedQuery( + "SELECT nspname FROM pg_catalog.pg_namespace WHERE nspname = ?;", + array($name) + )->first() ? true : false; + } - /** - * Creates a schema in the current database - * - * @param string $name - */ - public function createSchema($name) { - $nameSQL = $this->database->escapeIdentifier($name); - $this->query("CREATE SCHEMA $nameSQL;"); - } + /** + * Creates a schema in the current database + * + * @param string $name + */ + public function createSchema($name) + { + $nameSQL = $this->database->escapeIdentifier($name); + $this->query("CREATE SCHEMA $nameSQL;"); + } - /** - * Drops a schema from the database. Use carefully! - * - * @param string $name - */ - public function dropSchema($name) { - $nameSQL = $this->database->escapeIdentifier($name); - $this->query("DROP SCHEMA $nameSQL CASCADE;"); - } + /** + * Drops a schema from the database. Use carefully! + * + * @param string $name + */ + public function dropSchema($name) + { + $nameSQL = $this->database->escapeIdentifier($name); + $this->query("DROP SCHEMA $nameSQL CASCADE;"); + } - /** - * Returns the list of all available schemas on the current database - * - * @return array - */ - public function schemaList() { - return $this->query(" + /** + * Returns the list of all available schemas on the current database + * + * @return array + */ + public function schemaList() + { + return $this->query(" SELECT nspname FROM pg_catalog.pg_namespace WHERE nspname <> 'information_schema' AND nspname !~ E'^pg_'" - )->column(); - } + )->column(); + } - public function createTable($table, $fields = null, $indexes = null, $options = null, $advancedOptions = null) { + public function createTable($table, $fields = null, $indexes = null, $options = null, $advancedOptions = null) + { + $fieldSchemas = $indexSchemas = ""; + if ($fields) { + foreach ($fields as $k => $v) { + $fieldSchemas .= "\"$k\" $v,\n"; + } + } + 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; + } - $fieldSchemas = $indexSchemas = ""; - if($fields) foreach($fields as $k => $v) { - $fieldSchemas .= "\"$k\" $v,\n"; - } - 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; - } + //First of all, does this table already exist + $doesExist = $this->hasTable($table); + if ($doesExist) { + // Table already exists, just return the name, in line with baseclass documentation. + return $table; + } - //First of all, does this table already exist - $doesExist = $this->hasTable($table); - if($doesExist) { - // Table already exists, just return the name, in line with baseclass documentation. - return $table; - } + //If we have a fulltext search request, then we need to create a special column + //for GiST searches + $fulltexts = ''; + $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']; + } + } + } - //If we have a fulltext search request, then we need to create a special column - //for GiST searches - $fulltexts = ''; - $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']; - } - } - } + if ($indexes) { + foreach ($indexes as $k => $v) { + $indexSchemas .= $this->getIndexSqlDefinition($table, $k, $v) . "\n"; + } + } - if($indexes) foreach($indexes as $k => $v) { - $indexSchemas .= $this->getIndexSqlDefinition($table, $k, $v) . "\n"; - } + //Do we need to create a tablespace for this item? + if ($advancedOptions && isset($advancedOptions['tablespace'])) { + $this->createOrReplaceTablespace( + $advancedOptions['tablespace']['name'], + $advancedOptions['tablespace']['location'] + ); + $tableSpace = ' TABLESPACE ' . $advancedOptions['tablespace']['name']; + } else { + $tableSpace = ''; + } - //Do we need to create a tablespace for this item? - if($advancedOptions && isset($advancedOptions['tablespace'])){ - $this->createOrReplaceTablespace( - $advancedOptions['tablespace']['name'], - $advancedOptions['tablespace']['location'] - ); - $tableSpace = ' TABLESPACE ' . $advancedOptions['tablespace']['name']; - } else - $tableSpace = ''; - - $this->query("CREATE TABLE \"$table\" ( + $this->query("CREATE TABLE \"$table\" ( $fieldSchemas $fulltexts primary key (\"ID\") )$tableSpace; $indexSchemas $addOptions"); - if($triggers!=''){ - $this->query($triggers); - } + if ($triggers!='') { + $this->query($triggers); + } - //If we have a partitioning requirement, we do that here: - if($advancedOptions && isset($advancedOptions['partitions'])){ - $this->createOrReplacePartition($table, $advancedOptions['partitions'], $indexes, $advancedOptions); - } + //If we have a partitioning requirement, we do that here: + if ($advancedOptions && isset($advancedOptions['partitions'])) { + $this->createOrReplacePartition($table, $advancedOptions['partitions'], $indexes, $advancedOptions); + } - //Lastly, clustering goes here: - if($advancedOptions && isset($advancedOptions['cluster'])){ - $this->query("CLUSTER \"$table\" USING \"{$advancedOptions['cluster']}\";"); - } + //Lastly, clustering goes here: + if ($advancedOptions && isset($advancedOptions['cluster'])) { + $this->query("CLUSTER \"$table\" USING \"{$advancedOptions['cluster']}\";"); + } - return $table; - } + return $table; + } - /** - * Builds the internal Postgres index name given the silverstripe table and index name - * - * @param string $tableName - * @param string $indexName - * @param string $prefix The optional prefix for the index. Defaults to "ix" for indexes. - * @return string The postgres name of the index - */ - protected function buildPostgresIndexName($tableName, $indexName, $prefix = 'ix') { + /** + * Builds the internal Postgres index name given the silverstripe table and index name + * + * @param string $tableName + * @param string $indexName + * @param string $prefix The optional prefix for the index. Defaults to "ix" for indexes. + * @return string The postgres name of the index + */ + protected function buildPostgresIndexName($tableName, $indexName, $prefix = 'ix') + { - // Assume all indexes also contain the table name - // MD5 the table/index name combo to keep it to a fixed length. - // Exclude the prefix so that the trigger name can be easily generated from the index name - $indexNamePG = "{$prefix}_" . md5("{$tableName}_{$indexName}"); + // Assume all indexes also contain the table name + // MD5 the table/index name combo to keep it to a fixed length. + // Exclude the prefix so that the trigger name can be easily generated from the index name + $indexNamePG = "{$prefix}_" . md5("{$tableName}_{$indexName}"); - // Limit to 63 characters - if (strlen($indexNamePG) > 63) { - return substr($indexNamePG, 0, 63); - } else { - return $indexNamePG; - } - } + // Limit to 63 characters + if (strlen($indexNamePG) > 63) { + return substr($indexNamePG, 0, 63); + } else { + return $indexNamePG; + } + } - /** - * Builds the internal Postgres trigger name given the silverstripe table and trigger name - * - * @param string $tableName - * @param string $triggerName - * @return string The postgres name of the trigger - */ - function buildPostgresTriggerName($tableName, $triggerName) { - // Kind of cheating, but behaves the same way as indexes - return $this->buildPostgresIndexName($tableName, $triggerName, 'ts'); - } + /** + * Builds the internal Postgres trigger name given the silverstripe table and trigger name + * + * @param string $tableName + * @param string $triggerName + * @return string The postgres name of the trigger + */ + public function buildPostgresTriggerName($tableName, $triggerName) + { + // Kind of cheating, but behaves the same way as indexes + return $this->buildPostgresIndexName($tableName, $triggerName, 'ts'); + } - public function alterTable($table, $newFields = null, $newIndexes = null, $alteredFields = null, $alteredIndexes = null, $alteredOptions = null, $advancedOptions = null) { + public function alterTable($table, $newFields = null, $newIndexes = null, $alteredFields = null, $alteredIndexes = null, $alteredOptions = null, $advancedOptions = null) + { + $alterList = array(); + if ($newFields) { + foreach ($newFields as $fieldName => $fieldSpec) { + $alterList[] = "ADD \"$fieldName\" $fieldSpec"; + } + } - $alterList = array(); - if($newFields) foreach($newFields as $fieldName => $fieldSpec) { - $alterList[] = "ADD \"$fieldName\" $fieldSpec"; - } + if ($alteredFields) { + foreach ($alteredFields as $indexName => $indexSpec) { + $val = $this->alterTableAlterColumn($table, $indexName, $indexSpec); + if (!empty($val)) { + $alterList[] = $val; + } + } + } - if ($alteredFields) foreach ($alteredFields as $indexName => $indexSpec) { - $val = $this->alterTableAlterColumn($table, $indexName, $indexSpec); - if (!empty($val)) $alterList[] = $val; - } + //Do we need to do anything with the tablespaces? + if ($alteredOptions && isset($advancedOptions['tablespace'])) { + $this->createOrReplaceTablespace($advancedOptions['tablespace']['name'], $advancedOptions['tablespace']['location']); + $this->query("ALTER TABLE \"$table\" SET TABLESPACE {$advancedOptions['tablespace']['name']};"); + } - //Do we need to do anything with the tablespaces? - if($alteredOptions && isset($advancedOptions['tablespace'])){ - $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(); + //Pick up the altered indexes here: + $fieldList = $this->fieldList($table); + $fulltexts = false; + $drop_triggers = false; + $triggers = false; + if ($alteredIndexes) { + foreach ($alteredIndexes as $indexName=>$indexSpec) { + $indexSpec = $this->parseIndexSpec($indexName, $indexSpec); + $indexNamePG = $this->buildPostgresIndexName($table, $indexName); - //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(); - //Pick up the altered indexes here: - $fieldList = $this->fieldList($table); - $fulltexts = false; - $drop_triggers = false; - $triggers = false; - if($alteredIndexes) foreach($alteredIndexes as $indexName=>$indexSpec) { + if ($indexSpec['type']=='fulltext') { + //For full text indexes, we need to drop the trigger, drop the index, AND drop the column - $indexSpec = $this->parseIndexSpec($indexName, $indexSpec); - $indexNamePG = $this->buildPostgresIndexName($table, $indexName); + //Go and get the tsearch details: + $ts_details = $this->fulltext($indexSpec, $table, $indexName); - if($indexSpec['type']=='fulltext') { - //For full text indexes, we need to drop the trigger, drop the index, AND drop the column + //Drop this column if it already exists: - //Go and get the tsearch details: - $ts_details = $this->fulltext($indexSpec, $table, $indexName); + //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']}\";"; + } - //Drop this column if it already exists: + // 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']; + } - //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']}\";"; - } + // Create index action (including fulltext) + $alterIndexList[] = "DROP INDEX IF EXISTS \"$indexNamePG\";"; + $createIndex = $this->getIndexSqlDefinition($table, $indexName, $indexSpec); + if ($createIndex!==false) { + $alterIndexList[] = $createIndex; + } + } + } - // 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']; - } + //Add the new indexes: + if ($newIndexes) { + foreach ($newIndexes as $indexName => $indexSpec) { + $indexSpec = $this->parseIndexSpec($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']; + } + } - // Create index action (including fulltext) - $alterIndexList[] = "DROP INDEX IF EXISTS \"$indexNamePG\";"; - $createIndex = $this->getIndexSqlDefinition($table, $indexName, $indexSpec); - if($createIndex!==false) $alterIndexList[] = $createIndex; - } + //Check that this index doesn't already exist: + $indexes=$this->indexList($table); + if (isset($indexes[$indexName])) { + $alterIndexList[] = "DROP INDEX IF EXISTS \"$indexNamePG\";"; + } - //Add the new indexes: - if($newIndexes) foreach($newIndexes as $indexName => $indexSpec){ + $createIndex=$this->getIndexSqlDefinition($table, $indexName, $indexSpec); + if ($createIndex!==false) { + $alterIndexList[] = $createIndex; + } + } + } - $indexSpec = $this->parseIndexSpec($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 ($alterList) { + $alterations = implode(",\n", $alterList); + $this->query("ALTER TABLE \"$table\" " . $alterations); + } - //Check that this index doesn't already exist: - $indexes=$this->indexList($table); - if(isset($indexes[$indexName])){ - $alterIndexList[] = "DROP INDEX IF EXISTS \"$indexNamePG\";"; - } + //Do we need to create a tablespace for this item? + if ($advancedOptions && isset($advancedOptions['extensions']['tablespace'])) { + $extensions=$advancedOptions['extensions']; + $this->createOrReplaceTablespace($extensions['tablespace']['name'], $extensions['tablespace']['location']); + } - $createIndex=$this->getIndexSqlDefinition($table, $indexName, $indexSpec); - if($createIndex!==false) - $alterIndexList[] = $createIndex; - } + if ($alteredOptions && isset($this->class) && isset($alteredOptions[$this->class])) { + $this->query(sprintf("ALTER TABLE \"%s\" %s", $table, $alteredOptions[$this->class])); + Database::alteration_message( + sprintf("Table %s options changed: %s", $table, $alteredOptions[$this->class]), + "changed" + ); + } - if($alterList) { - $alterations = implode(",\n", $alterList); - $this->query("ALTER TABLE \"$table\" " . $alterations); - } + //Create any fulltext columns and triggers here: + if ($fulltexts) { + $this->query($fulltexts); + } + if ($drop_triggers) { + $this->query($drop_triggers); + } - //Do we need to create a tablespace for this item? - if($advancedOptions && isset($advancedOptions['extensions']['tablespace'])){ - $extensions=$advancedOptions['extensions']; - $this->createOrReplaceTablespace($extensions['tablespace']['name'], $extensions['tablespace']['location']); - } + if ($triggers) { + $this->query($triggers); - if($alteredOptions && isset($this->class) && isset($alteredOptions[$this->class])) { - $this->query(sprintf("ALTER TABLE \"%s\" %s", $table, $alteredOptions[$this->class])); - Database::alteration_message( - sprintf("Table %s options changed: %s", $table, $alteredOptions[$this->class]), - "changed" - ); - } + $triggerbits=explode(';', $triggers); + foreach ($triggerbits as $trigger) { + $trigger_fields=$this->triggerFieldsFromTrigger($trigger); - //Create any fulltext columns and triggers here: - if($fulltexts) $this->query($fulltexts); - if($drop_triggers) $this->query($drop_triggers); + 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]\";"); + } + } + } - if($triggers) { - $this->query($triggers); + foreach ($alterIndexList as $alteration) { + $this->query($alteration); + } - $triggerbits=explode(';', $triggers); - foreach($triggerbits as $trigger){ - $trigger_fields=$this->triggerFieldsFromTrigger($trigger); + //If we have a partitioning requirement, we do that here: + if ($advancedOptions && isset($advancedOptions['partitions'])) { + $this->createOrReplacePartition($table, $advancedOptions['partitions']); + } - 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]\";"); - } - } - } + //Lastly, clustering goes here: + if ($advancedOptions && isset($advancedOptions['cluster'])) { + $clusterIndex = $this->buildPostgresIndexName($table, $advancedOptions['cluster']); + $this->query("CLUSTER \"$table\" USING \"$clusterIndex\";"); + } else { + //Check that clustering is not on this table, and if it is, remove it: - foreach($alterIndexList as $alteration) $this->query($alteration); + //This is really annoying. We need the oid of this table: + $stats = $this->preparedQuery( + "SELECT relid FROM pg_stat_user_tables WHERE relname = ?;", + array($table) + )->first(); + $oid=$stats['relid']; - //If we have a partitioning requirement, we do that here: - if($advancedOptions && isset($advancedOptions['partitions'])){ - $this->createOrReplacePartition($table, $advancedOptions['partitions']); - } - - //Lastly, clustering goes here: - if ($advancedOptions && isset($advancedOptions['cluster'])) { - $clusterIndex = $this->buildPostgresIndexName($table, $advancedOptions['cluster']); - $this->query("CLUSTER \"$table\" USING \"$clusterIndex\";"); - } else { - //Check that clustering is not on this table, and if it is, remove it: - - //This is really annoying. We need the oid of this table: - $stats = $this->preparedQuery( - "SELECT relid FROM pg_stat_user_tables WHERE relname = ?;", - array($table) - )->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(" + //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 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) - )->first(); + array($oid) + )->first(); - if($clustered) { - $this->query("ALTER TABLE \"$table\" SET WITHOUT CLUSTER;"); - } - } - } + if ($clustered) { + $this->query("ALTER TABLE \"$table\" SET WITHOUT CLUSTER;"); + } + } + } - /* - * Creates an ALTER expression for a column in PostgreSQL - * - * @param $tableName Name of the table to be altered - * @param $colName Name of the column to be altered - * @param $colSpec String which contains conditions for a column - * @return string - */ - private function alterTableAlterColumn($tableName, $colName, $colSpec){ - // First, we split the column specifications into parts - // TODO: this returns an empty array for the following string: int(11) not null auto_increment - // on second thoughts, why is an auto_increment field being passed through? + /* + * Creates an ALTER expression for a column in PostgreSQL + * + * @param $tableName Name of the table to be altered + * @param $colName Name of the column to be altered + * @param $colSpec String which contains conditions for a column + * @return string + */ + private function alterTableAlterColumn($tableName, $colName, $colSpec) + { + // First, we split the column specifications into parts + // TODO: this returns an empty array for the following string: int(11) not null auto_increment + // on second thoughts, why is an auto_increment field being passed through? - $pattern = '/^([\w()]+)\s?((?:not\s)?null)?\s?(default\s[\w\']+)?\s?(check\s[\w()\'",\s]+)?$/i'; - preg_match($pattern, $colSpec, $matches); + $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) return ''; + if (sizeof($matches)==0) { + return ''; + } - if($matches[1]=='serial8') return ''; + if ($matches[1]=='serial8') { + return ''; + } - if(isset($matches[1])) { - $alterCol = "ALTER COLUMN \"$colName\" TYPE $matches[1]\n"; + if (isset($matches[1])) { + $alterCol = "ALTER COLUMN \"$colName\" TYPE $matches[1]\n"; - // SET null / not null - if(!empty($matches[2])) { - $alterCol .= ",\nALTER COLUMN \"$colName\" SET $matches[2]"; - } + // SET null / not null + if (!empty($matches[2])) { + $alterCol .= ",\nALTER COLUMN \"$colName\" SET $matches[2]"; + } - // SET default (we drop it first, for reasons of precaution) - if(!empty($matches[3])) { - $alterCol .= ",\nALTER COLUMN \"$colName\" DROP DEFAULT"; - $alterCol .= ",\nALTER COLUMN \"$colName\" SET $matches[3]"; - } + // SET default (we drop it first, for reasons of precaution) + if (!empty($matches[3])) { + $alterCol .= ",\nALTER COLUMN \"$colName\" DROP DEFAULT"; + $alterCol .= ",\nALTER COLUMN \"$colName\" SET $matches[3]"; + } - // SET check constraint (The constraint HAS to be dropped) - $existing_constraint=$this->query("SELECT conname FROM pg_constraint WHERE conname='{$tableName}_{$colName}_check';")->value(); - 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], " '"); + // SET check constraint (The constraint HAS to be dropped) + $existing_constraint=$this->query("SELECT conname FROM pg_constraint WHERE conname='{$tableName}_{$colName}_check';")->value(); + 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], " '"); - //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);"; - if($this->hasTable("{$tableName}_Live")) { - $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);"; - } + //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);"; + if ($this->hasTable("{$tableName}_Live")) { + $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);"; + } - $this->query($updateConstraint); - } + $this->query($updateConstraint); + } - //First, delete any existing constraint on this column, even if it's no longer an enum - if($existing_constraint) { - $alterCol .= ",\nDROP CONSTRAINT \"{$tableName}_{$colName}_check\""; - } + //First, delete any existing constraint on this column, even if it's no longer an enum + if ($existing_constraint) { + $alterCol .= ",\nDROP CONSTRAINT \"{$tableName}_{$colName}_check\""; + } - //Now create the constraint (if we've asked for one) - if(!empty($matches[4])) { - $alterCol .= ",\nADD CONSTRAINT \"{$tableName}_{$colName}_check\" $matches[4]"; - } - } + //Now create the constraint (if we've asked for one) + if (!empty($matches[4])) { + $alterCol .= ",\nADD CONSTRAINT \"{$tableName}_{$colName}_check\" $matches[4]"; + } + } - return isset($alterCol) ? $alterCol : ''; - } + return isset($alterCol) ? $alterCol : ''; + } - public function renameTable($oldTableName, $newTableName) { - $this->query("ALTER TABLE \"$oldTableName\" RENAME TO \"$newTableName\""); - unset(self::$cached_fieldlists[$oldTableName]); - } + public function renameTable($oldTableName, $newTableName) + { + $this->query("ALTER TABLE \"$oldTableName\" RENAME TO \"$newTableName\""); + unset(self::$cached_fieldlists[$oldTableName]); + } - public function checkAndRepairTable($tableName) { - $this->query("VACUUM FULL ANALYZE \"$tableName\""); - $this->query("REINDEX TABLE \"$tableName\""); - return true; - } + public function checkAndRepairTable($tableName) + { + $this->query("VACUUM FULL ANALYZE \"$tableName\""); + $this->query("REINDEX TABLE \"$tableName\""); + return true; + } - public function createField($table, $field, $spec) { - $this->query("ALTER TABLE \"$table\" ADD \"$field\" $spec"); - } + public function createField($table, $field, $spec) + { + $this->query("ALTER TABLE \"$table\" ADD \"$field\" $spec"); + } - /** - * Change the database type of the given field. - * - * @param string $tableName The name of the tbale the field is in. - * @param string $fieldName The name of the field to change. - * @param string $fieldSpec The new field specification - */ - public function alterField($tableName, $fieldName, $fieldSpec) { - $this->query("ALTER TABLE \"$tableName\" CHANGE \"$fieldName\" \"$fieldName\" $fieldSpec"); - } + /** + * Change the database type of the given field. + * + * @param string $tableName The name of the tbale the field is in. + * @param string $fieldName The name of the field to change. + * @param string $fieldSpec The new field specification + */ + public function alterField($tableName, $fieldName, $fieldSpec) + { + $this->query("ALTER TABLE \"$tableName\" CHANGE \"$fieldName\" \"$fieldName\" $fieldSpec"); + } - public function renameField($tableName, $oldName, $newName) { - $fieldList = $this->fieldList($tableName); - if(array_key_exists($oldName, $fieldList)) { - $this->query("ALTER TABLE \"$tableName\" RENAME COLUMN \"$oldName\" TO \"$newName\""); + public function renameField($tableName, $oldName, $newName) + { + $fieldList = $this->fieldList($tableName); + if (array_key_exists($oldName, $fieldList)) { + $this->query("ALTER TABLE \"$tableName\" RENAME COLUMN \"$oldName\" TO \"$newName\""); - //Remove this from the cached list: - unset(self::$cached_fieldlists[$tableName]); - } - } + //Remove this from the cached list: + unset(self::$cached_fieldlists[$tableName]); + } + } - public function fieldList($table) { - //Query from http://www.alberton.info/postgresql_meta_info.html - //This gets us more information than we need, but I've included it all for the moment.... + public function fieldList($table) + { + //Query from http://www.alberton.info/postgresql_meta_info.html + //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(" + //if(!isset(self::$cached_fieldlists[$table])){ + $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) foreach($fields as $field) { + $output = array(); + if ($fields) { + foreach ($fields as $field) { + switch ($field['data_type']) { + case 'character varying': + //Check to see if there's a constraint attached to this column: + //$constraint=$this->query("SELECT conname,pg_catalog.pg_get_constraintdef(r.oid, true) FROM pg_catalog.pg_constraint r WHERE r.contype = 'c' AND conname='" . $table . '_' . $field['column_name'] . "_check' ORDER BY 1;")->first(); + $constraint = $this->constraintExists($table . '_' . $field['column_name'] . '_check'); + if ($constraint) { + //Now we need to break this constraint text into bits so we can see what we have: + //Examples: + //CHECK ("CanEditType"::text = ANY (ARRAY['LoggedInUsers'::character varying, 'OnlyTheseUsers'::character varying, 'Inherit'::character varying]::text[])) + //CHECK ("ClassName"::text = 'PageComment'::text) - switch($field['data_type']){ - case 'character varying': - //Check to see if there's a constraint attached to this column: - //$constraint=$this->query("SELECT conname,pg_catalog.pg_get_constraintdef(r.oid, true) FROM pg_catalog.pg_constraint r WHERE r.contype = 'c' AND conname='" . $table . '_' . $field['column_name'] . "_check' ORDER BY 1;")->first(); - $constraint = $this->constraintExists($table . '_' . $field['column_name'] . '_check'); - if($constraint){ - //Now we need to break this constraint text into bits so we can see what we have: - //Examples: - //CHECK ("CanEditType"::text = ANY (ARRAY['LoggedInUsers'::character varying, 'OnlyTheseUsers'::character varying, 'Inherit'::character varying]::text[])) - //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); - //TODO: replace all this with a regular expression! - $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) { + //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)); + } + } else { + $output[$field['column_name']]='varchar(' . $field['character_maximum_length'] . ')'; + } + break; - 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)); - } - } else{ - $output[$field['column_name']]='varchar(' . $field['character_maximum_length'] . ')'; - } - break; + case 'numeric': + $output[$field['column_name']]='decimal(' . $field['numeric_precision'] . ',' . $field['numeric_scale'] . ') default ' . (int)$field['column_default']; + break; - case 'numeric': - $output[$field['column_name']]='decimal(' . $field['numeric_precision'] . ',' . $field['numeric_scale'] . ') default ' . (int)$field['column_default']; - break; + case 'integer': + $output[$field['column_name']]='integer default ' . (int)$field['column_default']; + break; - case 'integer': - $output[$field['column_name']]='integer default ' . (int)$field['column_default']; - break; + case 'timestamp without time zone': + $output[$field['column_name']]='timestamp'; + break; - case 'timestamp without time zone': - $output[$field['column_name']]='timestamp'; - break; + case 'smallint': + $output[$field['column_name']]='smallint default ' . (int)$field['column_default']; + break; - case 'smallint': - $output[$field['column_name']]='smallint default ' . (int)$field['column_default']; - break; + case 'time without time zone': + $output[$field['column_name']]='time'; + break; - case 'time without time zone': - $output[$field['column_name']]='time'; - break; + case 'double precision': + $output[$field['column_name']]='float'; + break; - case 'double precision': - $output[$field['column_name']]='float'; - break; + default: + $output[$field['column_name']] = $field; + } + } + } - default: - $output[$field['column_name']] = $field; - } + // self::$cached_fieldlists[$table]=$output; + //} - } + //return self::$cached_fieldlists[$table]; - // self::$cached_fieldlists[$table]=$output; - //} + return $output; + } - //return self::$cached_fieldlists[$table]; + public function clearCachedFieldlist($tableName=false) + { + if ($tableName) { + unset(self::$cached_fieldlists[$tableName]); + } else { + self::$cached_fieldlists=array(); + } + return true; + } - return $output; - } + /** + * Create an index on a table. + * + * @param string $tableName The name of the table. + * @param string $indexName The name of the index. + * @param string $indexSpec The specification of the index, see Database::requireIndex() for more details. + */ + public function createIndex($tableName, $indexName, $indexSpec) + { + $createIndex = $this->getIndexSqlDefinition($tableName, $indexName, $indexSpec); + if ($createIndex !== false) { + $this->query($createIndex); + } + } - function clearCachedFieldlist($tableName=false){ - if($tableName) unset(self::$cached_fieldlists[$tableName]); - else self::$cached_fieldlists=array(); - return true; - } + /* + * @todo - factor out? Is DBSchemaManager::convertIndexSpec sufficient? + public function convertIndexSpec($indexSpec, $asDbValue=false, $table=''){ - /** - * Create an index on a table. - * - * @param string $tableName The name of the table. - * @param string $indexName The name of the index. - * @param string $indexSpec The specification of the index, see Database::requireIndex() for more details. - */ - public function createIndex($tableName, $indexName, $indexSpec) { - $createIndex = $this->getIndexSqlDefinition($tableName, $indexName, $indexSpec); - if($createIndex !== false) $this->query($createIndex); - } + if(!$asDbValue){ + if(is_array($indexSpec)){ + //Here we create a db-specific version of whatever index we need to create. + switch($indexSpec['type']){ + case 'fulltext': + $indexSpec='fulltext (' . $indexSpec['value'] . ')'; + break; + case 'unique': + $indexSpec='unique (' . $indexSpec['value'] . ')'; + break; + case 'hash': + $indexSpec='using hash (' . $indexSpec['value'] . ')'; + break; + case 'index': + //The default index is 'btree', which we'll use by default (below): + default: + $indexSpec='using btree (' . $indexSpec['value'] . ')'; + break; + } + } + } else { + $indexSpec = $this->buildPostgresIndexName($table, $indexSpec); + } + return $indexSpec; + }*/ - /* - * @todo - factor out? Is DBSchemaManager::convertIndexSpec sufficient? - public function convertIndexSpec($indexSpec, $asDbValue=false, $table=''){ + protected function getIndexSqlDefinition($tableName, $indexName, $indexSpec, $asDbValue=false) + { - if(!$asDbValue){ - if(is_array($indexSpec)){ - //Here we create a db-specific version of whatever index we need to create. - switch($indexSpec['type']){ - case 'fulltext': - $indexSpec='fulltext (' . $indexSpec['value'] . ')'; - break; - case 'unique': - $indexSpec='unique (' . $indexSpec['value'] . ')'; - break; - case 'hash': - $indexSpec='using hash (' . $indexSpec['value'] . ')'; - break; - case 'index': - //The default index is 'btree', which we'll use by default (below): - default: - $indexSpec='using btree (' . $indexSpec['value'] . ')'; - break; - } - } - } else { - $indexSpec = $this->buildPostgresIndexName($table, $indexSpec); - } - return $indexSpec; - }*/ + //TODO: create table partition support + //TODO: create clustering options - protected function getIndexSqlDefinition($tableName, $indexName, $indexSpec, $asDbValue=false) { + //NOTE: it is possible for *_renamed tables to have indexes whose names are not updates + //Therefore, we now check for the existance of indexes before we create them. + //This is techically a bug, since new tables will not be indexed. - //TODO: create table partition support - //TODO: create clustering options + // If requesting the definition rather than the DDL + if ($asDbValue) { + $indexName=trim($indexName, '()'); + return $indexName; + } - //NOTE: it is possible for *_renamed tables to have indexes whose names are not updates - //Therefore, we now check for the existance of indexes before we create them. - //This is techically a bug, since new tables will not be indexed. + // Determine index name + $tableCol = $this->buildPostgresIndexName($tableName, $indexName); - // If requesting the definition rather than the DDL - if($asDbValue) { - $indexName=trim($indexName, '()'); - return $indexName; - } + // Consolidate/Cleanup spec into array format + $indexSpec = $this->parseIndexSpec($indexName, $indexSpec); - // Determine index name - $tableCol = $this->buildPostgresIndexName($tableName, $indexName); + //Misc options first: + $fillfactor = $where = ''; + if (isset($indexSpec['fillfactor'])) { + $fillfactor = 'WITH (FILLFACTOR = ' . $indexSpec['fillfactor'] . ')'; + } + if (isset($indexSpec['where'])) { + $where = 'WHERE ' . $indexSpec['where']; + } - // Consolidate/Cleanup spec into array format - $indexSpec = $this->parseIndexSpec($indexName, $indexSpec); + //create a type-specific index + // NOTE: hash should be removed. This is only here to demonstrate how other indexes can be made + // NOTE: Quote the index name to preserve case sensitivity + switch ($indexSpec['type']) { + case 'fulltext': + // @see fulltext() for the definition of the trigger that ts_$IndexName uses for fulltext searching + $clusterMethod = PostgreSQLDatabase::default_fts_cluster_method(); + $spec = "create index \"$tableCol\" ON \"$tableName\" USING $clusterMethod(\"ts_" . $indexName . "\") $fillfactor $where"; + break; - //Misc options first: - $fillfactor = $where = ''; - if (isset($indexSpec['fillfactor'])) { - $fillfactor = 'WITH (FILLFACTOR = ' . $indexSpec['fillfactor'] . ')'; - } - if (isset($indexSpec['where'])) { - $where = 'WHERE ' . $indexSpec['where']; - } + case 'unique': + $spec = "create unique index \"$tableCol\" ON \"$tableName\" (" . $indexSpec['value'] . ") $fillfactor $where"; + break; - //create a type-specific index - // NOTE: hash should be removed. This is only here to demonstrate how other indexes can be made - // NOTE: Quote the index name to preserve case sensitivity - switch ($indexSpec['type']) { - case 'fulltext': - // @see fulltext() for the definition of the trigger that ts_$IndexName uses for fulltext searching - $clusterMethod = PostgreSQLDatabase::default_fts_cluster_method(); - $spec = "create index \"$tableCol\" ON \"$tableName\" USING $clusterMethod(\"ts_" . $indexName . "\") $fillfactor $where"; - break; + case 'btree': + $spec = "create index \"$tableCol\" ON \"$tableName\" USING btree (" . $indexSpec['value'] . ") $fillfactor $where"; + break; - case 'unique': - $spec = "create unique index \"$tableCol\" ON \"$tableName\" (" . $indexSpec['value'] . ") $fillfactor $where"; - break; + case 'hash': + //NOTE: this is not a recommended index type + $spec = "create index \"$tableCol\" ON \"$tableName\" USING hash (" . $indexSpec['value'] . ") $fillfactor $where"; + break; - case 'btree': - $spec = "create index \"$tableCol\" ON \"$tableName\" USING btree (" . $indexSpec['value'] . ") $fillfactor $where"; - break; + case 'index': + //'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\" (" . $indexSpec['value'] . ") $fillfactor $where"; + } + return trim($spec) . ';'; + } - case 'hash': - //NOTE: this is not a recommended index type - $spec = "create index \"$tableCol\" ON \"$tableName\" USING hash (" . $indexSpec['value'] . ") $fillfactor $where"; - break; + public function alterIndex($tableName, $indexName, $indexSpec) + { + $indexSpec = trim($indexSpec); + if ($indexSpec[0] != '(') { + list($indexType, $indexFields) = explode(' ', $indexSpec, 2); + } else { + $indexFields = $indexSpec; + } - case 'index': - //'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\" (" . $indexSpec['value'] . ") $fillfactor $where"; - } - return trim($spec) . ';'; - } + if (!$indexType) { + $indexType = "index"; + } - public function alterIndex($tableName, $indexName, $indexSpec) { - $indexSpec = trim($indexSpec); - if($indexSpec[0] != '(') { - list($indexType, $indexFields) = explode(' ',$indexSpec,2); - } else { - $indexFields = $indexSpec; - } + $this->query("DROP INDEX \"$indexName\""); + $this->query("ALTER TABLE \"$tableName\" ADD $indexType \"$indexName\" $indexFields"); + } - if(!$indexType) { - $indexType = "index"; - } + /** + * Given a trigger name attempt to determine the columns upon which it acts + * + * @param string $triggerName Postgres trigger name + * @return array List of columns + */ + protected function extractTriggerColumns($triggerName) + { + $trigger = $this->preparedQuery( + "SELECT tgargs FROM pg_catalog.pg_trigger WHERE tgname = ?", + array($triggerName) + )->first(); - $this->query("DROP INDEX \"$indexName\""); - $this->query("ALTER TABLE \"$tableName\" ADD $indexType \"$indexName\" $indexFields"); - } + // Option 1: output as a string + if (strpos($trigger['tgargs'], '\000') !== false) { + $argList = explode('\000', $trigger['tgargs']); + array_pop($argList); - /** - * Given a trigger name attempt to determine the columns upon which it acts - * - * @param string $triggerName Postgres trigger name - * @return array List of columns - */ - protected function extractTriggerColumns($triggerName) { - $trigger = $this->preparedQuery( - "SELECT tgargs FROM pg_catalog.pg_trigger WHERE tgname = ?", - array($triggerName) - )->first(); + // Option 2: hex-encoded (not sure why this happens, depends on PGSQL config) + } else { + $bytes = str_split($trigger['tgargs'], 2); + $argList = array(); + $nextArg = ""; + foreach ($bytes as $byte) { + if ($byte == "00") { + $argList[] = $nextArg; + $nextArg = ""; + } else { + $nextArg .= chr(hexdec($byte)); + } + } + } - // Option 1: output as a string - if(strpos($trigger['tgargs'],'\000') !== false) { - $argList = explode('\000', $trigger['tgargs']); - array_pop($argList); + // Drop first two arguments (trigger name and config name) and implode into nice list + return array_slice($argList, 2); + } - // Option 2: hex-encoded (not sure why this happens, depends on PGSQL config) - } else { - $bytes = str_split($trigger['tgargs'],2); - $argList = array(); - $nextArg = ""; - foreach($bytes as $byte) { - if($byte == "00") { - $argList[] = $nextArg; - $nextArg = ""; - } else { - $nextArg .= chr(hexdec($byte)); - } - } - } - - // Drop first two arguments (trigger name and config name) and implode into nice list - return array_slice($argList, 2); - } - - public function indexList($table) { - //Retrieve a list of indexes for the specified table - $indexes = $this->preparedQuery(" + public function indexList($table) + { + //Retrieve a list of indexes for the specified table + $indexes = $this->preparedQuery(" SELECT tablename, indexname, indexdef FROM pg_catalog.pg_indexes WHERE tablename = ? AND schemaname = ?;", - array($table, $this->database->currentSchema()) - ); + array($table, $this->database->currentSchema()) + ); - $indexList = array(); - foreach($indexes as $index) { - // Key for the indexList array. Differs from other DB implementations, which is why - // requireIndex() needed to be overridden - $indexName = $index['indexname']; + $indexList = array(); + foreach ($indexes as $index) { + // Key for the indexList array. Differs from other DB implementations, which is why + // requireIndex() needed to be overridden + $indexName = $index['indexname']; - //We don't actually need the entire created command, just a few bits: - $type = ''; + //We don't actually need the entire created command, just a few bits: + $type = ''; - //Check for uniques: - if(substr($index['indexdef'], 0, 13)=='CREATE UNIQUE') { - $type = 'unique'; - } + //Check for uniques: + if (substr($index['indexdef'], 0, 13)=='CREATE UNIQUE') { + $type = 'unique'; + } - //check for hashes, btrees etc: - if(strpos(strtolower($index['indexdef']), 'using hash ')!==false) { - $type = 'hash'; - } + //check for hashes, btrees etc: + if (strpos(strtolower($index['indexdef']), 'using hash ')!==false) { + $type = 'hash'; + } - //TODO: Fix me: btree is the default index type: - //if(strpos(strtolower($index['indexdef']), 'using btree ')!==false) - // $prefix='using btree '; + //TODO: Fix me: btree is the default index type: + //if(strpos(strtolower($index['indexdef']), 'using btree ')!==false) + // $prefix='using btree '; - if(strpos(strtolower($index['indexdef']), 'using rtree ')!==false) { - $type = 'rtree'; - } + if (strpos(strtolower($index['indexdef']), 'using rtree ')!==false) { + $type = 'rtree'; + } - // For fulltext indexes we need to extract the columns from another source - if (stristr($index['indexdef'], 'using gin')) { - $type = 'fulltext'; - // Extract trigger information from postgres - $triggerName = preg_replace('/^ix_/', 'ts_', $index['indexname']); - $columns = $this->extractTriggerColumns($triggerName); - $columnString = $this->implodeColumnList($columns); - } else { - $columnString = $this->quoteColumnSpecString($index['indexdef']); - } + // For fulltext indexes we need to extract the columns from another source + if (stristr($index['indexdef'], 'using gin')) { + $type = 'fulltext'; + // Extract trigger information from postgres + $triggerName = preg_replace('/^ix_/', 'ts_', $index['indexname']); + $columns = $this->extractTriggerColumns($triggerName); + $columnString = $this->implodeColumnList($columns); + } else { + $columnString = $this->quoteColumnSpecString($index['indexdef']); + } - $indexList[$indexName] = $this->parseIndexSpec($index, array( - 'name' => $indexName, // Not the correct name in the PHP, as this will be a mangled postgres-unique code - 'value' => $columnString, - 'type' => $type - )); - } + $indexList[$indexName] = $this->parseIndexSpec($index, array( + 'name' => $indexName, // Not the correct name in the PHP, as this will be a mangled postgres-unique code + 'value' => $columnString, + 'type' => $type + )); + } - return $indexList; + return $indexList; + } - } + public function tableList() + { + $tables = array(); + $result = $this->preparedQuery( + "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = ? AND tablename NOT ILIKE 'pg\\\_%' AND tablename NOT ILIKE 'sql\\\_%'", + array($this->database->currentSchema()) + ); + foreach ($result as $record) { + $table = reset($record); + $tables[strtolower($table)] = $table; + } + return $tables; + } - public function tableList() { - $tables = array(); - $result = $this->preparedQuery( - "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = ? AND tablename NOT ILIKE 'pg\\\_%' AND tablename NOT ILIKE 'sql\\\_%'", - array($this->database->currentSchema()) - ); - foreach($result as $record) { - $table = reset($record); - $tables[strtolower($table)] = $table; - } - return $tables; - } - - /** - * Find out what the constraint information is, given a constraint name. - * We also cache this result, so the next time we don't need to do a - * query all over again. - * - * @param string $constraint - */ - protected function constraintExists($constraint){ - if(!isset(self::$cached_constraints[$constraint])){ - $exists = $this->preparedQuery(" + /** + * Find out what the constraint information is, given a constraint name. + * We also cache this result, so the next time we don't need to do a + * query all over again. + * + * @param string $constraint + */ + protected function constraintExists($constraint) + { + if (!isset(self::$cached_constraints[$constraint])) { + $exists = $this->preparedQuery(" SELECT conname,pg_catalog.pg_get_constraintdef(r.oid, true) FROM pg_catalog.pg_constraint r WHERE r.contype = 'c' AND conname = ? ORDER BY 1;", - array($constraint) - )->first(); - self::$cached_constraints[$constraint]=$exists; - } + array($constraint) + )->first(); + self::$cached_constraints[$constraint]=$exists; + } - return self::$cached_constraints[$constraint]; - } + return self::$cached_constraints[$constraint]; + } - /** - * A function to return the field names and datatypes for the particular table - * - * @param string $tableName - * @return array List of columns an an associative array with the keys Column and DataType - */ - public function tableDetails($tableName) { - $query = "SELECT a.attname as \"Column\", pg_catalog.format_type(a.atttypid, a.atttypmod) as \"Datatype\" + /** + * A function to return the field names and datatypes for the particular table + * + * @param string $tableName + * @return array List of columns an an associative array with the keys Column and DataType + */ + public function tableDetails($tableName) + { + $query = "SELECT a.attname as \"Column\", pg_catalog.format_type(a.atttypid, a.atttypmod) as \"Datatype\" FROM pg_catalog.pg_attribute a WHERE a.attnum > 0 AND NOT a.attisdropped AND a.attrelid = ( SELECT c.oid @@ -903,585 +966,606 @@ class PostgreSQLSchemaManager extends DBSchemaManager { WHERE c.relname = ? AND pg_catalog.pg_table_is_visible(c.oid) AND n.nspname = ? );"; - $result = $this->preparedQuery($query, $tableName, $this->database->currentSchema()); + $result = $this->preparedQuery($query, $tableName, $this->database->currentSchema()); - $table = array(); - while($row = pg_fetch_assoc($result)) { - $table[] = array( - 'Column' => $row['Column'], - 'DataType' => $row['DataType'] - ); - } + $table = array(); + while ($row = pg_fetch_assoc($result)) { + $table[] = array( + 'Column' => $row['Column'], + 'DataType' => $row['DataType'] + ); + } - return $table; - } + return $table; + } - /** - * Pass a legit trigger name and it will be dropped - * This assumes that the trigger has been named in a unique fashion - * - * @param string $triggerName Name of the trigger - * @param string $tableName Name of the table - */ - protected function dropTrigger($triggerName, $tableName){ - $exists = $this->preparedQuery(" + /** + * Pass a legit trigger name and it will be dropped + * This assumes that the trigger has been named in a unique fashion + * + * @param string $triggerName Name of the trigger + * @param string $tableName Name of the table + */ + protected function dropTrigger($triggerName, $tableName) + { + $exists = $this->preparedQuery(" SELECT trigger_name FROM information_schema.triggers WHERE trigger_name = ? AND trigger_schema = ?;", - array($triggerName, $this->database->currentSchema()) - )->first(); - if($exists){ - $this->query("DROP trigger IF EXISTS $triggerName ON \"$tableName\";"); - } - } + array($triggerName, $this->database->currentSchema()) + )->first(); + if ($exists) { + $this->query("DROP trigger IF EXISTS $triggerName ON \"$tableName\";"); + } + } - /** - * This will return the fields that the trigger is monitoring - * - * @param string $trigger Name of the trigger - * @return array - */ - protected function triggerFieldsFromTrigger($trigger) { - if($trigger){ - $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); + /** + * This will return the fields that the trigger is monitoring + * + * @param string $trigger Name of the trigger + * @return array + */ + protected function triggerFieldsFromTrigger($trigger) + { + if ($trigger) { + $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); - $fields=$bits[2]; + $fields=$bits[2]; - $field_bits=explode(',', str_replace('"', '', $fields)); - $result=array(); - foreach($field_bits as $field_bit) - $result[]=trim($field_bit); + $field_bits=explode(',', str_replace('"', '', $fields)); + $result=array(); + foreach ($field_bits as $field_bit) { + $result[]=trim($field_bit); + } - return $result; - } else { - return false; - } - } + return $result; + } else { + return false; + } + } - /** - * Return a boolean type-formatted string - * - * @param array $values Contains a tokenised list of info about this data type - * @param boolean $asDbValue - * @return string - */ - public function boolean($values, $asDbValue=false){ - //Annoyingly, we need to do a good ol' fashioned switch here: - $default = $values['default'] ? '1' : '0'; + /** + * Return a boolean type-formatted string + * + * @param array $values Contains a tokenised list of info about this data type + * @param boolean $asDbValue + * @return string + */ + public function boolean($values, $asDbValue=false) + { + //Annoyingly, we need to do a good ol' fashioned switch here: + $default = $values['default'] ? '1' : '0'; - if(!isset($values['arrayValue'])) { - $values['arrayValue']=''; - } + if (!isset($values['arrayValue'])) { + $values['arrayValue']=''; + } - if($asDbValue) { - return array('data_type'=>'smallint'); - } + if ($asDbValue) { + return array('data_type'=>'smallint'); + } - if($values['arrayValue'] != '') { - $default = ''; - } else { - $default = ' default ' . (int)$values['default']; - } - return "smallint{$values['arrayValue']}" . $default; - } + if ($values['arrayValue'] != '') { + $default = ''; + } else { + $default = ' default ' . (int)$values['default']; + } + return "smallint{$values['arrayValue']}" . $default; + } - /** - * Return a date type-formatted string - * - * @param array $values Contains a tokenised list of info about this data type - * @return string - */ - public function date($values){ + /** + * Return a date type-formatted string + * + * @param array $values Contains a tokenised list of info about this data type + * @return string + */ + public function date($values) + { + if (!isset($values['arrayValue'])) { + $values['arrayValue']=''; + } - if(!isset($values['arrayValue'])) { - $values['arrayValue']=''; - } + return "date{$values['arrayValue']}"; + } - return "date{$values['arrayValue']}"; - } + /** + * Return a decimal type-formatted string + * + * @param array $values Contains a tokenised list of info about this data type + * @param boolean $asDbValue + * @return string + */ + public function decimal($values, $asDbValue=false) + { + if (!isset($values['arrayValue'])) { + $values['arrayValue']=''; + } - /** - * Return a decimal type-formatted string - * - * @param array $values Contains a tokenised list of info about this data type - * @param boolean $asDbValue - * @return string - */ - public function decimal($values, $asDbValue=false){ + // Avoid empty strings being put in the db + if ($values['precision'] == '') { + $precision = 1; + } else { + $precision = $values['precision']; + } - if(!isset($values['arrayValue'])) { - $values['arrayValue']=''; - } + $defaultValue = ''; + if (isset($values['default']) && is_numeric($values['default'])) { + $defaultValue = ' default ' . $values['default']; + } - // Avoid empty strings being put in the db - if($values['precision'] == '') { - $precision = 1; - } else { - $precision = $values['precision']; - } + if ($asDbValue) { + return array('data_type' => 'numeric', 'precision' => $precision); + } else { + return "decimal($precision){$values['arrayValue']}$defaultValue"; + } + } - $defaultValue = ''; - if(isset($values['default']) && is_numeric($values['default'])) { - $defaultValue = ' default ' . $values['default']; - } + /** + * Return a enum type-formatted string + * + * @param array $values Contains a tokenised list of info about this data type + * @return string + */ + public function enum($values) + { + //Enums are a bit different. We'll be creating a varchar(255) with a constraint of all the usual enum options. + //NOTE: In this one instance, we are including the table name in the values array + if (!isset($values['arrayValue'])) { + $values['arrayValue']=''; + } - if($asDbValue) { - return array('data_type' => 'numeric', 'precision' => $precision); - } else { - return "decimal($precision){$values['arrayValue']}$defaultValue"; - } - } + if ($values['arrayValue']!='') { + $default = ''; + } else { + $default = " default '{$values['default']}'"; + } - /** - * Return a enum type-formatted string - * - * @param array $values Contains a tokenised list of info about this data type - * @return string - */ - public function enum($values){ - //Enums are a bit different. We'll be creating a varchar(255) with a constraint of all the usual enum options. - //NOTE: In this one instance, we are including the table name in the values array - if(!isset($values['arrayValue'])) { - $values['arrayValue']=''; - } + return "varchar(255){$values['arrayValue']}" . $default . " check (\"" . $values['name'] . "\" in ('" . implode('\', \'', $values['enums']) . "'))"; + } - if($values['arrayValue']!='') { - $default = ''; - } else { - $default = " default '{$values['default']}'"; - } + /** + * Return a float type-formatted string + * + * @param array $values Contains a tokenised list of info about this data type + * @param boolean $asDbValue + * @return string + */ + public function float($values, $asDbValue = false) + { + if (!isset($values['arrayValue'])) { + $values['arrayValue']=''; + } - return "varchar(255){$values['arrayValue']}" . $default . " check (\"" . $values['name'] . "\" in ('" . implode('\', \'', $values['enums']) . "'))"; + if ($asDbValue) { + return array('data_type' => 'double precision'); + } else { + return "float{$values['arrayValue']}"; + } + } - } + /** + * Return a float type-formatted string cause double is not supported + * + * @param array $values Contains a tokenised list of info about this data type + * @param boolean $asDbValue + * @return string + */ + public function double($values, $asDbValue=false) + { + return $this->float($values, $asDbValue); + } - /** - * Return a float type-formatted string - * - * @param array $values Contains a tokenised list of info about this data type - * @param boolean $asDbValue - * @return string - */ - public function float($values, $asDbValue = false){ - if(!isset($values['arrayValue'])) { - $values['arrayValue']=''; - } + /** + * Return a int type-formatted string + * + * @param array $values Contains a tokenised list of info about this data type + * @param boolean $asDbValue + * @return string + */ + public function int($values, $asDbValue = false) + { + if (!isset($values['arrayValue'])) { + $values['arrayValue']=''; + } - if($asDbValue) { - return array('data_type' => 'double precision'); - } else { - return "float{$values['arrayValue']}"; - } - } + if ($asDbValue) { + return array('data_type'=>'integer', 'precision'=>'32'); + } - /** - * Return a float type-formatted string cause double is not supported - * - * @param array $values Contains a tokenised list of info about this data type - * @param boolean $asDbValue - * @return string - */ - public function double($values, $asDbValue=false){ - return $this->float($values, $asDbValue); - } + if ($values['arrayValue']!='') { + $default=''; + } else { + $default=' default ' . (int)$values['default']; + } - /** - * Return a int type-formatted string - * - * @param array $values Contains a tokenised list of info about this data type - * @param boolean $asDbValue - * @return string - */ - public function int($values, $asDbValue = false){ + return "integer{$values['arrayValue']}" . $default; + } - if(!isset($values['arrayValue'])) { - $values['arrayValue']=''; - } + /** + * Return a bigint type-formatted string + * + * @param array $values Contains a tokenised list of info about this data type + * @param boolean $asDbValue + * @return string + */ + public function bigint($values, $asDbValue = false) + { + if (!isset($values['arrayValue'])) { + $values['arrayValue']=''; + } - if($asDbValue) { - return Array('data_type'=>'integer', 'precision'=>'32'); - } + if ($asDbValue) { + return array('data_type'=>'bigint', 'precision'=>'64'); + } - if($values['arrayValue']!='') { - $default=''; - } else { - $default=' default ' . (int)$values['default']; - } + if ($values['arrayValue']!='') { + $default=''; + } else { + $default=' default ' . (int)$values['default']; + } - return "integer{$values['arrayValue']}" . $default; - } + return "bigint{$values['arrayValue']}" . $default; + } - /** - * Return a bigint type-formatted string - * - * @param array $values Contains a tokenised list of info about this data type - * @param boolean $asDbValue - * @return string - */ - public function bigint($values, $asDbValue = false){ + /** + * Return a datetime type-formatted string + * For PostgreSQL, we simply return the word 'timestamp', no other parameters are necessary + * + * @param array $values Contains a tokenised list of info about this data type + * @param boolean $asDbValue + * @return string + */ + public function SS_Datetime($values, $asDbValue = false) + { + if (!isset($values['arrayValue'])) { + $values['arrayValue']=''; + } - if(!isset($values['arrayValue'])) { - $values['arrayValue']=''; - } + if ($asDbValue) { + return array('data_type'=>'timestamp without time zone'); + } else { + return "timestamp{$values['arrayValue']}"; + } + } - if($asDbValue) { - return Array('data_type'=>'bigint', 'precision'=>'64'); - } + /** + * Return a text type-formatted string + * + * @param array $values Contains a tokenised list of info about this data type + * @param boolean $asDbValue + * @return string + */ + public function text($values, $asDbValue = false) + { + if (!isset($values['arrayValue'])) { + $values['arrayValue'] = ''; + } - if($values['arrayValue']!='') { - $default=''; - } else { - $default=' default ' . (int)$values['default']; - } + if ($asDbValue) { + return array('data_type'=>'text'); + } else { + return "text{$values['arrayValue']}"; + } + } - return "bigint{$values['arrayValue']}" . $default; - } + /** + * Return a time type-formatted string + * + * @param array $values Contains a tokenised list of info about this data type + * @return string + */ + public function time($values) + { + if (!isset($values['arrayValue'])) { + $values['arrayValue'] = ''; + } - /** - * Return a datetime type-formatted string - * For PostgreSQL, we simply return the word 'timestamp', no other parameters are necessary - * - * @param array $values Contains a tokenised list of info about this data type - * @param boolean $asDbValue - * @return string - */ - public function SS_Datetime($values, $asDbValue = false){ + return "time{$values['arrayValue']}"; + } - if(!isset($values['arrayValue'])) { - $values['arrayValue']=''; - } + /** + * Return a varchar type-formatted string + * + * @param array $values Contains a tokenised list of info about this data type + * @param boolean $asDbValue + * @return string + */ + public function varchar($values, $asDbValue=false) + { + if (!isset($values['arrayValue'])) { + $values['arrayValue'] = ''; + } - if($asDbValue) { - return array('data_type'=>'timestamp without time zone'); - } else { - return "timestamp{$values['arrayValue']}"; - } - } + if (!isset($values['precision'])) { + $values['precision'] = 255; + } - /** - * Return a text type-formatted string - * - * @param array $values Contains a tokenised list of info about this data type - * @param boolean $asDbValue - * @return string - */ - public function text($values, $asDbValue = false){ + if ($asDbValue) { + return array('data_type'=>'varchar', 'precision'=>$values['precision']); + } else { + return "varchar({$values['precision']}){$values['arrayValue']}"; + } + } - if(!isset($values['arrayValue'])) { - $values['arrayValue'] = ''; - } + /* + * Return a 4 digit numeric type. MySQL has a proprietary 'Year' type. + * For Postgres, we'll use a 4 digit numeric + * + * @param array $values Contains a tokenised list of info about this data type + * @param boolean $asDbValue + * @return string + */ + public function year($values, $asDbValue = false) + { + if (!isset($values['arrayValue'])) { + $values['arrayValue'] = ''; + } - if($asDbValue) { - return array('data_type'=>'text'); - } else { - return "text{$values['arrayValue']}"; - } - } + //TODO: the DbValue result does not include the numeric_scale option (ie, the ,0 value in 4,0) + if ($asDbValue) { + return array('data_type'=>'decimal', 'precision'=>'4'); + } else { + return "decimal(4,0){$values['arrayValue']}"; + } + } - /** - * Return a time type-formatted string - * - * @param array $values Contains a tokenised list of info about this data type - * @return string - */ - public function time($values){ - if(!isset($values['arrayValue'])) { - $values['arrayValue'] = ''; - } + /** + * Create a fulltext search datatype for PostgreSQL + * This will also return a trigger to be applied to this table + * + * @todo: create custom functions to allow weighted searches + * + * @param array $this_index Index specification for the fulltext index + * @param string $tableName + * @param string $name + * @param array $spec + */ + protected function fulltext($this_index, $tableName, $name) + { + //For full text search, we need to create a column for the index + $columns = $this->quoteColumnSpecString($this_index['value']); - return "time{$values['arrayValue']}"; - } + $fulltexts = "\"ts_$name\" tsvector"; + $triggerName = $this->buildPostgresTriggerName($tableName, $name); + $language = PostgreSQLDatabase::search_language(); - /** - * Return a varchar type-formatted string - * - * @param array $values Contains a tokenised list of info about this data type - * @param boolean $asDbValue - * @return string - */ - public function varchar($values, $asDbValue=false){ - - if(!isset($values['arrayValue'])) { - $values['arrayValue'] = ''; - } - - if(!isset($values['precision'])) { - $values['precision'] = 255; - } - - if($asDbValue) { - return array('data_type'=>'varchar', 'precision'=>$values['precision']); - } else { - return "varchar({$values['precision']}){$values['arrayValue']}"; - } - } - - /* - * Return a 4 digit numeric type. MySQL has a proprietary 'Year' type. - * For Postgres, we'll use a 4 digit numeric - * - * @param array $values Contains a tokenised list of info about this data type - * @param boolean $asDbValue - * @return string - */ - public function year($values, $asDbValue = false){ - - if(!isset($values['arrayValue'])) { - $values['arrayValue'] = ''; - } - - //TODO: the DbValue result does not include the numeric_scale option (ie, the ,0 value in 4,0) - if($asDbValue) { - return array('data_type'=>'decimal', 'precision'=>'4'); - } else { - return "decimal(4,0){$values['arrayValue']}"; - } - } - - /** - * Create a fulltext search datatype for PostgreSQL - * This will also return a trigger to be applied to this table - * - * @todo: create custom functions to allow weighted searches - * - * @param array $this_index Index specification for the fulltext index - * @param string $tableName - * @param string $name - * @param array $spec - */ - protected function fulltext($this_index, $tableName, $name){ - //For full text search, we need to create a column for the index - $columns = $this->quoteColumnSpecString($this_index['value']); - - $fulltexts = "\"ts_$name\" tsvector"; - $triggerName = $this->buildPostgresTriggerName($tableName, $name); - $language = PostgreSQLDatabase::search_language(); - - $this->dropTrigger($triggerName, $tableName); - $triggers = "CREATE TRIGGER \"$triggerName\" BEFORE INSERT OR UPDATE + $this->dropTrigger($triggerName, $tableName); + $triggers = "CREATE TRIGGER \"$triggerName\" BEFORE INSERT OR UPDATE ON \"$tableName\" FOR EACH ROW EXECUTE PROCEDURE tsvector_update_trigger(\"ts_$name\", 'pg_catalog.$language', $columns);"; - return array( - 'name' => $name, - 'ts_name' => "ts_{$name}", - 'fulltexts' => $fulltexts, - 'triggers' => $triggers - ); - } + return array( + 'name' => $name, + 'ts_name' => "ts_{$name}", + 'fulltexts' => $fulltexts, + 'triggers' => $triggers + ); + } - public function IdColumn($asDbValue = false, $hasAutoIncPK = true){ - if($asDbValue) return 'bigint'; - else return 'serial8 not null'; - } + public function IdColumn($asDbValue = false, $hasAutoIncPK = true) + { + if ($asDbValue) { + return 'bigint'; + } else { + return 'serial8 not null'; + } + } - public function hasTable($tableName) { - $result = $this->preparedQuery( - "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = ? AND tablename = ?;", - array($this->database->currentSchema(), $tableName) - ); - return ($result->numRecords() > 0); - } + public function hasTable($tableName) + { + $result = $this->preparedQuery( + "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = ? AND tablename = ?;", + array($this->database->currentSchema(), $tableName) + ); + return ($result->numRecords() > 0); + } - /** - * Returns the values of the given enum field - * - * @todo Make a proper implementation - * - * @param string $tableName Name of table to check - * @param string $fieldName name of enum field to check - * @return array List of enum values - */ - public function enumValuesForField($tableName, $fieldName) { - //return array('SiteTree','Page'); - $constraints = $this->constraintExists("{$tableName}_{$fieldName}_check"); - if($constraints) { - return $this->enumValuesFromConstraint($constraints['pg_get_constraintdef']); - } else { - return array(); - } - } + /** + * Returns the values of the given enum field + * + * @todo Make a proper implementation + * + * @param string $tableName Name of table to check + * @param string $fieldName name of enum field to check + * @return array List of enum values + */ + public function enumValuesForField($tableName, $fieldName) + { + //return array('SiteTree','Page'); + $constraints = $this->constraintExists("{$tableName}_{$fieldName}_check"); + if ($constraints) { + return $this->enumValuesFromConstraint($constraints['pg_get_constraintdef']); + } else { + return array(); + } + } - /** - * Get the actual enum fields from the constraint value: - * - * @param string $constraint - * @return array - */ - protected function enumValuesFromConstraint($constraint){ - $constraint = substr($constraint, strpos($constraint, 'ANY (ARRAY[')+11); - $constraint = substr($constraint, 0, -11); - $constraints = array(); - $segments = explode(',', $constraint); - foreach($segments as $this_segment){ - $bits = preg_split('/ *:: */', $this_segment); - array_unshift($constraints, trim($bits[0], " '")); - } - return $constraints; - } + /** + * Get the actual enum fields from the constraint value: + * + * @param string $constraint + * @return array + */ + protected function enumValuesFromConstraint($constraint) + { + $constraint = substr($constraint, strpos($constraint, 'ANY (ARRAY[')+11); + $constraint = substr($constraint, 0, -11); + $constraints = array(); + $segments = explode(',', $constraint); + foreach ($segments as $this_segment) { + $bits = preg_split('/ *:: */', $this_segment); + array_unshift($constraints, trim($bits[0], " '")); + } + return $constraints; + } - public function dbDataType($type){ - $values = array( - 'unsigned integer' => 'INT' - ); + public function dbDataType($type) + { + $values = array( + 'unsigned integer' => 'INT' + ); - if(isset($values[$type])) return $values[$type]; - else return ''; - } + if (isset($values[$type])) { + return $values[$type]; + } else { + return ''; + } + } - /* - * Given a tablespace and and location, either create a new one - * or update the existing one - * - * @param string $name - * @param string $location - */ - public function createOrReplaceTablespace($name, $location){ - $existing = $this->preparedQuery( - "SELECT spcname, spclocation FROM pg_tablespace WHERE spcname = ?;", - array($name) - )->first(); + /* + * Given a tablespace and and location, either create a new one + * or update the existing one + * + * @param string $name + * @param string $location + */ + public function createOrReplaceTablespace($name, $location) + { + $existing = $this->preparedQuery( + "SELECT spcname, spclocation FROM pg_tablespace WHERE spcname = ?;", + array($name) + )->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 :( + //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 :( - //If a tablespace with this name exists, but the location has changed, then drop the current one - //if($existing && $location!=$existing['spclocation']) - // DB::query("DROP TABLESPACE $name;"); + //If a tablespace with this name exists, but the location has changed, then drop the current one + //if($existing && $location!=$existing['spclocation']) + // DB::query("DROP TABLESPACE $name;"); - //If this is a new tablespace, or we have dropped the current one: - if(!$existing || ($existing && $location != $existing['spclocation'])) { - $this->query("CREATE TABLESPACE $name LOCATION '$location';"); - } - } + //If this is a new tablespace, or we have dropped the current one: + if (!$existing || ($existing && $location != $existing['spclocation'])) { + $this->query("CREATE TABLESPACE $name LOCATION '$location';"); + } + } - /** - * - * @param string $tableName - * @param array $partitions - * @param array $indexes - * @param array $extensions - */ - public function createOrReplacePartition($tableName, $partitions, $indexes, $extensions){ + /** + * + * @param string $tableName + * @param array $partitions + * @param array $indexes + * @param array $extensions + */ + public function createOrReplacePartition($tableName, $partitions, $indexes, $extensions) + { - //We need the plpgsql language to be installed for this to work: - $this->createLanguage('plpgsql'); + //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']; - } else { - $tableSpace=''; - } + //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']; + } else { + $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;"); - } else { - //Drop the constraint, we will recreate in in the next line - $existing_constraint = $this->preparedQuery( - "SELECT conname FROM pg_constraint WHERE conname = ?;", - array("{$partition_name}_pkey") - ); - if($existing_constraint){ - $this->query("ALTER TABLE \"$partition_name\" DROP CONSTRAINT \"{$partition_name}_pkey\";"); - } - $this->dropTrigger(strtolower('trigger_' . $tableName . '_insert'), $tableName); - } + 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;"); + } else { + //Drop the constraint, we will recreate in in the next line + $existing_constraint = $this->preparedQuery( + "SELECT conname FROM pg_constraint WHERE conname = ?;", + array("{$partition_name}_pkey") + ); + if ($existing_constraint) { + $this->query("ALTER TABLE \"$partition_name\" DROP CONSTRAINT \"{$partition_name}_pkey\";"); + } + $this->dropTrigger(strtolower('trigger_' . $tableName . '_insert'), $tableName); + } - $this->query("ALTER TABLE \"$partition_name\" ADD CONSTRAINT \"{$partition_name}_pkey\" PRIMARY KEY (\"ID\");"); + $this->query("ALTER TABLE \"$partition_name\" ADD CONSTRAINT \"{$partition_name}_pkey\" PRIMARY KEY (\"ID\");"); - if($first){ - $trigger.='IF'; - $first=false; - } else { - $trigger.='ELSIF'; - } + if ($first) { + $trigger.='IF'; + $first=false; + } else { + $trigger.='ELSIF'; + } - $trigger .= " ($partition_value) THEN INSERT INTO \"$partition_name\" VALUES (NEW.*);"; + $trigger .= " ($partition_value) THEN INSERT INTO \"$partition_name\" VALUES (NEW.*);"; - if($indexes){ - // 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 ($indexes) { + // 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') { + $fillfactor = $where = ''; + if (isset($this_index['fillfactor'])) { + $fillfactor = 'WITH (FILLFACTOR = ' . $this_index['fillfactor'] . ')'; + } + if (isset($this_index['where'])) { + $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"); + $ts_details = $this->fulltext($this_index, $partition_name, $name); + $this->query($ts_details['triggers']); + } else { + if (is_array($this_index)) { + $index_name = $this_index['name']; + } else { + $index_name = trim($this_index, '()'); + } - if($this_index['type']=='fulltext'){ - $fillfactor = $where = ''; - if(isset($this_index['fillfactor'])) { - $fillfactor = 'WITH (FILLFACTOR = ' . $this_index['fillfactor'] . ')'; - } - if(isset($this_index['where'])) { - $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"); - $ts_details = $this->fulltext($this_index, $partition_name, $name); - $this->query($ts_details['triggers']); - } else { + $createIndex = $this->getIndexSqlDefinition($partition_name, $index_name, $this_index); + if ($createIndex !== false) { + $this->query($createIndex); + } + } + } + } - if(is_array($this_index)) { - $index_name = $this_index['name']; - } else { - $index_name = trim($this_index, '()'); - } + //Lastly, clustering goes here: + if ($extensions && isset($extensions['cluster'])) { + $this->query("CLUSTER \"$partition_name\" USING \"{$extensions['cluster']}\";"); + } + } - $createIndex = $this->getIndexSqlDefinition($partition_name, $index_name, $this_index); - if($createIndex !== false) { - $this->query($createIndex); - } - } - } - } + $trigger .= 'ELSE RAISE EXCEPTION \'Value id out of range. Fix the ' . $tableName . '_insert_trigger() function!\'; END IF; RETURN NULL; END; $$ LANGUAGE plpgsql;'; + $trigger .= 'CREATE TRIGGER trigger_' . $tableName . '_insert BEFORE INSERT ON "' . $tableName . '" FOR EACH ROW EXECUTE PROCEDURE ' . $tableName . '_insert_trigger();'; - //Lastly, clustering goes here: - if($extensions && isset($extensions['cluster'])){ - $this->query("CLUSTER \"$partition_name\" USING \"{$extensions['cluster']}\";"); - } - } + $this->query($trigger); + } - $trigger .= 'ELSE RAISE EXCEPTION \'Value id out of range. Fix the ' . $tableName . '_insert_trigger() function!\'; END IF; RETURN NULL; END; $$ LANGUAGE plpgsql;'; - $trigger .= 'CREATE TRIGGER trigger_' . $tableName . '_insert BEFORE INSERT ON "' . $tableName . '" FOR EACH ROW EXECUTE PROCEDURE ' . $tableName . '_insert_trigger();'; + /* + * This will create a language if it doesn't already exist. + * This is used by the createOrReplacePartition function, which needs plpgsql + * + * @param string $language Language name + */ + public function createLanguage($language) + { + $result = $this->preparedQuery( + "SELECT lanname FROM pg_language WHERE lanname = ?;", + array($language) + )->first(); - $this->query($trigger); - } + if (!$result) { + $this->query("CREATE LANGUAGE $language;"); + } + } - /* - * This will create a language if it doesn't already exist. - * This is used by the createOrReplacePartition function, which needs plpgsql - * - * @param string $language Language name - */ - public function createLanguage($language){ - $result = $this->preparedQuery( - "SELECT lanname FROM pg_language WHERE lanname = ?;", - array($language) - )->first(); - - if(!$result) { - $this->query("CREATE LANGUAGE $language;"); - } - } - - /** - * Return a set type-formatted string - * This is used for Multi-enum support, which isn't actually supported by Postgres. - * Throws a user error to show our lack of support, and return an "int", specifically for sapphire - * tests that test multi-enums. This results in a test failure, but not crashing the test run. - * - * @param array $values Contains a tokenised list of info about this data type - * @return string - */ - public function set($values){ - user_error("PostGreSQL does not support multi-enum", E_USER_ERROR); - return "int"; - } + /** + * Return a set type-formatted string + * This is used for Multi-enum support, which isn't actually supported by Postgres. + * Throws a user error to show our lack of support, and return an "int", specifically for sapphire + * tests that test multi-enums. This results in a test failure, but not crashing the test run. + * + * @param array $values Contains a tokenised list of info about this data type + * @return string + */ + public function set($values) + { + user_error("PostGreSQL does not support multi-enum", E_USER_ERROR); + return "int"; + } } diff --git a/tests/PostgreSQLConnectorTest.php b/tests/PostgreSQLConnectorTest.php index cdc4d74..ef4892f 100644 --- a/tests/PostgreSQLConnectorTest.php +++ b/tests/PostgreSQLConnectorTest.php @@ -5,45 +5,47 @@ * * @author Damian */ -class PostgreSQLConnectorTest extends SapphireTest { +class PostgreSQLConnectorTest extends SapphireTest +{ - public function testSubstitutesPlaceholders() { - $connector = new PostgreSQLConnector(); + public function testSubstitutesPlaceholders() + { + $connector = new PostgreSQLConnector(); - // basic case - $this->assertEquals( - "SELECT * FROM Table WHERE ID = $1", - $connector->replacePlaceholders("SELECT * FROM Table WHERE ID = ?") - ); + // basic case + $this->assertEquals( + "SELECT * FROM Table WHERE ID = $1", + $connector->replacePlaceholders("SELECT * FROM Table WHERE ID = ?") + ); - // Multiple variables - $this->assertEquals( - "SELECT * FROM Table WHERE ID = $1 AND Name = $2", - $connector->replacePlaceholders("SELECT * FROM Table WHERE ID = ? AND Name = ?") - ); + // Multiple variables + $this->assertEquals( + "SELECT * FROM Table WHERE ID = $1 AND Name = $2", + $connector->replacePlaceholders("SELECT * FROM Table WHERE ID = ? AND Name = ?") + ); - // Ignoring question mark placeholders within string literals - $this->assertEquals( - "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?

'" - ) - ); + // Ignoring question mark placeholders within string literals + $this->assertEquals( + "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?

'" + ) + ); - // 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", - $connector->replacePlaceholders( - "SELECT * FROM Table WHERE ID = ? AND Title = '\\'' AND Content = '

What is love?

' AND Name = ?" - ) - ); + // 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", + $connector->replacePlaceholders( + "SELECT * FROM Table WHERE ID = ? AND Title = '\\'' AND Content = '

What is love?

' AND Name = ?" + ) + ); - // 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", - $connector->replacePlaceholders( - "SELECT * FROM Table WHERE ID = ? AND Title = '''' AND Content = '

What is love?

' AND Name = ?" - ) - ); - } + // 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", + $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 8f6fc69..393538d 100644 --- a/tests/PostgreSQLDatabaseTest.php +++ b/tests/PostgreSQLDatabaseTest.php @@ -3,46 +3,44 @@ * @package postgresql * @subpackage tests */ -class PostgreSQLDatabaseTest extends SapphireTest { - function testReadOnlyTransaction(){ +class PostgreSQLDatabaseTest extends SapphireTest +{ + public function testReadOnlyTransaction() + { + if ( + DB::get_conn()->supportsTransactions() == true + && DB::get_conn() instanceof PostgreSQLDatabase + ) { + $page=new Page(); + $page->Title='Read only success'; + $page->write(); - if( - DB::get_conn()->supportsTransactions() == true - && DB::get_conn() instanceof PostgreSQLDatabase - ){ + DB::get_conn()->transactionStart('READ ONLY'); - $page=new Page(); - $page->Title='Read only success'; - $page->write(); + try { + $page=new Page(); + $page->Title='Read only page failed'; + $page->write(); + } catch (Exception $e) { + //could not write this record + //We need to do a rollback or a commit otherwise we'll get error messages + DB::get_conn()->transactionRollback(); + } - DB::get_conn()->transactionStart('READ ONLY'); + DB::get_conn()->transactionEnd(); - try { - $page=new Page(); - $page->Title='Read only page failed'; - $page->write(); - } catch (Exception $e) { - //could not write this record - //We need to do a rollback or a commit otherwise we'll get error messages - DB::get_conn()->transactionRollback(); - } + DataObject::flush_and_destroy_cache(); - DB::get_conn()->transactionEnd(); + $success=DataObject::get('Page', "\"Title\"='Read only success'"); + $fail=DataObject::get('Page', "\"Title\"='Read only page failed'"); - DataObject::flush_and_destroy_cache(); + //This page should be in the system + $this->assertTrue(is_object($success) && $success->exists()); - $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()); - - //This page should NOT exist, we had 'read only' permissions - $this->assertFalse(is_object($fail) && $fail->exists()); - - } else { - $this->markTestSkipped('Current database is not PostgreSQL'); - } - - } + //This page should NOT exist, we had 'read only' permissions + $this->assertFalse(is_object($fail) && $fail->exists()); + } else { + $this->markTestSkipped('Current database is not PostgreSQL'); + } + } }