2008-11-23 02:20:39 +01:00
< ? php
/**
* @ package sapphire
* @ subpackage model
*/
/**
* PostgreSQL connector class .
* @ package sapphire
* @ subpackage model
*/
2009-11-01 22:59:54 +01:00
class PostgreSQLDatabase extends SS_Database {
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
* Connection to the DBMS .
* @ var resource
*/
private $dbConn ;
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
* True if we are connected to a database .
* @ var boolean
*/
private $active ;
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
* The name of the database .
* @ var string
*/
private $database ;
2011-01-11 22:17:17 +01:00
2009-09-16 05:51:38 +02:00
/*
* This holds the name of the original database
* So if you switch to another for unit tests , you
2011-01-11 22:17:17 +01:00
* can then switch back in order to drop the temp database
2009-09-16 05:51:38 +02:00
*/
private $database_original ;
2011-01-11 22:17:17 +01:00
/**
2012-09-17 06:15:00 +02:00
* The database schema name .
* @ var string
*/
private $schema ;
2011-01-11 22:17:17 +01:00
2009-09-29 22:59:45 +02:00
/*
* This holds the parameters that the original connection was created with ,
* so we can switch back to it if necessary ( used for unit tests )
*/
2009-09-16 05:51:38 +02:00
private $parameters ;
2011-01-11 22:17:17 +01:00
2009-09-29 22:59:45 +02:00
/*
* These two values describe how T - search will work .
* You can use either GiST or GIN , and '@@' ( gist ) or '@@@' ( gin )
* Combinations of these two will also work , so you ' ll need to pick
* one which works best for you
*/
public $default_fts_cluster_method = 'GIN' ;
public $default_fts_search_method = '@@@' ;
2011-01-11 22:17:17 +01:00
2009-10-01 23:01:23 +02:00
private $supportsTransactions = true ;
2011-01-11 22:17:17 +01:00
2010-08-13 01:08:35 +02:00
/**
2011-01-11 22:17:17 +01:00
* Determines whether to check a database exists on the host by
2010-08-13 01:08:35 +02:00
* querying the 'postgres' database and running createDatabase .
2011-01-11 22:17:17 +01:00
*
2010-08-13 01:08:35 +02:00
* Some locked down systems prevent access to the 'postgres' table in
* which case you need to set this to false .
*/
public static $check_database_exists = true ;
2011-01-11 22:17:17 +01:00
2010-11-01 02:11:31 +01:00
/**
* This holds a copy of all the constraint results that are returned
2011-01-11 22:17:17 +01:00
* via the function constraintExists () . This is a bit faster than
2010-11-01 02:11:31 +01:00
* repeatedly querying this column , and should allow the database
* to use it ' s built - in caching features for better queries .
2011-01-11 22:17:17 +01:00
*
2010-11-01 02:11:31 +01:00
* @ var array
*/
private static $cached_constraints = array ();
2011-01-11 22:17:17 +01:00
2010-12-02 23:33:25 +01:00
/**
2011-01-11 22:17:17 +01:00
*
2010-12-02 23:33:25 +01:00
* This holds a copy of all the queries that run through the function orderMoreSpecifically ()
* It appears to be a performance bottleneck at times .
2011-01-11 22:17:17 +01:00
*
2010-12-02 23:33:25 +01:00
* @ var array
*/
private static $cached_ordered_specifically = array ();
2011-01-11 22:17:17 +01:00
/**
*
* 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
*/
private static $cached_fieldlists = array ();
2010-11-25 04:45:32 +01:00
/**
* 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 .
2011-01-11 22:17:17 +01:00
*
2010-11-25 04:45:32 +01:00
* @ var string
*/
private $search_language = 'english' ;
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
* Connect to a PostgreSQL database .
* @ param array $parameters An map of parameters , which should include :
* - server : The server , eg , localhost
* - username : The username to log on with
* - password : The password to log on with
* - database : The database to connect to
*/
public function __construct ( $parameters ) {
2011-01-11 22:17:17 +01:00
2009-09-16 05:51:38 +02:00
//We will store these connection parameters for use elsewhere (ie, unit tests)
$this -> parameters = $parameters ;
$this -> connectDatabase ();
2011-01-11 22:17:17 +01:00
2009-09-16 05:51:38 +02:00
$this -> database_original = $this -> database ;
}
2011-01-11 22:17:17 +01:00
2009-09-16 05:51:38 +02:00
/*
* Uses whatever connection details are in the $parameters array to connect to a database of a given name
*/
2010-02-12 04:28:04 +01:00
function connectDatabase (){
2011-01-11 22:17:17 +01:00
2010-02-12 04:28:04 +01:00
$parameters = $this -> parameters ;
2011-01-11 22:17:17 +01:00
2010-02-12 04:28:04 +01:00
if ( ! $parameters )
return false ;
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
( $parameters [ 'username' ] != '' ) ? $username = ' user=' . $parameters [ 'username' ] : $username = '' ;
2013-12-20 02:34:31 +01:00
( $parameters [ 'password' ] != '' ) ? $password = ' password=\'' . $parameters [ 'password' ] . '\'' : $password = '' ;
2011-01-11 22:17:17 +01:00
2010-02-12 04:28:04 +01:00
if ( ! isset ( $this -> database ))
$dbName = $parameters [ 'database' ];
else $dbName = $this -> database ;
2011-01-11 22:17:17 +01:00
2010-04-13 06:42:36 +02:00
$port = empty ( $parameters [ 'port' ]) ? 5432 : $parameters [ 'port' ];
2011-01-11 22:17:17 +01:00
2010-08-13 01:08:35 +02:00
// First, we need to check that this database exists. To do this, we will connect to the 'postgres' database first
// some setups prevent access to this database so set PostgreSQLDatabase::$check_database_exists = false
if ( self :: $check_database_exists ) {
$this -> dbConn = pg_connect ( 'host=' . $parameters [ 'server' ] . ' port=' . $port . ' dbname=postgres' . $username . $password );
2011-01-11 22:17:17 +01:00
2013-02-26 23:17:11 +01:00
if ( ! $this -> dbConn ) {
throw new ErrorException ( " Couldn't connect to PostgreSQL database " );
2013-12-20 02:34:31 +01:00
} elseif ( pg_connection_status ( $this -> dbConn ) != PGSQL_CONNECTION_OK ) {
throw new ErrorException ( pg_last_error ( $this -> dbConn ));
2013-02-26 23:17:11 +01:00
}
if ( ! $this -> databaseExists ( $dbName )) {
2010-08-13 01:08:35 +02:00
$this -> createDatabase ( $dbName );
2013-02-26 23:17:11 +01:00
}
2010-08-13 01:08:35 +02:00
}
2011-01-11 22:17:17 +01:00
2010-03-08 22:58:28 +01:00
//Now we can be sure that this database exists, so we can connect to it
2010-04-13 06:42:36 +02:00
$this -> dbConn = pg_connect ( 'host=' . $parameters [ 'server' ] . ' port=' . $port . ' dbname=' . $dbName . $username . $password );
2011-01-11 22:17:17 +01:00
2013-02-26 23:17:11 +01:00
if ( ! $this -> dbConn ) {
throw new ErrorException ( " Couldn't connect to PostgreSQL database " );
2013-12-20 02:34:31 +01:00
} elseif ( pg_connection_status ( $this -> dbConn ) != PGSQL_CONNECTION_OK ) {
throw new ErrorException ( pg_last_error ( $this -> dbConn ));
2013-02-26 23:17:11 +01:00
}
2010-02-12 04:28:04 +01:00
//By virtue of getting here, the connection is active:
$this -> active = true ;
2009-09-16 05:51:38 +02:00
$this -> database = $dbName ;
2011-01-11 22:17:17 +01:00
// Set up the schema if required
$schema = isset ( $parameters [ 'schema' ]) ? $parameters [ 'schema' ] : $this -> currentSchema ();
// Edge-case - database with no schemas:
if ( ! $schema ) $schema = " public " ;
if ( ! $this -> schemaExists ( $schema ))
2010-11-25 04:45:32 +01:00
$this -> createSchema ( $schema );
2011-01-11 22:17:17 +01:00
$this -> setSchema ( $schema );
2012-04-16 01:49:35 +02:00
// Set the timezone if required.
if ( isset ( $parameters [ 'timezone' ])) $this -> query ( sprintf ( " SET SESSION TIME ZONE '%s' " , $parameters [ 'timezone' ]));
2009-09-16 05:51:38 +02:00
return true ;
2008-11-23 02:20:39 +01:00
}
/**
* Not implemented , needed for PDO
*/
public function getConnect ( $parameters ) {
return null ;
}
2011-01-11 22:17:17 +01:00
2010-04-13 06:42:36 +02:00
/**
* Return the parameters used to construct this database connection
*/
public function getParameters () {
return $this -> parameters ;
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
* Returns true if this database supports collations
2009-09-16 05:51:38 +02:00
* TODO : get rid of this ?
2008-11-23 02:20:39 +01:00
* @ return boolean
*/
public function supportsCollations () {
2009-09-16 05:51:38 +02:00
return true ;
2008-11-23 02:20:39 +01:00
}
2011-01-11 22:17:17 +01:00
2012-07-06 11:35:17 +02:00
public function supportsTimezoneOverride () {
return true ;
}
2008-11-23 02:20:39 +01:00
/**
2009-11-02 02:58:51 +01:00
* Get the version of PostgreSQL .
2010-10-14 00:30:52 +02:00
* @ return string
2008-11-23 02:20:39 +01:00
*/
public function getVersion () {
2010-10-14 00:30:52 +02:00
$version = pg_version ( $this -> dbConn );
if ( isset ( $version [ 'server' ])) return $version [ 'server' ];
else return false ;
2008-11-23 02:20:39 +01:00
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
2009-09-16 05:51:38 +02:00
* Get the database server , namely PostgreSQL .
2008-11-23 02:20:39 +01:00
* @ return string
*/
public function getDatabaseServer () {
return " postgresql " ;
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
public function query ( $sql , $errorLevel = E_USER_ERROR ) {
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
if ( isset ( $_REQUEST [ 'previewwrite' ]) && in_array ( strtolower ( substr ( $sql , 0 , strpos ( $sql , ' ' ))), array ( 'insert' , 'update' , 'delete' , 'replace' ))) {
Debug :: message ( " Will execute: $sql " );
return ;
}
2011-01-11 22:17:17 +01:00
if ( isset ( $_REQUEST [ 'showqueries' ])) {
2008-11-23 02:20:39 +01:00
$starttime = microtime ( true );
}
2009-09-17 02:10:30 +02:00
2008-11-23 02:20:39 +01:00
$handle = pg_query ( $this -> dbConn , $sql );
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
if ( isset ( $_REQUEST [ 'showqueries' ])) {
2011-11-03 08:55:02 +01:00
$endtime = round (( microtime ( true ) - $starttime ) * 1000 , 1 );
2008-11-23 02:20:39 +01:00
Debug :: message ( " \n $sql\n { $endtime } ms \n " , false );
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
DB :: $lastQuery = $handle ;
2011-01-11 22:17:17 +01:00
2009-09-16 05:51:38 +02:00
if ( ! $handle && $errorLevel ) $this -> databaseError ( " Couldn't run query: $sql | " . pg_last_error ( $this -> dbConn ), $errorLevel );
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
return new PostgreSQLQuery ( $this , $handle );
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
public function getGeneratedID ( $table ) {
$result = DB :: query ( " SELECT last_value FROM \" { $table } _ID_seq \" ; " );
2011-01-11 22:17:17 +01:00
$row = $result -> first ();
2008-11-23 02:20:39 +01:00
return $row [ 'last_value' ];
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
* OBSOLETE : Get the ID for the next new record for the table .
2011-01-11 22:17:17 +01:00
*
2008-11-23 02:20:39 +01:00
* @ var string $table The name od the table .
* @ return int
*/
public function getNextID ( $table ) {
user_error ( 'getNextID is OBSOLETE (and will no longer work properly)' , E_USER_WARNING );
$result = $this -> query ( " SELECT MAX(ID)+1 FROM \" $table\ " " )->value();
return $result ? $result : 1 ;
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
public function isActive () {
return $this -> active ? true : false ;
}
2011-01-11 22:17:17 +01:00
2009-09-16 05:51:38 +02:00
/*
2010-03-08 22:58:28 +01:00
* You can create a database based either on a supplied name , or from whatever is in the $this -> database value
2009-09-16 05:51:38 +02:00
*/
2010-03-08 22:58:28 +01:00
public function createDatabase ( $name = false ) {
if ( ! $name )
$name = $this -> database ;
2011-01-11 22:17:17 +01:00
2010-03-08 22:58:28 +01:00
$this -> query ( " CREATE DATABASE \" $name\ " ; " );
2011-01-11 22:17:17 +01:00
2009-09-16 05:51:38 +02:00
$this -> connectDatabase ();
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
}
/**
* Drop the database that this object is currently connected to .
* Use with caution .
*/
public function dropDatabase () {
2011-01-11 22:17:17 +01:00
2009-09-16 05:51:38 +02:00
//First, we need to switch back to the original database so we can drop the current one
$db_to_drop = $this -> database ;
$this -> selectDatabase ( $this -> database_original );
$this -> connectDatabase ();
2011-01-11 22:17:17 +01:00
$this -> query ( " DROP DATABASE \" $db_to_drop\ " " );
2008-11-23 02:20:39 +01:00
}
2011-01-11 22:17:17 +01:00
2010-04-30 06:26:53 +02:00
/**
* Drop the database that this object is currently connected to .
* Use with caution .
*/
public function dropDatabaseByName ( $dbName ) {
if ( $dbName != $this -> database )
$this -> query ( " DROP DATABASE \" $dbName\ " ; " );
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
* Returns the name of the currently selected database
*/
public function currentDatabase () {
return $this -> database ;
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
* Switches to the given database .
* If the database doesn ' t exist , you should call createDatabase () after calling selectDatabase ()
*/
public function selectDatabase ( $dbname ) {
2010-02-12 04:28:04 +01:00
$this -> database = $dbname ;
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
$this -> tableList = $this -> fieldList = $this -> indexList = null ;
2011-01-11 22:17:17 +01:00
2009-09-16 05:51:38 +02:00
return true ;
2008-11-23 02:20:39 +01:00
}
2010-02-12 04:28:04 +01:00
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
* Returns true if the named database exists .
*/
public function databaseExists ( $name ) {
2013-12-20 02:34:31 +01:00
$SQL_name = $this -> addslashes ( $name );
2010-02-12 04:28:04 +01:00
return $this -> query ( " SELECT datname FROM pg_database WHERE datname=' $SQL_name '; " ) -> first () ? true : false ;
2008-11-23 02:20:39 +01:00
}
2011-01-11 22:17:17 +01:00
2010-04-30 06:26:53 +02:00
/**
2011-01-11 22:17:17 +01:00
* Returns a column
2010-04-30 06:26:53 +02:00
*/
public function allDatabaseNames () {
return $this -> query ( " SELECT datname FROM pg_database WHERE datistemplate=false; " ) -> column ();
}
2011-01-11 22:17:17 +01:00
/**
* Returns true if the schema exists in the current database
* @ param string $name
* @ return boolean
*/
public function schemaExists ( $name ) {
$SQL_name = pg_escape_string ( $this -> dbConn , $name );
return $this -> query ( " SELECT nspname FROM pg_catalog.pg_namespace WHERE nspname = ' { $SQL_name } '; " ) -> first () ? true : false ;
}
2012-09-17 06:15:00 +02:00
/**
* Creates a schema in the current database
* @ param string $name
*/
public function createSchema ( $name ) {
$SQL_name = pg_escape_string ( $this -> dbConn , $name );
$this -> query ( " CREATE SCHEMA \" { $SQL_name } \" ; " );
}
2011-01-11 22:17:17 +01:00
2012-09-17 06:15:00 +02:00
/**
* Drops a schema from the database . Use carefully !
* @ param string $name
*/
public function dropSchema ( $name ) {
$SQL_name = pg_escape_string ( $this -> dbConn , $name );
$this -> query ( " DROP SCHEMA \" { $SQL_name } \" CASCADE; " );
}
2011-01-11 22:17:17 +01:00
2012-09-17 06:15:00 +02:00
/**
* Returns the name of the current schema in use
*/
public function currentSchema () {
return $this -> query ( 'SELECT current_schema()' ) -> value ();
}
2011-01-11 22:17:17 +01:00
/**
* Utility method to manually set the schema to an alternative
* Check existance & sets search path to the supplied schema name
* @ param string $schema
*/
public function setSchema ( $schema ) {
if ( ! $this -> schemaExists ( $schema ))
$this -> databaseError ( " Schema $schema does not exist " );
$this -> setSchemaSearchPath ( $schema );
$this -> schema = $schema ;
}
/**
* 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 )
$this -> databaseError ( 'At least one Schema must be supplied to set a search path.' );
$args = array_values ( func_get_args ());
foreach ( $args as $key => $schema )
$args [ $key ] = '"' . pg_escape_string ( $this -> dbConn , $schema ) . '"' ;
$args_SQL = implode ( " , " , $args );
$this -> query ( " SET search_path TO { $args_SQL } " );
2012-09-17 06:15:00 +02:00
}
2011-01-11 22:17:17 +01:00
2009-10-08 03:19:15 +02:00
public function createTable ( $tableName , $fields = null , $indexes = null , $options = null , $extensions = null ) {
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
$fieldSchemas = $indexSchemas = " " ;
if ( $fields ) foreach ( $fields as $k => $v ) $fieldSchemas .= " \" $k\ " $v , \n " ;
2009-09-16 05:51:38 +02:00
if ( isset ( $this -> class )){
$addOptions = ( isset ( $options [ $this -> class ])) ? $options [ $this -> class ] : null ;
} else $addOptions = null ;
2011-01-11 22:17:17 +01:00
2009-09-16 05:51:38 +02:00
//First of all, does this table already exist
$doesExist = $this -> TableExists ( $tableName );
2010-04-29 05:56:53 +02:00
if ( $doesExist ) {
// Table already exists, just return the name, in line with baseclass documentation.
return $tableName ;
}
2011-01-11 22:17:17 +01:00
2009-02-13 03:46:54 +01:00
//If we have a fulltext search request, then we need to create a special column
//for GiST searches
2008-11-23 02:20:39 +01:00
$fulltexts = '' ;
2009-09-16 05:51:38 +02:00
$triggers = '' ;
2009-09-29 22:59:45 +02:00
if ( $indexes ){
foreach ( $indexes as $name => $this_index ){
2012-06-12 03:55:49 +02:00
if ( is_array ( $this_index ) && $this_index [ 'type' ] == 'fulltext' ){
2009-11-24 23:46:55 +01:00
$ts_details = $this -> fulltext ( $this_index , $tableName , $name );
2010-11-11 20:13:52 +01:00
$fulltexts .= $ts_details [ 'fulltexts' ] . ', ' ;
2009-11-24 23:46:55 +01:00
$triggers .= $ts_details [ 'triggers' ];
2009-09-29 22:59:45 +02:00
}
2008-11-23 02:20:39 +01:00
}
}
2012-08-21 06:22:17 +02:00
2008-11-23 02:20:39 +01:00
if ( $indexes ) foreach ( $indexes as $k => $v ) $indexSchemas .= $this -> getIndexSqlDefinition ( $tableName , $k , $v ) . " \n " ;
2011-01-11 22:17:17 +01:00
2009-10-08 03:19:15 +02:00
//Do we need to create a tablespace for this item?
if ( $extensions && isset ( $extensions [ 'tablespace' ])){
2011-01-11 22:17:17 +01:00
2009-10-08 03:19:15 +02:00
$this -> createOrReplaceTablespace ( $extensions [ 'tablespace' ][ 'name' ], $extensions [ 'tablespace' ][ 'location' ]);
$tableSpace = ' TABLESPACE ' . $extensions [ 'tablespace' ][ 'name' ];
2011-01-11 22:17:17 +01:00
} else
2009-10-08 03:19:15 +02:00
$tableSpace = '' ;
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
$this -> query ( " CREATE TABLE \" $tableName\ " (
$fieldSchemas
$fulltexts
primary key ( \ " ID \" )
2009-10-08 03:19:15 +02:00
) $tableSpace ; $indexSchemas $addOptions " );
2011-01-11 22:17:17 +01:00
2009-09-16 05:51:38 +02:00
if ( $triggers != '' ){
$this -> query ( $triggers );
}
2011-01-11 22:17:17 +01:00
2009-10-08 03:19:15 +02:00
//If we have a partitioning requirement, we do that here:
2009-11-24 23:46:55 +01:00
if ( $extensions && isset ( $extensions [ 'partitions' ])){
$this -> createOrReplacePartition ( $tableName , $extensions [ 'partitions' ], $indexes , $extensions );
2009-10-08 03:19:15 +02:00
}
2011-01-11 22:17:17 +01:00
2009-10-08 03:19:15 +02:00
//Lastly, clustering goes here:
if ( $extensions && isset ( $extensions [ 'cluster' ])){
DB :: query ( " CLUSTER \" $tableName\ " USING \ " { $extensions [ 'cluster' ] } \" ; " );
}
2011-01-11 22:17:17 +01:00
2009-09-29 22:59:45 +02:00
return $tableName ;
2008-11-23 02:20:39 +01:00
}
2012-09-17 06:15:00 +02:00
/**
* 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
*/
function buildPostgresIndexName ( $tableName , $indexName , $prefix = 'ix' ) {
// Assume all indexes also contain the table name
2012-09-18 02:41:34 +02:00
// 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
2012-09-24 02:46:23 +02:00
$indexNamePG = " { $prefix } _ " . md5 ( " { $tableName } _ { $indexName } " );
2012-09-17 06:15:00 +02:00
// Limit to 63 characters
if ( strlen ( $indexNamePG ) > 63 )
return substr ( $indexNamePG , 0 , 63 );
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' );
}
2008-11-23 02:20:39 +01:00
/**
* Alter a table ' s schema .
* @ param $table The name of the table to alter
* @ param $newFields New fields , a map of field name => field schema
* @ param $newIndexes New indexes , a map of index name => index type
* @ param $alteredFields Updated fields , a map of field name => field schema
* @ param $alteredIndexes Updated indexes , a map of index name => index type
*/
2009-10-08 03:19:15 +02:00
public function alterTable ( $tableName , $newFields = null , $newIndexes = null , $alteredFields = null , $alteredIndexes = null , $alteredOptions = null , $advancedOptions = null ) {
2011-01-11 22:17:17 +01:00
2008-11-24 00:29:09 +01:00
$alterList = array ();
2012-09-17 01:13:41 +02:00
if ( $newFields ) foreach ( $newFields as $fieldName => $fieldSpec ) {
$alterList [] = " ADD \" $fieldName\ " $fieldSpec " ;
}
2011-01-11 22:17:17 +01:00
2012-09-17 06:15:00 +02:00
if ( $alteredFields ) foreach ( $alteredFields as $indexName => $indexSpec ) {
$val = $this -> alterTableAlterColumn ( $tableName , $indexName , $indexSpec );
if ( ! empty ( $val )) $alterList [] = $val ;
2009-02-13 03:46:54 +01:00
}
2011-01-11 22:17:17 +01:00
2009-10-08 03:19:15 +02:00
//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 \" $tableName\ " SET TABLESPACE { $advancedOptions [ 'tablespace' ][ 'name' ]}; " );
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
//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
2009-03-04 22:55:40 +01:00
$alterIndexList = Array ();
2010-11-11 20:13:52 +01:00
//Pick up the altered indexes here:
$fieldList = $this -> fieldList ( $tableName );
$fulltexts = false ;
$drop_triggers = false ;
$triggers = false ;
2012-09-17 01:13:41 +02:00
if ( $alteredIndexes ) foreach ( $alteredIndexes as $indexName => $indexSpec ) {
$indexSpec = $this -> parseIndexSpec ( $indexName , $indexSpec );
2012-09-17 06:15:00 +02:00
$indexNamePG = $this -> buildPostgresIndexName ( $tableName , $indexName );
2011-01-11 22:17:17 +01:00
2012-09-17 01:13:41 +02:00
if ( $indexSpec [ 'type' ] == 'fulltext' ) {
2010-11-11 20:13:52 +01:00
//For full text indexes, we need to drop the trigger, drop the index, AND drop the column
2011-01-11 22:17:17 +01:00
2010-11-11 20:13:52 +01:00
//Go and get the tsearch details:
2012-09-17 01:13:41 +02:00
$ts_details = $this -> fulltext ( $indexSpec , $tableName , $indexName );
2011-01-11 22:17:17 +01:00
2010-11-11 20:13:52 +01:00
//Drop this column if it already exists:
2011-01-11 22:17:17 +01:00
2010-11-11 20:13:52 +01:00
//No IF EXISTS option is available for Postgres <9.0
if ( array_key_exists ( $ts_details [ 'ts_name' ], $fieldList )){
$fulltexts .= " ALTER TABLE \" { $tableName } \" DROP COLUMN \" { $ts_details [ 'ts_name' ] } \" ; " ;
}
2011-01-11 22:17:17 +01:00
2012-09-17 01:13:41 +02:00
// We'll execute these later:
2012-09-17 06:15:00 +02:00
$triggerNamePG = $this -> buildPostgresTriggerName ( $tableName , $indexName );
$drop_triggers .= " DROP TRIGGER IF EXISTS \" $triggerNamePG\ " ON \ " $tableName\ " ; " ;
$fulltexts .= " ALTER TABLE \" { $tableName } \" ADD COLUMN { $ts_details [ 'fulltexts' ] } ; " ;
2012-09-17 01:13:41 +02:00
$triggers .= $ts_details [ 'triggers' ];
2009-09-16 05:51:38 +02:00
}
2012-09-17 01:13:41 +02:00
// Create index action (including fulltext)
2012-09-17 06:15:00 +02:00
$alterIndexList [] = " DROP INDEX IF EXISTS \" $indexNamePG\ " ; " ;
2012-09-17 01:13:41 +02:00
$createIndex = $this -> getIndexSqlDefinition ( $tableName , $indexName , $indexSpec );
if ( $createIndex !== false ) $alterIndexList [] = $createIndex ;
2012-09-17 06:15:00 +02:00
}
2008-11-24 00:29:09 +01:00
2012-09-17 01:13:41 +02:00
//Add the new indexes:
if ( $newIndexes ) foreach ( $newIndexes as $indexName => $indexSpec ){
$indexSpec = $this -> parseIndexSpec ( $indexName , $indexSpec );
2012-09-17 06:15:00 +02:00
$indexNamePG = $this -> buildPostgresIndexName ( $tableName , $indexName );
2012-09-17 01:13:41 +02:00
//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 , $tableName , $indexName );
if ( ! isset ( $fieldList [ $ts_details [ 'ts_name' ]])){
$fulltexts .= " ALTER TABLE \" { $tableName } \" ADD COLUMN { $ts_details [ 'fulltexts' ] } ; " ;
$triggers .= $ts_details [ 'triggers' ];
2010-11-11 20:13:52 +01:00
}
}
2012-09-17 01:13:41 +02:00
2012-09-17 06:15:00 +02:00
//Check that this index doesn't already exist:
2009-09-16 05:51:38 +02:00
$indexes = $this -> indexList ( $tableName );
2012-09-17 01:13:41 +02:00
if ( isset ( $indexes [ $indexName ])){
2012-09-17 06:15:00 +02:00
$alterIndexList [] = " DROP INDEX IF EXISTS \" $indexNamePG\ " ; " ;
2009-09-16 05:51:38 +02:00
}
2011-01-11 22:17:17 +01:00
2012-09-17 01:13:41 +02:00
$createIndex = $this -> getIndexSqlDefinition ( $tableName , $indexName , $indexSpec );
2010-03-18 04:16:30 +01:00
if ( $createIndex !== false )
$alterIndexList [] = $createIndex ;
2009-04-06 01:20:09 +02:00
}
2011-01-11 22:17:17 +01:00
2009-02-13 03:46:54 +01:00
if ( $alterList ) {
2008-11-24 00:29:09 +01:00
$alterations = implode ( " , \n " , $alterList );
$this -> query ( " ALTER TABLE \" $tableName\ " " . $alterations );
}
2011-01-11 22:17:17 +01:00
2009-10-08 03:19:15 +02:00
//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' ]);
}
2011-01-11 22:17:17 +01:00
2009-10-08 03:19:15 +02:00
if ( $alteredOptions && isset ( $this -> class ) && isset ( $alteredOptions [ $this -> class ])) {
2009-05-19 23:47:48 +02:00
$this -> query ( sprintf ( " ALTER TABLE \" %s \" %s " , $tableName , $alteredOptions [ $this -> class ]));
Database :: alteration_message (
sprintf ( " Table %s options changed: %s " , $tableName , $alteredOptions [ $this -> class ]),
" changed "
);
}
2011-01-11 22:17:17 +01:00
2010-11-11 20:13:52 +01:00
//Create any fulltext columns and triggers here:
2012-09-17 01:13:41 +02:00
if ( $fulltexts ) $this -> query ( $fulltexts );
if ( $drop_triggers ) $this -> query ( $drop_triggers );
2011-01-11 22:17:17 +01:00
2010-11-11 20:13:52 +01:00
if ( $triggers ) {
$this -> query ( $triggers );
$triggerbits = explode ( ';' , $triggers );
foreach ( $triggerbits as $trigger ){
$trigger_fields = $this -> triggerFieldsFromTrigger ( $trigger );
2011-01-11 22:17:17 +01:00
2010-11-11 20:13:52 +01:00
if ( $trigger_fields ){
//We need to run a simple query to force the database to update the triggered columns
$this -> query ( " UPDATE \" { $tableName } \" SET \" { $trigger_fields [ 0 ] } \" = \" $trigger_fields[0] \" ; " );
}
}
}
2011-01-11 22:17:17 +01:00
2012-09-17 01:13:41 +02:00
foreach ( $alterIndexList as $alteration ) $this -> query ( $alteration );
2011-01-11 22:17:17 +01:00
2009-10-08 03:19:15 +02:00
//If we have a partitioning requirement, we do that here:
if ( $advancedOptions && isset ( $advancedOptions [ 'partitions' ])){
$this -> createOrReplacePartition ( $tableName , $advancedOptions [ 'partitions' ]);
}
//Lastly, clustering goes here:
2012-09-17 06:15:00 +02:00
if ( $advancedOptions && isset ( $advancedOptions [ 'cluster' ])) {
$clusterIndex = $this -> buildPostgresIndexName ( $tableName , $advancedOptions [ 'cluster' ]);
DB :: query ( " CLUSTER \" $tableName\ " USING \ " $clusterIndex\ " ; " );
2009-11-02 02:58:51 +01:00
} else {
//Check that clustering is not on this table, and if it is, remove it:
2011-01-11 22:17:17 +01:00
2009-11-02 02:58:51 +01:00
//This is really annoying. We need the oid of this table:
2012-08-21 06:21:45 +02:00
$stats = DB :: query ( " SELECT relid FROM pg_stat_user_tables WHERE relname=' " . $this -> addslashes ( $tableName ) . " '; " ) -> first ();
2009-11-02 02:58:51 +01:00
$oid = $stats [ 'relid' ];
2011-01-11 22:17:17 +01:00
2009-11-02 02:58:51 +01:00
//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 = DB :: query ( " SELECT c2.relname, i.indisclustered FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i WHERE c.oid = ' $oid ' AND c.oid = i.indrelid AND i.indexrelid = c2.oid AND indisclustered='t'; " ) -> first ();
2011-01-11 22:17:17 +01:00
2009-11-02 02:58:51 +01:00
if ( $clustered )
DB :: query ( " ALTER TABLE \" $tableName\ " SET WITHOUT CLUSTER ; " );
2011-01-11 22:17:17 +01:00
2009-11-02 02:58:51 +01:00
}
2008-11-23 02:20:39 +01:00
}
2011-01-11 22:17:17 +01:00
2009-02-13 03:46:54 +01:00
/*
* Creates an ALTER expression for a column in PostgreSQL
2011-01-11 22:17:17 +01:00
*
2009-02-13 03:46:54 +01:00
* @ 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?
2011-01-11 22:17:17 +01:00
2009-02-13 03:46:54 +01:00
$pattern = '/^([\w()]+)\s?((?:not\s)?null)?\s?(default\s[\w\']+)?\s?(check\s[\w()\'",\s]+)?$/i' ;
preg_match ( $pattern , $colSpec , $matches );
2011-01-11 22:17:17 +01:00
2012-09-17 01:13:41 +02:00
if ( sizeof ( $matches ) == 0 ) return '' ;
2011-01-11 22:17:17 +01:00
2012-09-17 01:13:41 +02:00
if ( $matches [ 1 ] == 'serial8' ) return '' ;
2011-01-11 22:17:17 +01:00
2009-02-13 03:46:54 +01:00
if ( isset ( $matches [ 1 ])) {
$alterCol = " ALTER COLUMN \" $colName\ " TYPE $matches [ 1 ] \n " ;
2011-01-11 22:17:17 +01:00
2009-02-13 03:46:54 +01:00
// SET null / not null
2012-09-17 01:13:41 +02:00
if ( ! empty ( $matches [ 2 ])) {
$alterCol .= " , \n ALTER COLUMN \" $colName\ " SET $matches [ 2 ] " ;
}
2011-01-11 22:17:17 +01:00
2009-02-13 03:46:54 +01:00
// SET default (we drop it first, for reasons of precaution)
if ( ! empty ( $matches [ 3 ])) {
$alterCol .= " , \n ALTER COLUMN \" $colName\ " DROP DEFAULT " ;
$alterCol .= " , \n ALTER COLUMN \" $colName\ " SET $matches [ 3 ] " ;
}
2011-01-11 22:17:17 +01:00
2009-02-13 03:46:54 +01:00
// SET check constraint (The constraint HAS to be dropped)
2010-11-01 01:53:57 +01:00
$existing_constraint = $this -> query ( " SELECT conname FROM pg_constraint WHERE conname=' { $tableName } _ { $colName } _check'; " ) -> value ();
2010-12-05 05:59:43 +01:00
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 ); " ;
2012-09-17 01:13:41 +02:00
if ( $this -> hasTable ( " { $tableName } _Live " )) {
2010-12-05 05:59:43 +01:00
$updateConstraint .= " UPDATE \" { $tableName } _Live \" SET \" $colName\ " = '$default' WHERE \ " $colName\ " NOT IN ( $constraint_values ); " ;
2012-09-17 01:13:41 +02:00
}
if ( $this -> hasTable ( " { $tableName } _versions " )) {
2010-12-05 05:59:43 +01:00
$updateConstraint .= " UPDATE \" { $tableName } _versions \" SET \" $colName\ " = '$default' WHERE \ " $colName\ " NOT IN ( $constraint_values ); " ;
2012-09-17 01:13:41 +02:00
}
2010-12-05 05:59:43 +01:00
DB :: query ( $updateConstraint );
}
2011-01-12 01:10:38 +01:00
2010-11-01 01:53:57 +01:00
//First, delete any existing constraint on this column, even if it's no longer an enum
if ( $existing_constraint )
$alterCol .= " , \n DROP CONSTRAINT \" { $tableName } _ { $colName } _check \" " ;
2011-01-11 22:17:17 +01:00
2010-11-01 01:53:57 +01:00
//Now create the constraint (if we've asked for one)
if ( ! empty ( $matches [ 4 ]))
2010-02-17 21:19:22 +01:00
$alterCol .= " , \n ADD CONSTRAINT \" { $tableName } _ { $colName } _check \" $matches[4] " ;
2009-02-13 03:46:54 +01:00
}
2011-01-11 22:17:17 +01:00
2009-02-13 03:46:54 +01:00
return isset ( $alterCol ) ? $alterCol : '' ;
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
public function renameTable ( $oldTableName , $newTableName ) {
2010-03-22 00:02:54 +01:00
$this -> query ( " ALTER TABLE \" $oldTableName\ " RENAME TO \ " $newTableName\ " " );
2011-01-12 01:10:38 +01:00
unset ( self :: $cached_fieldlists [ $oldTableName ]);
2008-11-23 02:20:39 +01:00
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
2009-09-29 22:59:45 +02:00
* Repairs and reindexes the table . This might take a long time on a very large table .
2008-11-23 02:20:39 +01:00
* @ var string $tableName The name of the table .
* @ return boolean Return true if the table has integrity after the method is complete .
*/
public function checkAndRepairTable ( $tableName ) {
2011-01-12 01:10:38 +01:00
2009-09-29 22:59:45 +02:00
$this -> runTableCheckCommand ( " VACUUM FULL ANALYZE \" $tableName\ " " );
2009-09-30 00:19:48 +02:00
$this -> runTableCheckCommand ( " REINDEX TABLE \" $tableName\ " " );
2008-11-23 02:20:39 +01:00
return true ;
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
* Helper function used by checkAndRepairTable .
* @ param string $sql Query to run .
2009-09-29 22:59:45 +02:00
* @ return boolean Returns true no matter what ; we ' re not currently checking the status of the command
2008-11-23 02:20:39 +01:00
*/
protected function runTableCheckCommand ( $sql ) {
$testResults = $this -> query ( $sql );
return true ;
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
public function createField ( $tableName , $fieldName , $fieldSpec ) {
$this -> query ( " ALTER TABLE \" $tableName\ " ADD \ " $fieldName\ " $fieldSpec " );
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
* 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 " );
}
2008-11-24 00:29:09 +01:00
/**
* Change the database column name of the given field .
2011-01-11 22:17:17 +01:00
*
2011-01-12 01:10:38 +01:00
* @ param string $tableName The name of the table the field is in .
2008-11-24 00:29:09 +01:00
* @ param string $oldName The name of the field to change .
* @ param string $newName The new name of the field
*/
public function renameField ( $tableName , $oldName , $newName ) {
$fieldList = $this -> fieldList ( $tableName );
if ( array_key_exists ( $oldName , $fieldList )) {
2009-02-13 03:46:54 +01:00
$this -> query ( " ALTER TABLE \" $tableName\ " RENAME COLUMN \ " $oldName\ " TO \ " $newName\ " " );
2011-01-12 01:10:38 +01:00
//Remove this from the cached list:
unset ( self :: $cached_fieldlists [ $tableName ]);
2008-11-24 00:29:09 +01:00
}
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
public function fieldList ( $table ) {
2009-02-17 04:57:21 +01:00
//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....
2011-01-11 22:17:17 +01:00
2011-01-18 05:31:47 +01:00
//if(!isset(self::$cached_fieldlists[$table])){
2012-08-21 06:21:45 +02:00
$fields = $this -> query ( " 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 = ' " . $this -> addslashes ( $table ) . " ' ORDER BY ordinal_position; " );
2011-01-11 22:17:17 +01:00
$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)
//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 ; $i < strlen ( $value ); $i ++ ){
$char = substr ( $value , $i , 1 );
if ( $in_value )
$current_value .= $char ;
if ( $char == " ' " ){
if ( ! $in_value )
$in_value = true ;
else {
$in_value = false ;
$constraints [] = substr ( $current_value , 0 , - 1 );
$current_value = '' ;
}
2009-03-03 22:46:27 +01:00
}
}
2011-01-11 22:17:17 +01:00
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' ] . ')' ;
2009-03-03 22:46:27 +01:00
}
2011-01-11 22:17:17 +01:00
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 'timestamp without time zone' :
$output [ $field [ 'column_name' ]] = 'timestamp' ;
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 'double precision' :
$output [ $field [ 'column_name' ]] = 'float' ;
break ;
default :
$output [ $field [ 'column_name' ]] = $field ;
}
2009-02-17 04:57:21 +01:00
}
2011-01-11 22:17:17 +01:00
2011-01-18 05:31:47 +01:00
// self::$cached_fieldlists[$table]=$output;
//}
2011-01-12 01:10:38 +01:00
2011-01-18 05:31:47 +01:00
//return self::$cached_fieldlists[$table];
2012-08-21 06:21:45 +02:00
2011-01-18 05:31:47 +01:00
return $output ;
2008-11-23 02:20:39 +01:00
}
2011-01-11 22:17:17 +01:00
2011-01-12 01:10:38 +01:00
/**
*
* This allows the cached values for a table ' s field list to be erased .
* If $tablename is empty , then the whole cache is erased .
*
* @ param string $tableName
*
* @ return boolean
*/
2011-03-11 02:25:10 +01:00
function clearCachedFieldlist ( $tableName = false ){
2012-09-17 01:13:41 +02:00
if ( $tableName ) unset ( self :: $cached_fieldlists [ $tableName ]);
else self :: $cached_fieldlists = array ();
2011-01-12 01:10:38 +01:00
return true ;
}
2008-11-23 02:20:39 +01:00
/**
* 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 ) {
2010-03-18 04:16:30 +01:00
$createIndex = $this -> getIndexSqlDefinition ( $tableName , $indexName , $indexSpec );
2012-09-17 01:13:41 +02:00
if ( $createIndex !== false ) $this -> query ();
2008-11-23 02:20:39 +01:00
}
2011-01-11 22:17:17 +01:00
2009-01-09 05:01:37 +01:00
/*
* This takes the index spec which has been provided by a class ( ie static $indexes = blah blah )
* and turns it into a proper string .
* Some indexes may be arrays , such as fulltext and unique indexes , and this allows database - specific
* arrays to be created .
2012-09-17 01:13:41 +02:00
* @ see parseIndexSpec () for approximate inverse
2009-01-09 05:01:37 +01:00
*/
2009-03-04 22:55:40 +01:00
public function convertIndexSpec ( $indexSpec , $asDbValue = false , $table = '' ){
2011-01-11 22:17:17 +01:00
2009-03-04 22:55:40 +01:00
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' :
2012-09-17 01:13:41 +02:00
$indexSpec = 'fulltext (' . $indexSpec [ 'value' ] . ')' ;
2009-03-04 22:55:40 +01:00
break ;
case 'unique' :
$indexSpec = 'unique (' . $indexSpec [ 'value' ] . ')' ;
break ;
case 'hash' :
2010-11-19 00:22:07 +01:00
$indexSpec = 'using hash (' . $indexSpec [ 'value' ] . ')' ;
2009-03-04 22:55:40 +01:00
break ;
2010-11-01 01:53:57 +01:00
case 'index' :
//The default index is 'btree', which we'll use by default (below):
default :
2010-11-19 00:22:07 +01:00
$indexSpec = 'using btree (' . $indexSpec [ 'value' ] . ')' ;
2010-11-01 01:53:57 +01:00
break ;
2009-03-04 22:55:40 +01:00
}
2009-01-09 05:01:37 +01:00
}
2009-03-04 22:55:40 +01:00
} else {
2012-09-18 02:41:34 +02:00
$indexSpec = $this -> buildPostgresIndexName ( $table , $indexSpec );
2009-01-09 05:01:37 +01:00
}
2009-02-17 04:57:21 +01:00
return $indexSpec ;
2009-01-09 05:01:37 +01:00
}
2012-09-17 01:13:41 +02:00
/**
* Splits a spec string safely , considering quoted columns , whitespace ,
* and cleaning brackets
* @ param string $spec The input index specification
* @ return array List of columns in the spec
*/
function explodeColumnString ( $spec ) {
// Remove any leading/trailing brackets and outlying modifiers
// E.g. 'unique (Title, "QuotedColumn");' => 'Title, "QuotedColumn"'
$containedSpec = preg_replace ( '/(.*\(\s*)|(\s*\).*)/' , '' , $spec );
// Split potentially quoted modifiers
// E.g. 'Title, "QuotedColumn"' => array('Title', 'QuotedColumn')
return preg_split ( '/"?\s*,\s*"?/' , trim ( $containedSpec , '(") ' ));
}
/**
* Builds a properly quoted column list from an array
* @ param array $columns List of columns to implode
* @ return string A properly quoted list of column names
*/
function implodeColumnList ( $columns ) {
if ( empty ( $columns )) return '' ;
return '"' . implode ( '","' , $columns ) . '"' ;
}
/**
* Given an index specification in the form of a string ensure that each
2012-09-17 06:15:00 +02:00
* column name is property quoted , stripping brackets and modifiers .
* This index may also be in the form of a " CREATE INDEX... " sql fragment
* @ param string $spec The input specification or query . E . g . 'unique (Column1, Column2)'
* @ return string The properly quoted column list . E . g . '"Column1", "Column2"'
2012-09-17 01:13:41 +02:00
*/
function quoteColumnSpecString ( $spec ) {
$bits = $this -> explodeColumnString ( $spec );
return $this -> implodeColumnList ( $bits );
}
/**
* Given an index spec determines the index type
* @ param type $spec
* @ return string
*/
function determineIndexType ( $spec ) {
// check array spec
if ( is_array ( $spec ) && isset ( $spec [ 'type' ])) {
return $spec [ 'type' ];
} elseif ( ! is_array ( $spec ) && preg_match ( '/(?<type>\w+)\s*\(/' , $spec , $matchType )) {
return strtolower ( $matchType [ 'type' ]);
} else {
return 'index' ;
}
}
/**
* Converts an array or string index spec into a universally useful array
* @ see convertIndexSpec () for approximate inverse
* @ param string | array $spec
* @ return array The resulting spec array with the required fields name , type , and value
*/
function parseIndexSpec ( $name , $spec ){
// Do minimal cleanup on any already parsed spec
if ( is_array ( $spec )) {
$spec [ 'value' ] = $this -> quoteColumnSpecString ( $spec [ 'value' ]);
return $spec ;
}
// Nicely formatted spec!
return array (
'name' => $name ,
'value' => $this -> quoteColumnSpecString ( $spec ),
'type' => $this -> determineIndexType ( $spec )
);
}
2011-01-11 22:17:17 +01:00
2009-03-04 22:55:40 +01:00
protected function getIndexSqlDefinition ( $tableName , $indexName , $indexSpec , $asDbValue = false ) {
2011-01-11 22:17:17 +01:00
2009-09-17 02:10:30 +02:00
//TODO: create table partition support
2009-09-16 05:51:38 +02:00
//TODO: create clustering options
2011-01-11 22:17:17 +01:00
2010-03-18 04:16:30 +01:00
//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.
2012-09-17 01:13:41 +02:00
// If requesting the definition rather than the DDL
if ( $asDbValue ) {
$indexName = trim ( $indexName , '()' );
return $indexName ;
}
2011-01-11 22:17:17 +01:00
2012-09-17 06:15:00 +02:00
// Determine index name
$tableCol = $this -> buildPostgresIndexName ( $tableName , $indexName );
2011-01-11 22:17:17 +01:00
2012-09-17 01:13:41 +02:00
// Consolidate/Cleanup spec into array format
$indexSpec = $this -> parseIndexSpec ( $indexName , $indexSpec );
2012-09-17 06:15:00 +02:00
2012-09-17 01:13:41 +02:00
//Misc options first:
2012-09-17 06:15:00 +02:00
$fillfactor = $where = '' ;
if ( isset ( $indexSpec [ 'fillfactor' ])) {
$fillfactor = 'WITH (FILLFACTOR = ' . $indexSpec [ 'fillfactor' ] . ')' ;
2012-09-17 01:13:41 +02:00
}
2012-09-17 06:15:00 +02:00
if ( isset ( $indexSpec [ 'where' ])) {
$where = 'WHERE ' . $indexSpec [ 'where' ];
2012-09-17 01:13:41 +02:00
}
2011-01-11 22:17:17 +01:00
2012-09-17 01:13:41 +02:00
//create a type-specific index
2012-09-17 06:15:00 +02:00
// 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' ]) {
2012-09-17 01:13:41 +02:00
case 'fulltext' :
// @see fulltext() for the definition of the trigger that ts_$IndexName uses for fulltext searching
2012-09-17 06:15:00 +02:00
$spec = " create index \" $tableCol\ " ON \ " $tableName\ " USING " . $this->default_fts_cluster_method . " ( \ " ts_ " . $indexName . " \" ) $fillfactor $where " ;
2012-09-17 01:13:41 +02:00
break ;
case 'unique' :
2012-09-17 06:15:00 +02:00
$spec = " create unique index \" $tableCol\ " ON \ " $tableName\ " ( " . $indexSpec['value'] . " ) $fillfactor $where " ;
2012-09-17 01:13:41 +02:00
break ;
case 'btree' :
2012-09-17 06:15:00 +02:00
$spec = " create index \" $tableCol\ " ON \ " $tableName\ " USING btree ( " . $indexSpec['value'] . " ) $fillfactor $where " ;
2012-09-17 01:13:41 +02:00
break ;
case 'hash' :
//NOTE: this is not a recommended index type
2012-09-17 06:15:00 +02:00
$spec = " create index \" $tableCol\ " ON \ " $tableName\ " USING hash ( " . $indexSpec['value'] . " ) $fillfactor $where " ;
2012-09-17 01:13:41 +02:00
break ;
case 'index' :
2012-09-17 06:15:00 +02:00
//'index' is the same as default, just a normal index with the default type decided by the database.
2012-09-17 01:13:41 +02:00
default :
2012-09-17 06:15:00 +02:00
$spec = " create index \" $tableCol\ " ON \ " $tableName\ " ( " . $indexSpec['value'] . " ) $fillfactor $where " ;
2008-11-23 02:20:39 +01:00
}
2012-09-17 01:13:41 +02:00
return trim ( $spec ) . ';' ;
2009-03-04 22:55:40 +01:00
}
2011-01-11 22:17:17 +01:00
2012-09-17 06:15:00 +02:00
function getDbSqlDefinition ( $tableName , $indexName , $indexSpec ) {
2009-03-04 22:55:40 +01:00
return $this -> getIndexSqlDefinition ( $tableName , $indexName , $indexSpec , true );
2008-11-23 02:20:39 +01:00
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
* Alter 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 alterIndex ( $tableName , $indexName , $indexSpec ) {
$indexSpec = trim ( $indexSpec );
if ( $indexSpec [ 0 ] != '(' ) {
list ( $indexType , $indexFields ) = explode ( ' ' , $indexSpec , 2 );
} else {
$indexFields = $indexSpec ;
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
if ( ! $indexType ) {
$indexType = " index " ;
}
2011-01-11 22:17:17 +01:00
2012-09-17 06:15:00 +02:00
$this -> query ( " DROP INDEX \" $indexName\ " " );
2008-11-23 02:20:39 +01:00
$this -> query ( " ALTER TABLE \" $tableName\ " ADD $indexType \ " $indexName\ " $indexFields " );
}
2012-09-17 06:15:00 +02:00
/**
* 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 = DB :: query ( $statement = sprintf (
" SELECT tgargs FROM pg_catalog.pg_trigger WHERE tgname='%s' " , $this -> addslashes ( $triggerName )
)) -> first ();
2012-09-19 07:27:09 +02:00
// Option 1: output as a string
if ( strpos ( $trigger [ 'tgargs' ], '\000' ) !== false ) {
$argList = explode ( '\000' , $trigger [ 'tgargs' ]);
array_pop ( $argList );
// 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 ));
}
}
}
2012-09-17 06:15:00 +02:00
// Drop first two arguments (trigger name and config name) and implode into nice list
2012-09-18 02:42:17 +02:00
return array_slice ( $argList , 2 );
2012-09-17 06:15:00 +02:00
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
* Return the list of indexes in a table .
* @ param string $table The table name .
* @ return array
*/
public function indexList ( $table ) {
2011-01-11 22:17:17 +01:00
2009-02-13 03:46:54 +01:00
//Retrieve a list of indexes for the specified table
2011-01-11 22:17:17 +01:00
$schema_SQL = pg_escape_string ( $this -> dbConn , $this -> schema );
2012-08-21 06:21:45 +02:00
$indexes = DB :: query ( " SELECT tablename, indexname, indexdef FROM pg_catalog.pg_indexes WHERE tablename=' " . $this -> addslashes ( $table ) . " ' AND schemaname = ' { $schema_SQL } '; " );
2011-01-11 22:17:17 +01:00
2009-09-16 05:51:38 +02:00
$indexList = Array ();
2009-03-04 22:55:40 +01:00
foreach ( $indexes as $index ) {
2012-09-18 02:41:34 +02:00
// Key for the indexList array. Differs from other DB implementations, which is why
// requireIndex() needed to be overridden
$indexName = $index [ 'indexname' ];
2012-09-17 06:15:00 +02:00
2009-03-04 22:55:40 +01:00
//We don't actually need the entire created command, just a few bits:
$prefix = '' ;
2011-01-11 22:17:17 +01:00
2009-03-04 22:55:40 +01:00
//Check for uniques:
2012-09-17 01:13:41 +02:00
if ( substr ( $index [ 'indexdef' ], 0 , 13 ) == 'CREATE UNIQUE' ) {
2009-03-04 22:55:40 +01:00
$prefix = 'unique ' ;
2012-09-17 01:13:41 +02:00
}
2011-01-11 22:17:17 +01:00
2009-03-04 22:55:40 +01:00
//check for hashes, btrees etc:
2012-09-17 01:13:41 +02:00
if ( strpos ( strtolower ( $index [ 'indexdef' ]), 'using hash ' ) !== false ) {
2009-03-04 22:55:40 +01:00
$prefix = 'using hash ' ;
2012-09-17 01:13:41 +02:00
}
2009-03-04 22:55:40 +01:00
//TODO: Fix me: btree is the default index type:
//if(strpos(strtolower($index['indexdef']), 'using btree ')!==false)
// $prefix='using btree ';
2011-01-11 22:17:17 +01:00
2012-09-17 01:13:41 +02:00
if ( strpos ( strtolower ( $index [ 'indexdef' ]), 'using rtree ' ) !== false ) {
2009-03-04 22:55:40 +01:00
$prefix = 'using rtree ' ;
2012-09-17 01:13:41 +02:00
}
2009-03-04 22:55:40 +01:00
2012-09-17 06:15:00 +02:00
// For fulltext indexes we need to extract the columns from another source
if ( stristr ( $index [ 'indexdef' ], 'using gin' )) {
$prefix = 'fulltext ' ;
// Extract trigger information from postgres
2012-09-18 02:41:34 +02:00
$triggerName = preg_replace ( '/^ix_/' , 'ts_' , $index [ 'indexname' ]);
2012-09-17 06:15:00 +02:00
$columns = $this -> extractTriggerColumns ( $triggerName );
$columnString = $this -> implodeColumnList ( $columns );
} else {
$columnString = $this -> quoteColumnSpecString ( $index [ 'indexdef' ]);
}
2011-01-11 22:17:17 +01:00
2012-09-17 06:15:00 +02:00
$indexList [ $indexName ][ 'indexname' ] = $index [ 'indexname' ];
$indexList [ $indexName ][ 'spec' ] = " $prefix ( $columnString ) " ;
}
2009-02-13 03:46:54 +01:00
2009-09-16 05:51:38 +02:00
return isset ( $indexList ) ? $indexList : null ;
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
}
2012-09-18 02:41:34 +02:00
/**
* Generate the given index in the database , modifying whatever already exists as necessary .
*
* The keys of the array are the names of the index .
* The values of the array can be one of :
* - true : Create a single column index on the field named the same as the index .
* - array ( 'type' => 'index|unique|fulltext' , 'value' => 'FieldA, FieldB' ) : This gives you full
* control over the index .
*
* @ param string $table The table name .
* @ param string $index The index name .
* @ param string | boolean $spec The specification of the index . See requireTable () for more information .
*/
function requireIndex ( $table , $index , $spec ) {
$newTable = false ;
//DB Abstraction: remove this ===true option as a possibility?
if ( $spec === true ) {
$spec = " ( \" $index\ " ) " ;
}
//Indexes specified as arrays cannot be checked with this line: (it flattens out the array)
if ( ! is_array ( $spec )) {
$spec = preg_replace ( '/\s*,\s*/' , ',' , $spec );
}
if ( ! isset ( $this -> tableList [ strtolower ( $table )])) $newTable = true ;
if ( ! $newTable && ! isset ( $this -> indexList [ $table ])) {
$this -> indexList [ $table ] = $this -> indexList ( $table );
}
//Fix up the index for database purposes
$index = DB :: getConn () -> getDbSqlDefinition ( $table , $index , null , true );
//Fix the key for database purposes
$index_alt = $this -> buildPostgresIndexName ( $table , $index );
if ( ! $newTable ) {
if ( isset ( $this -> indexList [ $table ][ $index_alt ])) {
if ( is_array ( $this -> indexList [ $table ][ $index_alt ])) {
$array_spec = $this -> indexList [ $table ][ $index_alt ][ 'spec' ];
} else {
$array_spec = $this -> indexList [ $table ][ $index_alt ];
}
}
}
if ( $newTable || ! isset ( $this -> indexList [ $table ][ $index_alt ])) {
$this -> transCreateIndex ( $table , $index , $spec );
$this -> alterationMessage ( " Index $table . $index : created as " . DB :: getConn () -> convertIndexSpec ( $spec ), " created " );
} else if ( $array_spec != DB :: getConn () -> convertIndexSpec ( $spec )) {
$this -> transAlterIndex ( $table , $index , $spec );
$spec_msg = DB :: getConn () -> convertIndexSpec ( $spec );
$this -> alterationMessage ( " Index $table . $index : changed to $spec_msg <i style= \" color: #AAA \" >(from { $array_spec } )</i> " , " changed " );
}
}
2008-11-23 02:20:39 +01:00
/**
* Returns a list of all the tables in the database .
* Table names will all be in lowercase .
* @ return array
*/
public function tableList () {
2011-01-11 22:17:17 +01:00
$schema_SQL = pg_escape_string ( $this -> dbConn , $this -> schema );
$tables = array ();
2012-12-11 15:09:10 +01:00
foreach ( $this -> query ( " SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = ' { $schema_SQL } ' AND tablename NOT ILIKE 'pg \\ \ _%' AND tablename NOT ILIKE 'sql \\ \ _%' " ) as $record ) {
2009-09-16 05:51:38 +02:00
//$table = strtolower(reset($record));
$table = reset ( $record );
2008-11-23 02:20:39 +01:00
$tables [ $table ] = $table ;
}
2011-01-11 22:17:17 +01:00
2009-09-16 05:51:38 +02:00
//Return an empty array if there's nothing in this database
2011-01-11 22:17:17 +01:00
return $tables ;
2009-09-16 05:51:38 +02:00
}
2011-01-11 22:17:17 +01:00
2012-09-17 01:13:41 +02:00
/**
* Determines if a table exists
* @ param string $tableName
* @ return boolean
*/
2009-09-16 05:51:38 +02:00
function TableExists ( $tableName ){
2011-01-11 22:17:17 +01:00
$schema_SQL = pg_escape_string ( $this -> dbConn , $this -> schema );
2012-08-21 06:21:45 +02:00
$result = $this -> query ( " SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = ' { $schema_SQL } ' AND tablename=' " . $this -> addslashes ( $tableName ) . " '; " ) -> first ();
2011-01-11 22:17:17 +01:00
2012-09-17 01:13:41 +02:00
return ! empty ( $result );
2008-11-23 02:20:39 +01:00
}
2011-01-11 22:17:17 +01:00
2010-11-01 02:11:31 +01:00
/**
* 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 .
2011-01-11 22:17:17 +01:00
*
2010-11-01 02:11:31 +01:00
* @ param string $constraint
*/
2009-10-08 03:19:15 +02:00
function constraintExists ( $constraint ){
2010-11-01 02:11:31 +01:00
if ( ! isset ( self :: $cached_constraints [ $constraint ])){
$exists = DB :: query ( " SELECT conname,pg_catalog.pg_get_constraintdef(r.oid, true) FROM pg_catalog.pg_constraint r WHERE r.contype = 'c' AND conname=' $constraint ' ORDER BY 1; " ) -> first ();
self :: $cached_constraints [ $constraint ] = $exists ;
}
2011-01-11 22:17:17 +01:00
2010-11-01 02:11:31 +01:00
return self :: $cached_constraints [ $constraint ];
2009-10-08 03:19:15 +02:00
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
* Return the number of rows affected by the previous operation .
* @ return int
*/
public function affectedRows () {
return pg_affected_rows ( DB :: $lastQuery );
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
* A function to return the field names and datatypes for the particular table
*/
public function tableDetails ( $tableName ){
2011-01-11 22:17:17 +01:00
$schema_SQL = pg_escape_string ( $this -> dbConn , $this -> schema );
2010-11-25 04:45:32 +01:00
$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 FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname ~ '^( $tableName ) $ ' AND pg_catalog.pg_table_is_visible(c.oid) AND n.nspname = ' { $schema_SQL } '); " ;
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
$result = DB :: query ( $query );
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
$table = Array ();
while ( $row = pg_fetch_assoc ( $result )){
$table [] = Array ( 'Column' => $row [ 'Column' ], 'DataType' => $row [ 'DataType' ]);
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
return $table ;
}
2011-01-11 22:17:17 +01:00
2009-09-16 05:51:38 +02:00
/**
* Pass a legit trigger name and it will be dropped
* This assumes that the trigger has been named in a unique fashion
*/
function dropTrigger ( $triggerName , $tableName ){
$exists = DB :: query ( " SELECT tgname FROM pg_trigger WHERE tgname=' $triggerName '; " ) -> first ();
if ( $exists ){
DB :: query ( " DROP trigger $triggerName ON \" $tableName\ " ; " );
}
}
2011-01-11 22:17:17 +01:00
2010-11-11 20:13:52 +01:00
/**
* This will return the fields that the trigger is monitoring
* @ param string $trigger
2011-01-11 22:17:17 +01:00
*
2010-11-11 20:13:52 +01:00
* @ return array
*/
function triggerFieldsFromTrigger ( $trigger ){
2011-01-11 22:17:17 +01:00
2010-11-11 20:13:52 +01:00
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 );
2011-01-11 22:17:17 +01:00
2010-11-11 20:13:52 +01:00
$fields = $bits [ 2 ];
2011-01-11 22:17:17 +01:00
2010-11-11 20:13:52 +01:00
$field_bits = explode ( ',' , str_replace ( '"' , '' , $fields ));
$result = array ();
foreach ( $field_bits as $field_bit )
$result [] = trim ( $field_bit );
2011-01-11 22:17:17 +01:00
2010-11-11 20:13:52 +01:00
return $result ;
} else
return false ;
}
2011-01-11 22:17:17 +01:00
2013-06-06 03:43:25 +02:00
/**
* 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 . '";' );
}
2008-11-23 02:20:39 +01:00
/**
* Return a boolean type - formatted string
2011-01-11 22:17:17 +01:00
*
2008-11-23 02:20:39 +01:00
* @ params array $values Contains a tokenised list of info about this data type
* @ return string
*/
2009-02-17 04:57:21 +01:00
public function boolean ( $values , $asDbValue = false ){
//Annoyingly, we need to do a good ol' fashioned switch here:
2009-03-12 22:58:20 +01:00
( $values [ 'default' ]) ? $default = '1' : $default = '0' ;
2011-01-11 22:17:17 +01:00
2009-10-05 04:03:00 +02:00
if ( ! isset ( $values [ 'arrayValue' ]))
$values [ 'arrayValue' ] = '' ;
2011-01-11 22:17:17 +01:00
2009-02-17 04:57:21 +01:00
if ( $asDbValue )
2009-03-12 22:58:20 +01:00
return Array ( 'data_type' => 'smallint' );
2009-09-29 22:59:45 +02:00
else {
if ( $values [ 'arrayValue' ] != '' )
$default = '' ;
else
$default = ' default ' . ( int ) $values [ 'default' ];
2011-01-11 22:17:17 +01:00
2009-09-29 22:59:45 +02:00
return " smallint { $values [ 'arrayValue' ] } " . $default ;
2011-01-11 22:17:17 +01:00
2009-09-29 22:59:45 +02:00
}
2008-11-23 02:20:39 +01:00
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
* Return a date type - formatted string
2011-01-11 22:17:17 +01:00
*
2008-11-23 02:20:39 +01:00
* @ params array $values Contains a tokenised list of info about this data type
* @ return string
*/
public function date ( $values ){
2011-01-11 22:17:17 +01:00
2009-10-05 04:03:00 +02:00
if ( ! isset ( $values [ 'arrayValue' ]))
$values [ 'arrayValue' ] = '' ;
2011-01-11 22:17:17 +01:00
2009-09-29 22:59:45 +02:00
return " date { $values [ 'arrayValue' ] } " ;
2008-11-23 02:20:39 +01:00
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
* Return a decimal type - formatted string
2011-01-11 22:17:17 +01:00
*
2008-11-23 02:20:39 +01:00
* @ params array $values Contains a tokenised list of info about this data type
* @ return string
*/
2009-02-17 04:57:21 +01:00
public function decimal ( $values , $asDbValue = false ){
2011-01-11 22:17:17 +01:00
2009-10-05 04:03:00 +02:00
if ( ! isset ( $values [ 'arrayValue' ]))
$values [ 'arrayValue' ] = '' ;
2011-01-11 22:17:17 +01:00
2009-02-17 04:57:21 +01:00
// Avoid empty strings being put in the db
if ( $values [ 'precision' ] == '' ) {
$precision = 1 ;
} else {
$precision = $values [ 'precision' ];
}
2011-01-11 22:17:17 +01:00
2010-03-18 04:16:30 +01:00
$defaultValue = '' ;
if ( isset ( $values [ 'default' ]) && is_numeric ( $values [ 'default' ])) {
$defaultValue = ' default ' . $values [ 'default' ];
}
2011-01-11 22:17:17 +01:00
2009-02-17 04:57:21 +01:00
if ( $asDbValue )
2010-11-11 20:13:52 +01:00
return Array ( 'data_type' => 'numeric' , 'precision' => $precision );
else return " decimal( $precision ) { $values [ 'arrayValue' ] } $defaultValue " ;
2008-11-23 02:20:39 +01:00
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
* Return a enum type - formatted string
2011-01-11 22:17:17 +01:00
*
2008-11-23 02:20:39 +01:00
* @ params array $values Contains a tokenised list of info about this data type
* @ return string
*/
public function enum ( $values ){
2009-02-17 04:57:21 +01:00
//Enums are a bit different. We'll be creating a varchar(255) with a constraint of all the usual enum options.
2008-11-23 02:20:39 +01:00
//NOTE: In this one instance, we are including the table name in the values array
2009-09-29 22:59:45 +02:00
if ( ! isset ( $values [ 'arrayValue' ]))
$values [ 'arrayValue' ] = '' ;
2011-01-11 22:17:17 +01:00
2009-09-29 22:59:45 +02:00
if ( $values [ 'arrayValue' ] != '' )
$default = '' ;
else
$default = " default ' { $values [ 'default' ] } ' " ;
2011-01-11 22:17:17 +01:00
2009-09-29 22:59:45 +02:00
return " varchar(255) { $values [ 'arrayValue' ] } " . $default . " check ( \" " . $values [ 'name' ] . " \" in (' " . implode ( '\', \'' , $values [ 'enums' ]) . " ')) " ;
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
* Return a float type - formatted string
2011-01-11 22:17:17 +01:00
*
2008-11-23 02:20:39 +01:00
* @ params array $values Contains a tokenised list of info about this data type
* @ return string
*/
2009-02-17 04:57:21 +01:00
public function float ( $values , $asDbValue = false ){
2009-10-05 04:03:00 +02:00
if ( ! isset ( $values [ 'arrayValue' ]))
$values [ 'arrayValue' ] = '' ;
2011-01-11 22:17:17 +01:00
2009-02-17 04:57:21 +01:00
if ( $asDbValue )
return Array ( 'data_type' => 'double precision' );
2009-09-29 22:59:45 +02:00
else return " float { $values [ 'arrayValue' ] } " ;
2008-11-23 02:20:39 +01:00
}
2011-01-11 22:17:17 +01:00
2010-02-04 05:29:37 +01:00
/**
* Return a float type - formatted string cause double is not supported
2011-01-11 22:17:17 +01:00
*
2010-02-04 05:29:37 +01:00
* @ params array $values Contains a tokenised list of info about this data type
* @ return string
*/
public function double ( $values , $asDbValue = false ){
return $this -> float ( $values , $asDbValue );
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
* Return a int type - formatted string
2011-01-11 22:17:17 +01:00
*
2008-11-23 02:20:39 +01:00
* @ params array $values Contains a tokenised list of info about this data type
* @ return string
*/
2009-02-17 04:57:21 +01:00
public function int ( $values , $asDbValue = false ){
2011-01-11 22:17:17 +01:00
2009-10-05 04:03:00 +02:00
if ( ! isset ( $values [ 'arrayValue' ]))
$values [ 'arrayValue' ] = '' ;
2011-01-11 22:17:17 +01:00
2009-02-17 04:57:21 +01:00
if ( $asDbValue )
2009-10-05 04:03:00 +02:00
return Array ( 'data_type' => 'integer' , 'precision' => '32' );
2009-09-29 22:59:45 +02:00
else {
if ( $values [ 'arrayValue' ] != '' )
$default = '' ;
else
$default = ' default ' . ( int ) $values [ 'default' ];
2011-01-11 22:17:17 +01:00
2009-10-01 23:01:23 +02:00
return " integer { $values [ 'arrayValue' ] } " . $default ;
2009-09-29 22:59:45 +02:00
}
2008-11-23 02:20:39 +01:00
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
* Return a datetime type - formatted string
* For PostgreSQL , we simply return the word 'timestamp' , no other parameters are necessary
2011-01-11 22:17:17 +01:00
*
2008-11-23 02:20:39 +01:00
* @ params array $values Contains a tokenised list of info about this data type
* @ return string
*/
2009-11-02 02:58:51 +01:00
public function SS_Datetime ( $values , $asDbValue = false ){
2011-01-11 22:17:17 +01:00
2009-10-05 04:03:00 +02:00
if ( ! isset ( $values [ 'arrayValue' ]))
$values [ 'arrayValue' ] = '' ;
2011-01-11 22:17:17 +01:00
2009-02-17 04:57:21 +01:00
if ( $asDbValue )
return Array ( 'data_type' => 'timestamp without time zone' );
else
2009-09-29 22:59:45 +02:00
return " timestamp { $values [ 'arrayValue' ] } " ;
2008-11-23 02:20:39 +01:00
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
* Return a text type - formatted string
2011-01-11 22:17:17 +01:00
*
2008-11-23 02:20:39 +01:00
* @ params array $values Contains a tokenised list of info about this data type
* @ return string
*/
2009-02-17 04:57:21 +01:00
public function text ( $values , $asDbValue = false ){
2011-01-11 22:17:17 +01:00
2009-10-05 04:03:00 +02:00
if ( ! isset ( $values [ 'arrayValue' ]))
$values [ 'arrayValue' ] = '' ;
2011-01-11 22:17:17 +01:00
2009-02-17 04:57:21 +01:00
if ( $asDbValue )
return Array ( 'data_type' => 'text' );
else
2009-09-29 22:59:45 +02:00
return " text { $values [ 'arrayValue' ] } " ;
2008-11-23 02:20:39 +01:00
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
* Return a time type - formatted string
2011-01-11 22:17:17 +01:00
*
2008-11-23 02:20:39 +01:00
* @ params array $values Contains a tokenised list of info about this data type
* @ return string
*/
public function time ( $values ){
2009-10-05 04:03:00 +02:00
if ( ! isset ( $values [ 'arrayValue' ]))
$values [ 'arrayValue' ] = '' ;
2011-01-11 22:17:17 +01:00
2009-09-29 22:59:45 +02:00
return " time { $values [ 'arrayValue' ] } " ;
2008-11-23 02:20:39 +01:00
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
* Return a varchar type - formatted string
2011-01-11 22:17:17 +01:00
*
2008-11-23 02:20:39 +01:00
* @ params array $values Contains a tokenised list of info about this data type
* @ return string
*/
2009-02-17 04:57:21 +01:00
public function varchar ( $values , $asDbValue = false ){
2011-01-11 22:17:17 +01:00
2009-10-05 04:03:00 +02:00
if ( ! isset ( $values [ 'arrayValue' ]))
$values [ 'arrayValue' ] = '' ;
2011-01-11 22:17:17 +01:00
2009-09-16 05:51:38 +02:00
if ( ! isset ( $values [ 'precision' ]))
$values [ 'precision' ] = 255 ;
2011-01-11 22:17:17 +01:00
2009-02-17 04:57:21 +01:00
if ( $asDbValue )
2009-09-16 05:51:38 +02:00
return Array ( 'data_type' => 'varchar' , 'precision' => $values [ 'precision' ]);
2009-02-17 04:57:21 +01:00
else
2009-09-29 22:59:45 +02:00
return " varchar( { $values [ 'precision' ] } ) { $values [ 'arrayValue' ] } " ;
2009-02-17 04:57:21 +01:00
}
2011-01-11 22:17:17 +01:00
2009-02-17 04:57:21 +01:00
/*
* Return a 4 digit numeric type . MySQL has a proprietary 'Year' type .
2009-11-02 02:58:51 +01:00
* For Postgres , we ' ll use a 4 digit numeric
2009-02-17 04:57:21 +01:00
*/
public function year ( $values , $asDbValue = false ){
2011-01-11 22:17:17 +01:00
2009-10-05 04:03:00 +02:00
if ( ! isset ( $values [ 'arrayValue' ]))
$values [ 'arrayValue' ] = '' ;
2011-01-11 22:17:17 +01:00
2010-11-19 00:22:07 +01:00
//TODO: the DbValue result does not include the numeric_scale option (ie, the ,0 value in 4,0)
2009-02-17 04:57:21 +01:00
if ( $asDbValue )
2011-01-11 22:17:17 +01:00
return Array ( 'data_type' => 'decimal' , 'precision' => '4' );
else
return " decimal(4,0) { $values [ 'arrayValue' ] } " ;
2008-11-23 02:20:39 +01:00
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
function escape_character ( $escape = false ){
if ( $escape )
return " \\ \" " ;
else
return " \" " ;
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
2009-03-03 22:46:27 +01:00
* Create a fulltext search datatype for PostgreSQL
2009-11-24 23:46:55 +01:00
* This will also return a trigger to be applied to this table
2011-01-11 22:17:17 +01:00
*
2009-11-24 23:46:55 +01:00
* @ todo : create custom functions to allow weighted searches
2008-11-23 02:20:39 +01:00
*
* @ param array $spec
*/
2009-11-24 23:46:55 +01:00
function fulltext ( $this_index , $tableName , $name ){
//For full text search, we need to create a column for the index
2012-09-17 01:13:41 +02:00
$columns = $this -> quoteColumnSpecString ( $this_index [ 'value' ]);
2011-01-11 22:17:17 +01:00
2012-09-17 06:15:00 +02:00
$fulltexts = " \" ts_ $name\ " tsvector " ;
$triggerName = $this -> buildPostgresTriggerName ( $tableName , $name );
$language = $this -> get_search_language ();
2011-01-11 22:17:17 +01:00
2009-11-24 23:46:55 +01:00
$this -> dropTrigger ( $triggerName , $tableName );
2012-09-17 06:15:00 +02:00
$triggers = " CREATE TRIGGER \" $triggerName\ " BEFORE INSERT OR UPDATE
2009-11-24 23:46:55 +01:00
ON \ " $tableName\ " FOR EACH ROW EXECUTE PROCEDURE
2010-11-25 04:45:32 +01:00
tsvector_update_trigger ( \ " ts_ $name\ " , 'pg_catalog.$language' , $columns ); " ;
2011-01-11 22:17:17 +01:00
2012-09-17 06:15:00 +02:00
return Array ( 'name' => $name , 'ts_name' => " ts_ { $name } " , 'fulltexts' => $fulltexts , 'triggers' => $triggers );
2009-11-24 23:46:55 +01:00
}
2011-01-11 22:17:17 +01:00
2009-02-13 03:46:54 +01:00
/**
* This returns the column which is the primary key for each table
* In Postgres , it is a SERIAL8 , which is the equivalent of an auto_increment
*
* @ return string
*/
2009-02-17 04:57:21 +01:00
function IdColumn ( $asDbValue = false ){
if ( $asDbValue )
return 'bigint' ;
else return 'serial8 not null' ;
2009-02-13 03:46:54 +01:00
}
2011-01-11 22:17:17 +01:00
2008-11-24 00:29:09 +01:00
/**
* Returns true if this table exists
*/
function hasTable ( $tableName ) {
2011-01-11 22:17:17 +01:00
$schema_SQL = pg_escape_string ( $this -> dbConn , $this -> schema );
2012-08-21 06:21:45 +02:00
$result = $this -> query ( " SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = ' { $schema_SQL } ' AND tablename = ' " . $this -> addslashes ( $tableName ) . " ' " );
2011-01-11 22:17:17 +01:00
2010-06-03 07:02:11 +02:00
if ( $result -> numRecords () > 0 ) return true ;
else return false ;
2008-11-24 00:29:09 +01:00
}
2011-01-11 22:17:17 +01:00
2009-03-12 03:48:48 +01:00
/**
* Returns the SQL command to get all the tables in this database
*/
function allTablesSQL (){
2011-01-11 22:17:17 +01:00
$schema_SQL = pg_escape_string ( $this -> dbConn , $this -> schema );
return " SELECT table_name FROM information_schema.tables WHERE table_schema=' { $schema_SQL } ' AND table_type='BASE TABLE'; " ;
2009-03-12 03:48:48 +01:00
}
2011-01-11 22:17:17 +01:00
2008-11-24 00:29:09 +01:00
/**
* Return enum values for the given field
* @ todo Make a proper implementation
*/
function enumValuesForField ( $tableName , $fieldName ) {
2009-11-26 22:19:18 +01:00
//return array('SiteTree','Page');
$constraints = $this -> constraintExists ( " { $tableName } _ { $fieldName } _check " );
$classes = Array ();
if ( $constraints )
$classes = $this -> EnumValuesFromConstraint ( $constraints [ 'pg_get_constraintdef' ]);
2011-01-11 22:17:17 +01:00
2009-11-26 22:19:18 +01:00
return $classes ;
2008-11-24 00:29:09 +01:00
}
2009-11-26 22:19:18 +01:00
/**
* Get the actual enum fields from the constraint value :
*/
private 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 ], " ' " ));
}
2011-01-11 22:17:17 +01:00
2009-11-26 22:19:18 +01:00
return $constraints ;
}
2011-01-11 22:17:17 +01:00
2009-03-12 03:48:48 +01:00
/**
* Because NOW () doesn ' t always work ...
* MSSQL , I ' m looking at you
*
*/
function now (){
return 'NOW()' ;
}
2011-01-11 22:17:17 +01:00
2009-09-16 05:51:38 +02:00
/*
* Returns the database - specific version of the random () function
*/
function random (){
return 'RANDOM()' ;
}
2011-01-11 22:17:17 +01:00
2010-03-08 22:58:28 +01:00
/*
* This is a lookup table for data types .
* For instance , Postgres uses 'INT' , while MySQL uses 'UNSIGNED'
* So this is a DB - specific list of equivilents .
*/
function dbDataType ( $type ){
$values = Array (
'unsigned integer' => 'INT'
);
2011-01-11 22:17:17 +01:00
2010-03-08 22:58:28 +01:00
if ( isset ( $values [ $type ]))
return $values [ $type ];
else return '' ;
}
2011-01-11 22:17:17 +01:00
2009-03-12 03:48:48 +01:00
/*
* This will return text which has been escaped in a database - friendly manner
* Using PHP 's addslashes method won' t work in MSSQL
*/
function addslashes ( $value ){
2013-12-20 02:34:31 +01:00
return pg_escape_string ( $this -> dbConn , $value );
2009-03-12 03:48:48 +01:00
}
2011-01-11 22:17:17 +01:00
2009-04-06 01:20:09 +02:00
/*
* This changes the index name depending on database requirements .
*/
2012-09-17 06:15:00 +02:00
function modifyIndex ( $index , $spec ) {
return $index ;
2009-04-06 01:20:09 +02:00
}
2011-01-11 22:17:17 +01:00
2009-09-16 05:51:38 +02:00
/**
* The core search engine configuration .
2009-10-05 04:03:00 +02:00
* @ todo Properly extract the search functions out of the core .
2011-01-11 22:17:17 +01:00
*
2009-09-16 05:51:38 +02:00
* @ param string $keywords Keywords as a space separated string
* @ return object DataObjectSet of result pages
*/
2009-09-30 05:50:27 +02:00
public function searchEngine ( $classesToSearch , $keywords , $start , $pageLength , $sortBy = " ts_rank DESC " , $extraFilter = " " , $booleanSearch = false , $alternativeFileFilter = " " , $invertedMatch = false ) {
2010-08-03 06:18:51 +02:00
//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 );
2011-01-11 22:17:17 +01:00
2009-09-30 05:50:27 +02:00
$keywords = Convert :: raw2sql ( trim ( $keywords ));
2010-11-01 01:53:57 +01:00
$htmlEntityKeywords = htmlentities ( $keywords , ENT_NOQUOTES );
2011-01-11 22:17:17 +01:00
2009-09-30 05:50:27 +02:00
//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:
$result = DB :: query ( " SELECT table_name, column_name, data_type FROM information_schema.columns WHERE data_type='tsvector' AND table_name in (' " . implode ( " ', ' " , $classesToSearch ) . " '); " );
2010-03-31 02:27:28 +02:00
if ( ! $result -> numRecords ()) throw new Exception ( 'there are no full text columns to search' );
2011-01-11 22:17:17 +01:00
2009-09-16 05:51:38 +02:00
$tables = Array ();
2011-01-11 22:17:17 +01:00
2009-10-05 22:26:41 +02:00
// Make column selection lists
$select = array (
2012-05-08 05:33:18 +02:00
'SiteTree' => array (
" \" ClassName \" " ,
" \" SiteTree \" . \" ID \" " ,
" \" ParentID \" " ,
" \" Title \" " ,
" \" URLSegment \" " ,
" \" Content \" " ,
" \" LastEdited \" " ,
" \" Created \" " ,
" NULL AS \" Filename \" " ,
" NULL AS \" Name \" " ,
" \" CanViewType \" "
),
'File' => array (
" \" ClassName \" " ,
" \" File \" . \" ID \" " ,
" 0 AS \" ParentID \" " ,
" \" Title \" " ,
" NULL AS \" URLSegment \" " ,
" \" Content \" " ,
" \" LastEdited \" " ,
" \" Created \" " ,
" \" Filename \" " ,
" \" Name \" " ,
" NULL AS \" CanViewType \" "
)
2009-10-05 22:26:41 +02:00
);
2011-01-11 22:17:17 +01:00
2009-09-16 05:51:38 +02:00
foreach ( $result as $row ){
2011-09-15 16:01:53 +02:00
if ( $row [ 'table_name' ] == 'SiteTree' ) {
2009-09-30 05:50:27 +02:00
$showInSearch = " AND \" ShowInSearch \" =1 " ;
2011-09-15 16:01:53 +02:00
} elseif ( $row [ 'table_name' ] == 'File' ) {
// File.ShowInSearch was added later, keep the database driver backwards compatible
// by checking for its existence first
$fields = $this -> fieldList ( $row [ 'table_name' ]);
if ( array_key_exists ( 'ShowInSearch' , $fields )) $showInSearch = " AND \" ShowInSearch \" =1 " ;
2011-09-15 18:01:02 +02:00
else $showInSearch = '' ;
2011-09-15 16:01:53 +02:00
} else {
2009-09-30 05:50:27 +02:00
$showInSearch = '' ;
2011-09-15 16:01:53 +02:00
}
2011-01-11 22:17:17 +01:00
2009-10-05 22:26:41 +02:00
//public function extendedSQL($filter = "", $sort = "", $limit = "", $join = "", $having = ""){
2012-02-11 03:58:04 +01:00
$where = " \" " . $row [ 'table_name' ] . " \" . \" " . $row [ 'column_name' ] . " \" " . $this -> default_fts_search_method . ' q ' . $showInSearch ;
2012-05-08 05:33:18 +02:00
$query = DataList :: create ( $row [ 'table_name' ]) -> where ( $where , '' ) -> dataQuery () -> query ();
2011-01-11 22:17:17 +01:00
2012-05-08 05:33:18 +02:00
$query -> addFrom ( array ( 'tsearch' => " , to_tsquery(' " . $this -> get_search_language () . " ', ' $keywords ') AS q " ));
$query -> setSelect ( array ());
2011-01-11 22:17:17 +01:00
2012-05-08 05:33:18 +02:00
foreach ( $select [ $row [ 'table_name' ]] as $clause ) {
if ( preg_match ( '/^(.*) +AS +"?([^"]*)"?/i' , $clause , $matches )) {
$query -> selectField ( $matches [ 1 ], $matches [ 2 ]);
} else {
$query -> selectField ( $clause );
}
}
2011-01-11 22:17:17 +01:00
2012-05-08 05:33:18 +02:00
$query -> selectField ( " ts_rank( \" { $row [ 'table_name' ] } \" . \" { $row [ 'column_name' ] } \" , q) " , 'Relevance' );
$query -> setOrderBy ( array ());
2011-01-11 22:17:17 +01:00
2009-09-30 05:50:27 +02:00
//Add this query to the collection
2009-10-05 22:26:41 +02:00
$tables [] = $query -> sql ();
2009-09-16 05:51:38 +02:00
}
2011-01-11 22:17:17 +01:00
2009-09-30 05:50:27 +02:00
$limit = $pageLength ;
$offset = $start ;
2011-01-11 22:17:17 +01:00
2009-09-30 05:50:27 +02:00
if ( $keywords )
$orderBy = " ORDER BY $sortBy " ;
else $orderBy = '' ;
2011-01-11 22:17:17 +01:00
2009-09-30 05:50:27 +02:00
$fullQuery = " SELECT * FROM ( " . implode ( " UNION " , $tables ) . " ) AS q1 $orderBy LIMIT $limit OFFSET $offset " ;
2011-01-11 22:17:17 +01:00
2009-09-16 05:51:38 +02:00
// Get records
$records = DB :: query ( $fullQuery );
2009-09-30 05:50:27 +02:00
$totalCount = 0 ;
2009-09-16 05:51:38 +02:00
foreach ( $records as $record ){
2009-10-05 22:26:41 +02:00
$objects [] = new $record [ 'ClassName' ]( $record );
2009-09-30 05:50:27 +02:00
$totalCount ++ ;
2009-09-16 05:51:38 +02:00
}
2011-01-11 22:17:17 +01:00
2012-05-08 05:33:18 +02:00
if ( isset ( $objects )) $results = new ArrayList ( $objects );
else $results = new ArrayList ();
$list = new PaginatedList ( $results );
2012-09-17 06:51:20 +02:00
$list -> setLimitItems ( false );
2012-05-08 05:33:18 +02:00
$list -> setPageStart ( $start );
$list -> setPageLength ( $pageLength );
$list -> setTotalItems ( $totalCount );
return $list ;
2009-09-16 05:51:38 +02:00
}
2011-01-11 22:17:17 +01:00
2009-10-01 23:01:23 +02:00
/*
* Does this database support transactions ?
*/
public function supportsTransactions (){
return $this -> supportsTransactions ;
}
2011-01-11 22:17:17 +01:00
2009-10-08 03:19:15 +02:00
/*
* 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 ;
}
2011-03-11 04:43:27 +01:00
/**
* @ deprecated 1.0 Use transactionStart () ( method required for 2.4 . x )
*/
public function startTransaction ( $transaction_mode = false , $session_characteristics = false ){
$this -> transactionStart ( $transaction_mode , $session_characteristics );
}
2011-01-11 22:17:17 +01:00
2009-10-01 23:01:23 +02:00
/*
* Start a prepared transaction
* See http :// developer . postgresql . org / pgdocs / postgres / sql - set - transaction . html for details on transaction isolation options
*/
2011-02-11 02:24:39 +01:00
public function transactionStart ( $transaction_mode = false , $session_characteristics = false ){
2009-10-01 23:01:23 +02:00
DB :: query ( 'BEGIN;' );
if ( $transaction_mode )
DB :: query ( 'SET TRANSACTION ' . $transaction_mode . ';' );
2011-01-11 22:17:17 +01:00
2009-10-01 23:01:23 +02:00
if ( $session_characteristics )
DB :: query ( 'SET SESSION CHARACTERISTICS AS TRANSACTION ' . $session_characteristics . ';' );
}
2011-01-11 22:17:17 +01:00
2009-10-01 23:01:23 +02:00
/*
* Create a savepoint that you can jump back to if you encounter problems
*/
public function transactionSavepoint ( $savepoint ){
DB :: query ( " SAVEPOINT $savepoint ; " );
}
2011-01-11 22:17:17 +01:00
2009-10-01 23:01:23 +02:00
/*
* Rollback or revert to a savepoint if your queries encounter problems
* If you encounter a problem at any point during a transaction , you may
* need to rollback that particular query , or return to a savepoint
*/
public function transactionRollback ( $savepoint = false ){
2011-01-11 22:17:17 +01:00
2009-10-01 23:01:23 +02:00
if ( $savepoint )
DB :: query ( " ROLLBACK TO $savepoint ; " );
else
DB :: query ( 'ROLLBACK;' );
2011-01-12 01:10:38 +01:00
2009-10-01 23:01:23 +02:00
}
2011-03-11 04:43:27 +01:00
/**
* @ deprecated 1.0 Use transactionEnd () ( method required for 2.4 . x )
*/
public function endTransaction (){
$this -> transactionEnd ();
}
2011-01-11 22:17:17 +01:00
2009-10-01 23:01:23 +02:00
/*
* Commit everything inside this transaction so far
*/
2011-01-11 22:17:17 +01:00
public function transactionEnd (){
2009-10-01 23:01:23 +02:00
DB :: query ( 'COMMIT;' );
}
2011-01-11 22:17:17 +01:00
2009-10-08 03:19:15 +02:00
/*
* Given a tablespace and and location , either create a new one
* or update the existing one
*/
public function createOrReplaceTablespace ( $name , $location ){
$existing = DB :: query ( " SELECT spcname, spclocation FROM pg_tablespace WHERE spcname=' $name '; " ) -> first ();
2011-01-11 22:17:17 +01:00
2009-10-08 03:19:15 +02:00
//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 :(
2011-01-11 22:17:17 +01:00
2009-10-08 03:19:15 +02:00
//If a tablespace with this name exists, but the location has changed, then drop the current one
2009-11-24 23:46:55 +01:00
//if($existing && $location!=$existing['spclocation'])
// DB::query("DROP TABLESPACE $name;");
2011-01-11 22:17:17 +01:00
2009-10-08 03:19:15 +02:00
//If this is a new tablespace, or we have dropped the current one:
if ( ! $existing || ( $existing && $location != $existing [ 'spclocation' ]))
DB :: query ( " CREATE TABLESPACE $name LOCATION ' $location '; " );
2011-01-11 22:17:17 +01:00
2009-10-08 03:19:15 +02:00
}
2011-01-11 22:17:17 +01:00
2009-11-24 23:46:55 +01:00
public function createOrReplacePartition ( $tableName , $partitions , $indexes , $extensions ){
2011-01-11 22:17:17 +01:00
2009-11-24 23:46:55 +01:00
//We need the plpgsql language to be installed for this to work:
$this -> createLanguage ( 'plpgsql' );
2011-01-11 22:17:17 +01:00
2009-10-08 03:19:15 +02:00
$trigger = 'CREATE OR REPLACE FUNCTION ' . $tableName . '_insert_trigger() RETURNS TRIGGER AS $$ BEGIN ' ;
$first = true ;
2011-01-11 22:17:17 +01:00
2009-11-24 23:46:55 +01:00
//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' ];
2011-01-11 22:17:17 +01:00
} else
2009-11-24 23:46:55 +01:00
$tableSpace = '' ;
2011-01-11 22:17:17 +01:00
2009-10-08 03:19:15 +02:00
foreach ( $partitions as $partition_name => $partition_value ){
//Check that this child table does not already exist:
if ( ! $this -> TableExists ( $partition_name )){
2009-11-24 23:46:55 +01:00
DB :: query ( " CREATE TABLE \" $partition_name\ " ( CHECK ( " . str_replace('NEW.', '', $partition_value ) . " )) INHERITS ( \ " $tableName\ " ) $tableSpace ; " );
2009-10-08 03:19:15 +02:00
} else {
//Drop the constraint, we will recreate in in the next line
2010-02-17 21:19:22 +01:00
$existing_constraint = $this -> query ( " SELECT conname FROM pg_constraint WHERE conname=' { $partition_name } _pkey'; " );
if ( $existing_constraint ){
DB :: query ( " ALTER TABLE \" $partition_name\ " DROP CONSTRAINT \ " { $partition_name } _pkey \" ; " );
}
2009-10-08 03:19:15 +02:00
$this -> dropTrigger ( strtolower ( 'trigger_' . $tableName . '_insert' ), $tableName );
}
2011-01-11 22:17:17 +01:00
2009-11-24 23:46:55 +01:00
DB :: query ( " ALTER TABLE \" $partition_name\ " ADD CONSTRAINT \ " { $partition_name } _pkey \" PRIMARY KEY ( \" ID \" ); " );
2011-01-11 22:17:17 +01:00
2009-10-08 03:19:15 +02:00
if ( $first ){
$trigger .= 'IF' ;
$first = false ;
} else
$trigger .= 'ELSIF' ;
2011-01-11 22:17:17 +01:00
2009-11-24 23:46:55 +01:00
$trigger .= " ( $partition_value ) THEN INSERT INTO \" $partition_name\ " VALUES ( NEW .* ); " ;
2011-01-11 22:17:17 +01:00
2009-11-24 23:46:55 +01:00
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 ){
2011-01-11 22:17:17 +01:00
2009-11-24 23:46:55 +01:00
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' ];
2011-01-11 22:17:17 +01:00
2012-09-18 02:41:34 +02:00
DB :: query ( " CREATE INDEX \" " . $this -> buildPostgresIndexName ( $partition_name , $this_index [ 'name' ]) . " \" ON \" " . $partition_name . " \" USING " . $this -> default_fts_cluster_method . " ( \" ts_ " . $name . " \" ) $fillfactor $where " );
2009-11-24 23:46:55 +01:00
$ts_details = $this -> fulltext ( $this_index , $partition_name , $name );
DB :: query ( $ts_details [ 'triggers' ]);
} else {
2011-01-11 22:17:17 +01:00
2009-11-24 23:46:55 +01:00
if ( is_array ( $this_index ))
$index_name = $this_index [ 'name' ];
else $index_name = trim ( $this_index , '()' );
2011-01-11 22:17:17 +01:00
2010-03-18 04:16:30 +01:00
$createIndex = $this -> getIndexSqlDefinition ( $partition_name , $index_name , $this_index );
if ( $createIndex !== false )
DB :: query ( $createIndex );
2009-11-24 23:46:55 +01:00
}
}
}
2011-01-11 22:17:17 +01:00
2009-11-24 23:46:55 +01:00
//Lastly, clustering goes here:
if ( $extensions && isset ( $extensions [ 'cluster' ])){
DB :: query ( " CLUSTER \" $partition_name\ " USING \ " { $extensions [ 'cluster' ] } \" ; " );
}
2009-10-08 03:19:15 +02:00
}
2011-01-11 22:17:17 +01:00
2009-10-08 03:19:15 +02:00
$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();' ;
2011-01-11 22:17:17 +01:00
2009-10-08 03:19:15 +02:00
DB :: query ( $trigger );
2009-11-24 23:46:55 +01:00
}
2011-01-11 22:17:17 +01:00
2009-11-24 23:46:55 +01:00
/*
* This will create a language if it doesn ' t already exist .
* This is used by the createOrReplacePartition function , which needs plpgsql
*/
public function createLanguage ( $language ){
$result = DB :: query ( " SELECT lanname FROM pg_language WHERE lanname=' $language '; " ) -> first ();
2011-01-11 22:17:17 +01:00
2009-11-24 23:46:55 +01:00
if ( ! $result ){
DB :: query ( " CREATE LANGUAGE $language ; " );
}
2009-10-08 03:19:15 +02:00
}
2010-02-04 05:29:37 +01:00
2012-12-11 01:47:47 +01:00
/**
* Generate a WHERE clause for text matching .
*
* @ param String $field Quoted field name
* @ param String $value Escaped search . Can include percentage wildcards .
* @ param boolean $exact Exact matches or wildcard support .
* @ param boolean $negate Negate the clause .
* @ param boolean $caseSensitive Enforce case sensitivity if TRUE or FALSE .
* Stick with default collation if set to NULL .
* @ return String SQL
*/
public function comparisonClause ( $field , $value , $exact = false , $negate = false , $caseSensitive = null ) {
if ( $exact && $caseSensitive === null ) {
$comp = ( $negate ) ? '!=' : '=' ;
} else {
$comp = ( $caseSensitive === true ) ? 'LIKE' : 'ILIKE' ;
if ( $negate ) $comp = 'NOT ' . $comp ;
}
return sprintf ( " %s %s '%s' " , $field , $comp , $value );
}
2010-02-04 05:29:37 +01:00
/**
* 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
*/
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 );
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 ' " ;
}
2010-02-04 22:30:10 +01:00
if ( $format == '%U' ) return " FLOOR(EXTRACT(epoch FROM $date )) " ;
2011-01-11 22:17:17 +01:00
2010-02-04 05:29:37 +01:00
return " to_char( $date , TEXT ' $format ') " ;
2011-01-11 22:17:17 +01:00
2010-02-04 05:29:37 +01:00
}
2011-01-11 22:17:17 +01:00
2010-02-04 05:29:37 +01:00
/**
* 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
*/
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 ' " ;
}
2010-04-30 06:26:53 +02:00
// ... when being too precise becomes a pain. we need to cut of the fractions.
2010-02-04 22:30:10 +01:00
// 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) " ;
2010-02-04 05:29:37 +01:00
}
/**
* 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
*/
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() " ;
} else if ( preg_match ( '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i' , $date2 )) {
$date2 = " TIMESTAMP ' $date2 ' " ;
}
2010-02-04 22:30:10 +01:00
return " (FLOOR(EXTRACT(epoch FROM $date1 )) - FLOOR(EXTRACT(epoch from $date2 ))) " ;
2010-02-04 05:29:37 +01:00
}
2011-01-11 22:17:17 +01:00
2010-03-18 04:16:30 +01:00
/**
* Return a set type - formatted string
* This is used for Multi - enum support , which isn ' t actually supported by Postgres .
2010-03-30 05:06:35 +02:00
* 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 .
2011-01-11 22:17:17 +01:00
*
2010-03-18 04:16:30 +01:00
* @ param array $values Contains a tokenised list of info about this data type
* @ return string
*/
public function set ( $values ){
2010-03-30 05:06:35 +02:00
user_error ( " PostGreSQL does not support multi-enum " );
return " int " ;
2010-03-18 04:16:30 +01:00
}
2011-01-11 22:17:17 +01:00
2010-11-25 04:45:32 +01:00
/**
* Set the current language for the tsearch functions
2011-01-11 22:17:17 +01:00
*
2010-11-25 04:45:32 +01:00
* @ todo : somehow link this to the locale options ?
2011-01-11 22:17:17 +01:00
*
2010-11-25 04:45:32 +01:00
* @ param string $lang
*/
public function set_search_language ( $lang ){
$this -> search_language = $lang ;
}
2011-01-11 22:17:17 +01:00
2010-11-25 04:45:32 +01:00
/**
* Returns the current language for the tsearch functions
2011-01-11 22:17:17 +01:00
*
2010-11-25 04:45:32 +01:00
* @ param string $lang
*/
public function get_search_language (){
return $this -> search_language ;
}
2008-11-23 02:20:39 +01:00
}
/**
2009-11-02 02:58:51 +01:00
* A result - set from a PostgreSQL database .
2008-11-23 02:20:39 +01:00
* @ package sapphire
* @ subpackage model
*/
2009-11-01 22:59:54 +01:00
class PostgreSQLQuery extends SS_Query {
2008-11-23 02:20:39 +01:00
/**
* The MySQLDatabase object that created this result set .
2009-11-02 02:58:51 +01:00
* @ var PostgreSQLDatabase
2008-11-23 02:20:39 +01:00
*/
private $database ;
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
/**
2009-11-02 02:58:51 +01:00
* The internal Postgres handle that points to the result set .
2008-11-23 02:20:39 +01:00
* @ 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 .
2009-11-02 02:58:51 +01:00
* @ param handle the internal Postgres handle that is points to the resultset .
2008-11-23 02:20:39 +01:00
*/
public function __construct ( PostgreSQLDatabase $database , $handle ) {
$this -> database = $database ;
$this -> handle = $handle ;
}
2011-01-11 22:17:17 +01:00
2010-10-14 07:29:28 +02:00
public function __destruct () {
if ( is_resource ( $this -> handle )) pg_free_result ( $this -> handle );
2008-11-23 02:20:39 +01:00
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
public function seek ( $row ) {
2010-05-28 04:39:52 +02:00
return pg_result_seek ( $this -> handle , $row );
2008-11-23 02:20:39 +01:00
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
public function numRecords () {
return pg_num_rows ( $this -> handle );
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
public function nextRecord () {
2012-05-03 23:56:52 +02:00
if ( $data = pg_fetch_assoc ( $this -> handle )) {
return $data ;
2008-11-23 02:20:39 +01:00
} else {
return false ;
}
}
2011-01-11 22:17:17 +01:00
2008-11-23 02:20:39 +01:00
}