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 {
2010-08-13 01:08:35 +02:00
2008-11-23 02:20:39 +01:00
/**
* Connection to the DBMS .
* @ var resource
*/
private $dbConn ;
/**
* True if we are connected to a database .
* @ var boolean
*/
private $active ;
/**
* The name of the database .
* @ var string
*/
private $database ;
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
* can then switch back in order to drop the temp database
*/
private $database_original ;
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 ;
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 = '@@@' ;
2009-10-01 23:01:23 +02:00
private $supportsTransactions = true ;
2009-09-29 22:59:45 +02:00
2010-08-13 01:08:35 +02:00
/**
* Determines whether to check a database exists on the host by
* querying the 'postgres' database and running createDatabase .
*
* Some locked down systems prevent access to the 'postgres' table in
* which case you need to set this to false .
*/
public static $check_database_exists = true ;
2010-11-01 02:11:31 +01:00
/**
* This holds a copy of all the constraint results that are returned
* via the function constraintExists () . This is a bit faster than
* repeatedly querying this column , and should allow the database
* to use it ' s built - in caching features for better queries .
*
* @ var array
*/
private static $cached_constraints = array ();
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 ) {
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 ();
$this -> database_original = $this -> database ;
}
/*
* 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 (){
$parameters = $this -> parameters ;
if ( ! $parameters )
return false ;
2009-09-16 05:51:38 +02:00
2008-11-23 02:20:39 +01:00
( $parameters [ 'username' ] != '' ) ? $username = ' user=' . $parameters [ 'username' ] : $username = '' ;
( $parameters [ 'password' ] != '' ) ? $password = ' password=' . $parameters [ 'password' ] : $password = '' ;
2009-09-16 05:51:38 +02:00
2010-02-12 04:28:04 +01:00
if ( ! isset ( $this -> database ))
$dbName = $parameters [ 'database' ];
else $dbName = $this -> database ;
2008-11-23 02:20:39 +01:00
2010-04-13 06:42:36 +02:00
$port = empty ( $parameters [ 'port' ]) ? 5432 : $parameters [ 'port' ];
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 );
if ( ! $this -> databaseExists ( $dbName ))
$this -> createDatabase ( $dbName );
}
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 );
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 ;
2008-11-23 02:20:39 +01:00
if ( ! $this -> dbConn ) {
$this -> databaseError ( " Couldn't connect to PostgreSQL database " );
2009-09-16 05:51:38 +02:00
return false ;
2008-11-23 02:20:39 +01:00
}
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 ;
}
2010-04-13 06:42:36 +02:00
/**
* Return the parameters used to construct this database connection
*/
public function getParameters () {
return $this -> parameters ;
}
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
}
/**
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
}
/**
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 " ;
}
public function query ( $sql , $errorLevel = E_USER_ERROR ) {
2009-09-16 05:51:38 +02: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 ;
}
if ( isset ( $_REQUEST [ 'showqueries' ])) {
$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 );
if ( isset ( $_REQUEST [ 'showqueries' ])) {
$endtime = round ( microtime ( true ) - $starttime , 4 );
Debug :: message ( " \n $sql\n { $endtime } ms \n " , false );
}
DB :: $lastQuery = $handle ;
2009-09-16 05:51:38 +02:00
if ( ! $handle && $errorLevel ) $this -> databaseError ( " Couldn't run query: $sql | " . pg_last_error ( $this -> dbConn ), $errorLevel );
2009-03-03 22:46:27 +01:00
2008-11-23 02:20:39 +01:00
return new PostgreSQLQuery ( $this , $handle );
}
public function getGeneratedID ( $table ) {
$result = DB :: query ( " SELECT last_value FROM \" { $table } _ID_seq \" ; " );
$row = $result -> first ();
return $row [ 'last_value' ];
}
/**
* OBSOLETE : Get the ID for the next new record for the table .
*
* @ 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 ;
}
public function isActive () {
return $this -> active ? true : false ;
}
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 ;
$this -> query ( " CREATE DATABASE \" $name\ " ; " );
2010-02-12 04:28:04 +01:00
2009-09-16 05:51:38 +02:00
$this -> connectDatabase ();
2010-02-12 04:28:04 +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 () {
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 ();
2009-09-30 05:53:12 +02:00
$this -> query ( " DROP DATABASE $db_to_drop " );
2008-11-23 02:20:39 +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\ " ; " );
}
2008-11-23 02:20:39 +01:00
/**
* Returns the name of the currently selected database
*/
public function currentDatabase () {
return $this -> database ;
}
/**
* 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 ;
2008-11-23 02:20:39 +01:00
$this -> tableList = $this -> fieldList = $this -> indexList = null ;
2010-02-12 04:28:04 +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
2009-09-16 05:51:38 +02:00
2008-11-23 02:20:39 +01:00
/**
* Returns true if the named database exists .
*/
public function databaseExists ( $name ) {
2010-03-08 22:58:28 +01:00
// We have to use addslashes here, since there may not be a database connection to base the Convert::raw2sql
// function off.
$SQL_name = 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
}
2010-04-30 06:26:53 +02:00
/**
* Returns a column
*/
public function allDatabaseNames () {
return $this -> query ( " SELECT datname FROM pg_database WHERE datistemplate=false; " ) -> column ();
}
2009-10-08 03:19:15 +02:00
public function createTable ( $tableName , $fields = null , $indexes = null , $options = null , $extensions = null ) {
2009-10-05 22:26:41 +02: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 ;
2008-11-23 02:20:39 +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 ;
}
2009-09-16 05:51:38 +02: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 ){
if ( $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
}
}
if ( $indexes ) foreach ( $indexes as $k => $v ) $indexSchemas .= $this -> getIndexSqlDefinition ( $tableName , $k , $v ) . " \n " ;
2009-10-08 03:19:15 +02: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' ];
} else
$tableSpace = '' ;
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 " );
2009-09-16 05:51:38 +02:00
if ( $triggers != '' ){
$this -> query ( $triggers );
}
2009-09-29 22:59:45 +02: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
}
//Lastly, clustering goes here:
if ( $extensions && isset ( $extensions [ 'cluster' ])){
DB :: query ( " CLUSTER \" $tableName\ " USING \ " { $extensions [ 'cluster' ] } \" ; " );
}
2009-11-02 02:58:51 +01:00
2009-09-29 22:59:45 +02:00
return $tableName ;
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 ) {
2008-11-23 02:20:39 +01:00
2009-10-08 03:19:15 +02:00
$fieldSchemas = $indexSchemas = " " ;
2008-11-24 00:29:09 +01:00
$alterList = array ();
2008-11-23 02:20:39 +01:00
if ( $newFields ) foreach ( $newFields as $k => $v ) $alterList [] .= " ADD \" $k\ " $v " ;
2009-02-13 03:46:54 +01:00
if ( $alteredFields ) {
foreach ( $alteredFields as $k => $v ) {
$val = $this -> alterTableAlterColumn ( $tableName , $k , $v );
if ( $val != '' )
$alterList [] .= $val ;
}
}
2008-11-24 00:29:09 +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' ]}; " );
}
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 ;
if ( $alteredIndexes ) foreach ( $alteredIndexes as $key => $v ) {
2009-09-16 05:51:38 +02:00
//We are only going to delete indexes which exist
$indexes = $this -> indexList ( $tableName );
2010-11-11 20:13:52 +01:00
if ( $v [ 'type' ] == 'fulltext' ){
//For full text indexes, we need to drop the trigger, drop the index, AND drop the column
//Go and get the tsearch details:
$ts_details = $this -> fulltext ( $v , $tableName , $key );
//Drop this column if it already exists:
//No IF EXISTS option is available for Postgres <9.0
if ( array_key_exists ( $ts_details [ 'ts_name' ], $fieldList )){
$fulltexts .= " ALTER TABLE \" { $tableName } \" DROP COLUMN \" { $ts_details [ 'ts_name' ] } \" ; " ;
}
$drop_triggers .= 'DROP TRIGGER IF EXISTS ts_' . strtolower ( $tableName ) . '_' . strtolower ( $key ) . ' ON "' . $tableName . '";' ;
$alterIndexList [] = 'DROP INDEX IF EXISTS ix_' . strtolower ( $tableName ) . '_' . strtolower ( $v [ 'value' ]) . ';' ;
//We'll execute these later:
$fulltexts .= " ALTER TABLE \" { $tableName } \" ADD COLUMN { $ts_details [ 'fulltexts' ] } ; " ;
$triggers .= $ts_details [ 'triggers' ];
} else {
if ( isset ( $indexes [ $v [ 'value' ]])){
if ( is_array ( $v ))
$alterIndexList [] = 'DROP INDEX IF EXISTS ix_' . strtolower ( $tableName ) . '_' . strtolower ( $v [ 'value' ]) . ';' ;
else
$alterIndexList [] = 'DROP INDEX IF EXISTS ix_' . strtolower ( $tableName ) . '_' . strtolower ( trim ( $v , '()' )) . ';' ;
$k = $v [ 'value' ];
$createIndex = $this -> getIndexSqlDefinition ( $tableName , $k , $v );
if ( $createIndex !== false )
$alterIndexList [] .= $createIndex ;
}
2009-09-16 05:51:38 +02:00
}
2008-11-23 02:20:39 +01:00
}
2008-11-24 00:29:09 +01:00
2010-11-11 20:13:52 +01: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 ( $newIndexes ){
foreach ( $newIndexes as $name => $this_index ){
if ( $this_index [ 'type' ] == 'fulltext' ){
$ts_details = $this -> fulltext ( $this_index , $tableName , $name );
if ( ! isset ( $fieldList [ $ts_details [ 'ts_name' ]])){
$fulltexts .= " ALTER TABLE \" { $tableName } \" ADD COLUMN { $ts_details [ 'fulltexts' ] } ; " ;
$triggers .= $ts_details [ 'triggers' ];
}
}
}
}
2009-04-06 01:20:09 +02:00
//Add the new indexes:
2009-09-16 05:51:38 +02:00
if ( $newIndexes ) foreach ( $newIndexes as $k => $v ){
//Check that this index doesn't already exist:
$indexes = $this -> indexList ( $tableName );
2010-03-18 04:16:30 +01:00
if ( ! is_array ( $v )){
$name = trim ( $v , '()' );
} else {
2010-06-04 03:36:35 +02:00
$name = ( isset ( $v [ 'name' ])) ? $v [ 'name' ] : $k ;
2010-03-18 04:16:30 +01:00
}
if ( isset ( $indexes [ $name ])){
2009-09-16 05:51:38 +02:00
if ( is_array ( $v )){
2010-09-17 02:44:11 +02:00
$alterIndexList [] = 'DROP INDEX IF EXISTS ix_' . strtolower ( $tableName ) . '_' . strtolower ( $v [ 'value' ]) . ';' ;
2009-09-16 05:51:38 +02:00
} else {
2010-09-17 02:44:11 +02:00
$alterIndexList [] = 'DROP INDEX IF EXISTS ' . $indexes [ $name ][ 'indexname' ] . ';' ;
2009-09-16 05:51:38 +02:00
}
}
2010-03-18 04:16:30 +01:00
$createIndex = $this -> getIndexSqlDefinition ( $tableName , $k , $v );
if ( $createIndex !== false )
$alterIndexList [] = $createIndex ;
2009-04-06 01:20:09 +02: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 );
}
2009-03-04 22:55:40 +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' ]);
}
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 "
);
}
2010-11-11 20:13:52 +01:00
//Create any fulltext columns and triggers here:
if ( $fulltexts )
$this -> query ( $fulltexts );
if ( $drop_triggers )
$this -> query ( $drop_triggers );
if ( $triggers ) {
$this -> query ( $triggers );
$triggerbits = explode ( ';' , $triggers );
foreach ( $triggerbits as $trigger ){
$trigger_fields = $this -> triggerFieldsFromTrigger ( $trigger );
if ( $trigger_fields ){
//We need to run a simple query to force the database to update the triggered columns
$this -> query ( " UPDATE \" { $tableName } \" SET \" { $trigger_fields [ 0 ] } \" = \" $trigger_fields[0] \" ; " );
}
}
}
2009-03-04 22:55:40 +01:00
foreach ( $alterIndexList as $alteration )
$this -> query ( $alteration );
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:
if ( $advancedOptions && isset ( $advancedOptions [ 'cluster' ])){
DB :: query ( " CLUSTER \" $tableName\ " USING ix_ { $tableName } _ { $advancedOptions [ 'cluster' ]}; " );
2009-11-02 02:58:51 +01:00
} else {
//Check that clustering is not on this table, and if it is, remove it:
//This is really annoying. We need the oid of this table:
$stats = DB :: query ( " SELECT relid FROM pg_stat_user_tables WHERE relname=' $tableName '; " ) -> first ();
$oid = $stats [ 'relid' ];
//Now we can run a long query to get the clustered status:
//If anyone knows a better way to get the clustered status, then feel free to replace this!
$clustered = 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 ();
if ( $clustered )
DB :: query ( " ALTER TABLE \" $tableName\ " SET WITHOUT CLUSTER ; " );
}
2008-11-23 02:20:39 +01:00
}
2009-02-13 03:46:54 +01:00
/*
* Creates an ALTER expression for a column in PostgreSQL
*
* @ param $tableName Name of the table to be altered
* @ param $colName Name of the column to be altered
* @ param $colSpec String which contains conditions for a column
* @ return string
*/
private function alterTableAlterColumn ( $tableName , $colName , $colSpec ){
// First, we split the column specifications into parts
// TODO: this returns an empty array for the following string: int(11) not null auto_increment
// on second thoughts, why is an auto_increment field being passed through?
$pattern = '/^([\w()]+)\s?((?:not\s)?null)?\s?(default\s[\w\']+)?\s?(check\s[\w()\'",\s]+)?$/i' ;
preg_match ( $pattern , $colSpec , $matches );
2009-09-16 05:51:38 +02:00
if ( sizeof ( $matches ) == 0 )
return '' ;
2009-02-17 04:57:21 +01:00
if ( $matches [ 1 ] == 'serial8' )
2009-02-13 03:46:54 +01:00
return '' ;
if ( isset ( $matches [ 1 ])) {
$alterCol = " ALTER COLUMN \" $colName\ " TYPE $matches [ 1 ] \n " ;
// SET null / not null
if ( ! empty ( $matches [ 2 ])) $alterCol .= " , \n ALTER COLUMN \" $colName\ " SET $matches [ 2 ] " ;
// 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 ] " ;
}
// 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-11-19 00:10:38 +01:00
//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 [ 0 ], " ' " );
//Now go and convert anything that's not in this list to the first thing in the enum list
//We have to run this as a query, not as part of the alteration queries due to the way they are constructed.
$updateConstraint = '' ;
$updateConstraint .= " UPDATE \" { $tableName } \" SET \" $colName\ " = '$default' WHERE \ " $colName\ " NOT IN ( $constraint_values ); " ;
$updateConstraint .= " UPDATE \" { $tableName } _Live \" SET \" $colName\ " = '$default' WHERE \ " $colName\ " NOT IN ( $constraint_values ); " ;
$updateConstraint .= " UPDATE \" { $tableName } _versions \" SET \" $colName\ " = '$default' WHERE \ " $colName\ " NOT IN ( $constraint_values ); " ;
DB :: query ( $updateConstraint );
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 \" " ;
//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
}
return isset ( $alterCol ) ? $alterCol : '' ;
}
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\ " " );
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 ) {
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 ;
}
/**
* 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 ;
}
public function createField ( $tableName , $fieldName , $fieldSpec ) {
$this -> query ( " ALTER TABLE \" $tableName\ " ADD \ " $fieldName\ " $fieldSpec " );
}
/**
* Change the database type of the given field .
* @ param string $tableName The name of the tbale the field is in .
* @ param string $fieldName The name of the field to change .
* @ param string $fieldSpec The new field specification
*/
public function alterField ( $tableName , $fieldName , $fieldSpec ) {
$this -> query ( " ALTER TABLE \" $tableName\ " CHANGE \ " $fieldName\ " \ " $fieldName\ " $fieldSpec " );
}
2008-11-24 00:29:09 +01:00
/**
* Change the database column name of the given field .
*
* @ param string $tableName The name of the tbale the field is in .
* @ 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\ " " );
2008-11-24 00:29:09 +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....
2010-11-11 20:13:52 +01: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 = ' $table ' ORDER BY ordinal_position; " );
2008-11-24 00:29:09 +01:00
$output = array ();
2008-11-24 00:52:06 +01:00
if ( $fields ) foreach ( $fields as $field ) {
2009-11-25 00:53:38 +01:00
2009-03-03 22:46:27 +01:00
switch ( $field [ 'data_type' ]){
case 'character varying' :
//Check to see if there's a constraint attached to this column:
2009-10-08 03:19:15 +02:00
//$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' );
2009-09-16 05:51:38 +02:00
$enum = '' ;
2009-03-03 22:46:27 +01:00
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 = '' ;
}
}
}
if ( sizeof ( $constraints ) > 0 ){
//Get the default:
$default = trim ( substr ( $field [ 'column_default' ], 0 , strpos ( $field [ 'column_default' ], '::' )), " ' " );
2009-09-16 05:51:38 +02:00
$output [ $field [ 'column_name' ]] = $this -> enum ( Array ( 'default' => $default , 'name' => $field [ 'column_name' ], 'enums' => $constraints ));
2009-03-03 22:46:27 +01:00
}
2009-09-16 05:51:38 +02:00
} else {
$output [ $field [ 'column_name' ]] = 'varchar(' . $field [ 'character_maximum_length' ] . ')' ;
2009-03-03 22:46:27 +01:00
}
2009-09-16 05:51:38 +02:00
break ;
2009-03-03 22:46:27 +01:00
2009-09-16 05:51:38 +02:00
case 'numeric' :
2010-11-11 20:13:52 +01:00
$output [ $field [ 'column_name' ]] = 'decimal(' . $field [ 'numeric_precision' ] . ',' . $field [ 'numeric_scale' ] . ') default ' . $field [ 'column_default' ];
2009-02-17 04:57:21 +01:00
break ;
2009-10-05 04:03:00 +02:00
case 'integer' :
2009-11-25 00:53:38 +01:00
$output [ $field [ 'column_name' ]] = 'integer default ' . $field [ 'column_default' ];
break ;
case 'timestamp without time zone' :
$output [ $field [ 'column_name' ]] = 'timestamp' ;
2009-10-05 04:03:00 +02:00
break ;
2009-11-25 00:53:38 +01:00
case 'smallint' :
$output [ $field [ 'column_name' ]] = 'smallint default ' . $field [ 'column_default' ];
break ;
2010-11-01 02:34:29 +01:00
case 'time without time zone' :
$output [ $field [ 'column_name' ]] = 'time' ;
break ;
case 'double precision' :
$output [ $field [ 'column_name' ]] = 'float' ;
break ;
2009-02-17 04:57:21 +01:00
default :
$output [ $field [ 'column_name' ]] = $field ;
}
2008-11-23 02:20:39 +01:00
}
2008-11-24 00:29:09 +01:00
return $output ;
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 );
if ( $createIndex !== false )
$this -> query ();
2008-11-23 02:20:39 +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 .
*/
2009-03-04 22:55:40 +01:00
public function convertIndexSpec ( $indexSpec , $asDbValue = false , $table = '' ){
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' :
2010-11-11 20:13:52 +01:00
//We need to include the fields so if we change the columns it's indexing, but not the name,
//then the change will be picked up.
$indexSpec = '(ts_' . $indexSpec [ 'name' ] . '_' . $indexSpec [ 'value' ] . ')' ;
2009-03-04 22:55:40 +01:00
break ;
case 'unique' :
$indexSpec = 'unique (' . $indexSpec [ 'value' ] . ')' ;
break ;
case 'hash' :
2010-11-01 02:34:29 +01:00
$indexSpec = '(' . $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-01 02:34:29 +01:00
$indexSpec = '(' . $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 {
$indexSpec = 'ix_' . $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
}
2009-03-04 22:55:40 +01:00
protected function getIndexSqlDefinition ( $tableName , $indexName , $indexSpec , $asDbValue = false ) {
2009-02-13 03:46:54 +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
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.
2009-03-04 22:55:40 +01:00
if ( ! $asDbValue ){
2009-09-16 05:51:38 +02:00
$tableCol = 'ix_' . $tableName . '_' . $indexName ;
if ( strlen ( $tableCol ) > 64 ){
$tableCol = substr ( $indexName , 0 , 59 ) . rand ( 1000 , 9999 );
}
2009-10-05 04:03:00 +02:00
//It is possible to specify indexes through strings:
2009-03-04 22:55:40 +01:00
if ( ! is_array ( $indexSpec )){
$indexSpec = trim ( $indexSpec , '()' );
$bits = explode ( ',' , $indexSpec );
$indexes = " \" " . implode ( " \" , \" " , $bits ) . " \" " ;
2009-09-16 05:51:38 +02:00
2010-03-18 04:16:30 +01:00
//One last check:
$existing = DB :: query ( " SELECT tablename FROM pg_indexes WHERE indexname=' " . strtolower ( $tableCol ) . " '; " ) -> first ();
if ( ! $existing )
return " create index $tableCol ON \" " . $tableName . " \" ( " . $indexes . " ); " ;
else
return false ;
2009-03-04 22:55:40 +01:00
} else {
2009-09-16 05:51:38 +02:00
2009-10-05 04:03:00 +02:00
//Arrays offer much more flexibility and many more options:
2009-09-16 05:51:38 +02:00
//Misc options first:
2009-10-05 04:03:00 +02:00
$fillfactor = $where = '' ;
2009-09-16 05:51:38 +02:00
if ( isset ( $indexSpec [ 'fillfactor' ]))
$fillfactor = 'WITH (FILLFACTOR = ' . $indexSpec [ 'fillfactor' ] . ')' ;
if ( isset ( $indexSpec [ 'where' ]))
$where = 'WHERE ' . $indexSpec [ 'where' ];
2009-11-02 02:58:51 +01:00
2010-11-01 01:53:57 +01:00
//Fix up the value entry to be quoted:
$value_bits = explode ( ',' , $indexSpec [ 'value' ]);
$new_values = Array ();
foreach ( $value_bits as $value ){
$new_values [] = " \" " . trim ( $value , ' "' ) . " \" " ;
}
$indexSpec [ 'value' ] = implode ( ',' , $new_values );
2010-03-18 04:16:30 +01:00
//One last check:
2010-11-01 01:53:57 +01:00
$existing = DB :: query ( " SELECT tablename FROM pg_indexes WHERE indexname=' " . strtolower ( $tableCol ) . " '; " );
if ( ! $existing -> first ()){
2010-03-18 04:16:30 +01:00
//create a type-specific index
//NOTE: hash should be removed. This is only here to demonstrate how other indexes can be made
switch ( $indexSpec [ 'type' ]){
case 'fulltext' :
$spec = " create index $tableCol ON \" " . $tableName . " \" USING " . $this -> default_fts_cluster_method . " ( \" ts_ " . $indexName . " \" ) $fillfactor $where " ;
break ;
case 'unique' :
2010-11-01 01:53:57 +01:00
$spec = " create unique index $tableCol ON \" " . $tableName . " \" ( " . $indexSpec [ 'value' ] . " ) $fillfactor $where " ;
2010-03-18 04:16:30 +01:00
break ;
case 'btree' :
2010-11-01 01:53:57 +01:00
$spec = " create index $tableCol ON \" " . $tableName . " \" USING btree ( " . $indexSpec [ 'value' ] . " ) $fillfactor $where " ;
2010-03-18 04:16:30 +01:00
break ;
case 'hash' :
//NOTE: this is not a recommended index type
2010-11-01 01:53:57 +01:00
$spec = " create index $tableCol ON \" " . $tableName . " \" USING hash ( " . $indexSpec [ 'value' ] . " ) $fillfactor $where " ;
2010-03-18 04:16:30 +01:00
break ;
case 'index' :
//'index' is the same as default, just a normal index with the default type decided by the database.
default :
2010-11-01 01:53:57 +01:00
$spec = " create index $tableCol ON \" " . $tableName . " \" ( " . $indexSpec [ 'value' ] . " ) $fillfactor $where " ;
2010-03-18 04:16:30 +01:00
}
return trim ( $spec ) . ';' ;
2010-11-01 01:53:57 +01:00
} else {
2010-03-18 04:16:30 +01:00
return false ;
2010-11-01 01:53:57 +01:00
}
2009-03-04 22:55:40 +01:00
}
} else {
$indexName = trim ( $indexName , '()' );
return $indexName ;
2008-11-23 02:20:39 +01:00
}
2009-03-04 22:55:40 +01:00
}
function getDbSqlDefinition ( $tableName , $indexName , $indexSpec ){
return $this -> getIndexSqlDefinition ( $tableName , $indexName , $indexSpec , true );
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 ;
}
if ( ! $indexType ) {
$indexType = " index " ;
}
$this -> query ( " DROP INDEX $indexName " );
$this -> query ( " ALTER TABLE \" $tableName\ " ADD $indexType \ " $indexName\ " $indexFields " );
}
/**
* Return the list of indexes in a table .
* @ param string $table The table name .
* @ return array
*/
public function indexList ( $table ) {
2009-03-04 22:55:40 +01:00
2009-02-13 03:46:54 +01:00
//Retrieve a list of indexes for the specified table
2009-03-04 22:55:40 +01:00
$indexes = DB :: query ( " SELECT tablename, indexname, indexdef FROM pg_indexes WHERE tablename=' $table '; " );
2008-11-23 02:20:39 +01:00
2009-09-16 05:51:38 +02:00
$indexList = Array ();
2009-03-04 22:55:40 +01:00
foreach ( $indexes as $index ) {
//We don't actually need the entire created command, just a few bits:
$prefix = '' ;
//Check for uniques:
if ( substr ( $index [ 'indexdef' ], 0 , 13 ) == 'CREATE UNIQUE' )
$prefix = 'unique ' ;
//check for hashes, btrees etc:
if ( strpos ( strtolower ( $index [ 'indexdef' ]), 'using hash ' ) !== false )
$prefix = 'using hash ' ;
//TODO: Fix me: btree is the default index type:
//if(strpos(strtolower($index['indexdef']), 'using btree ')!==false)
// $prefix='using btree ';
if ( strpos ( strtolower ( $index [ 'indexdef' ]), 'using rtree ' ) !== false )
$prefix = 'using rtree ' ;
$value = explode ( ' ' , substr ( $index [ 'indexdef' ], strpos ( $index [ 'indexdef' ], ' USING ' ) + 7 ));
if ( sizeof ( $value ) > 2 ){
for ( $i = 2 ; $i < sizeof ( $value ); $i ++ )
$value [ 1 ] .= $value [ $i ];
}
2009-09-17 02:10:30 +02:00
$key = substr ( $value [ 1 ], 0 , strpos ( $value [ 1 ], ')' ));
$key = trim ( trim ( str_replace ( " \" " , '' , $key ), '()' ));
2009-03-04 22:55:40 +01:00
$indexList [ $key ][ 'indexname' ] = $index [ 'indexname' ];
$indexList [ $key ][ 'spec' ] = $prefix . '(' . $key . ')' ;
}
2009-02-13 03:46:54 +01:00
2009-09-16 05:51:38 +02:00
return isset ( $indexList ) ? $indexList : null ;
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 () {
foreach ( $this -> query ( " SELECT tablename FROM pg_tables WHERE 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 ;
}
2009-09-16 05:51:38 +02:00
//Return an empty array if there's nothing in this database
return isset ( $tables ) ? $tables : Array ();
}
function TableExists ( $tableName ){
$result = $this -> query ( " SELECT tablename FROM pg_tables WHERE tablename=' $tableName '; " ) -> first ();
if ( $result )
return true ;
else
return false ;
2008-11-23 02:20:39 +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 .
*
* @ 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 ;
}
2009-10-08 03:19:15 +02:00
2010-11-01 02:11:31 +01:00
return self :: $cached_constraints [ $constraint ];
2009-10-08 03:19:15 +02: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 );
}
/**
* A function to return the field names and datatypes for the particular table
*/
public function tableDetails ( $tableName ){
$query = " SELECT a.attname as \" Column \" , pg_catalog.format_type(a.atttypid, a.atttypmod) as \" Datatype \" FROM pg_catalog.pg_attribute a WHERE a.attnum > 0 AND NOT a.attisdropped AND a.attrelid = ( SELECT c.oid 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)); " ;
$result = DB :: query ( $query );
$table = Array ();
while ( $row = pg_fetch_assoc ( $result )){
$table [] = Array ( 'Column' => $row [ 'Column' ], 'DataType' => $row [ 'DataType' ]);
}
return $table ;
}
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\ " ; " );
}
}
2010-11-11 20:13:52 +01:00
/**
* This will return the fields that the trigger is monitoring
* @ param string $trigger
*
* @ return array
*/
function triggerFieldsFromTrigger ( $trigger ){
if ( $trigger ){
$tsvector = 'tsvector_update_trigger' ;
$ts_pos = strpos ( $trigger , $tsvector );
$details = trim ( substr ( $trigger , $ts_pos + strlen ( $tsvector )), '();' );
//Now split this into bits:
$bits = explode ( ',' , $details );
$fields = $bits [ 2 ];
$field_bits = explode ( ',' , str_replace ( '"' , '' , $fields ));
$result = array ();
foreach ( $field_bits as $field_bit )
$result [] = trim ( $field_bit );
return $result ;
} else
return false ;
}
2008-11-23 02:20:39 +01:00
/**
* Return a boolean type - formatted string
*
* @ 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' ;
2008-11-23 02:20:39 +01:00
2009-10-05 04:03:00 +02:00
if ( ! isset ( $values [ 'arrayValue' ]))
$values [ 'arrayValue' ] = '' ;
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' ];
return " smallint { $values [ 'arrayValue' ] } " . $default ;
}
2008-11-23 02:20:39 +01:00
}
/**
* Return a date type - formatted string
*
* @ params array $values Contains a tokenised list of info about this data type
* @ return string
*/
public function date ( $values ){
2009-10-05 04:03:00 +02:00
if ( ! isset ( $values [ 'arrayValue' ]))
$values [ 'arrayValue' ] = '' ;
2009-09-29 22:59:45 +02:00
return " date { $values [ 'arrayValue' ] } " ;
2008-11-23 02:20:39 +01:00
}
/**
* Return a decimal type - formatted string
*
* @ 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 ){
2009-10-05 04:03:00 +02:00
if ( ! isset ( $values [ 'arrayValue' ]))
$values [ 'arrayValue' ] = '' ;
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' ];
}
2010-03-18 04:16:30 +01:00
$defaultValue = '' ;
if ( isset ( $values [ 'default' ]) && is_numeric ( $values [ 'default' ])) {
$defaultValue = ' default ' . $values [ 'default' ];
}
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
}
/**
* Return a enum type - formatted string
*
* @ 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' ] = '' ;
if ( $values [ 'arrayValue' ] != '' )
$default = '' ;
else
$default = " default ' { $values [ 'default' ] } ' " ;
return " varchar(255) { $values [ 'arrayValue' ] } " . $default . " check ( \" " . $values [ 'name' ] . " \" in (' " . implode ( '\', \'' , $values [ 'enums' ]) . " ')) " ;
2008-11-23 02:20:39 +01:00
}
/**
* Return a float type - formatted string
*
* @ 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' ] = '' ;
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
}
2010-02-04 05:29:37 +01:00
/**
* Return a float type - formatted string cause double is not supported
*
* @ 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 );
}
2008-11-23 02:20:39 +01:00
/**
* Return a int type - formatted string
*
* @ 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 ){
2009-10-05 04:03:00 +02:00
if ( ! isset ( $values [ 'arrayValue' ]))
$values [ 'arrayValue' ] = '' ;
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' ];
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
}
/**
* Return a datetime type - formatted string
* For PostgreSQL , we simply return the word 'timestamp' , no other parameters are necessary
*
* @ 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 ){
2009-10-05 04:03:00 +02:00
if ( ! isset ( $values [ 'arrayValue' ]))
$values [ 'arrayValue' ] = '' ;
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
}
/**
* Return a text type - formatted string
*
* @ 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 ){
2009-10-05 04:03:00 +02:00
if ( ! isset ( $values [ 'arrayValue' ]))
$values [ 'arrayValue' ] = '' ;
2008-11-23 02:20:39 +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
}
/**
* Return a time type - formatted string
*
* @ 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' ] = '' ;
2009-09-29 22:59:45 +02:00
return " time { $values [ 'arrayValue' ] } " ;
2008-11-23 02:20:39 +01:00
}
/**
* Return a varchar type - formatted string
*
* @ 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 ){
2009-10-05 04:03:00 +02:00
if ( ! isset ( $values [ 'arrayValue' ]))
$values [ 'arrayValue' ] = '' ;
2009-09-16 05:51:38 +02:00
if ( ! isset ( $values [ 'precision' ]))
$values [ 'precision' ] = 255 ;
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
}
/*
* 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 ){
2009-10-05 04:03:00 +02:00
if ( ! isset ( $values [ 'arrayValue' ]))
$values [ 'arrayValue' ] = '' ;
2009-02-17 04:57:21 +01:00
if ( $asDbValue )
2009-09-16 05:51:38 +02:00
return Array ( 'data_type' => 'numeric' , 'precision' => '4' );
2009-09-29 22:59:45 +02:00
else return " numeric(4) { $values [ 'arrayValue' ] } " ;
2008-11-23 02:20:39 +01:00
}
function escape_character ( $escape = false ){
if ( $escape )
return " \\ \" " ;
else
return " \" " ;
}
/**
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
*
* @ 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
$columns = explode ( ',' , $this_index [ 'value' ]);
for ( $i = 0 ; $i < sizeof ( $columns ); $i ++ )
$columns [ $i ] = " \" " . trim ( $columns [ $i ]) . " \" " ;
$columns = implode ( ', ' , $columns );
2010-11-11 20:13:52 +01:00
$fulltexts = " \" ts_ $name\ " tsvector " ;
2009-11-24 23:46:55 +01:00
$triggerName = " ts_ { $tableName } _ { $name } " ;
2008-11-23 02:20:39 +01:00
2009-11-24 23:46:55 +01:00
$this -> dropTrigger ( $triggerName , $tableName );
$triggers = " CREATE TRIGGER $triggerName BEFORE INSERT OR UPDATE
ON \ " $tableName\ " FOR EACH ROW EXECUTE PROCEDURE
tsvector_update_trigger ( \ " ts_ $name\ " , 'pg_catalog.english' , $columns ); " ;
2010-11-11 20:13:52 +01:00
return Array ( 'name' => $name , 'ts_name' => " ts_ { $name } " , 'fulltexts' => $fulltexts , 'triggers' => $triggers );
2009-11-24 23:46:55 +01:00
}
2008-11-24 00:29:09 +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
}
2008-11-24 00:29:09 +01:00
/**
* Returns true if this table exists
*/
function hasTable ( $tableName ) {
2010-06-03 07:02:11 +02:00
$result = $this -> query ( " SELECT tablename FROM pg_tables WHERE tablename = ' $tableName ' " );
if ( $result -> numRecords () > 0 ) return true ;
else return false ;
2008-11-24 00:29:09 +01:00
}
2009-03-12 03:48:48 +01:00
/**
* Returns the SQL command to get all the tables in this database
*/
function allTablesSQL (){
2009-10-01 23:01:23 +02:00
return " SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE'; " ;
2009-03-12 03:48:48 +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' ]);
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 ], " ' " ));
}
return $constraints ;
}
2009-03-12 03:48:48 +01:00
/**
* Because NOW () doesn ' t always work ...
* MSSQL , I ' m looking at you
*
*/
function now (){
return 'NOW()' ;
}
2009-09-16 05:51:38 +02:00
/*
* Returns the database - specific version of the random () function
*/
function random (){
return 'RANDOM()' ;
}
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'
);
if ( isset ( $values [ $type ]))
return $values [ $type ];
else return '' ;
}
2008-11-24 00:29:09 +01:00
/**
* Convert a SQLQuery object into a SQL statement
* @ todo There is a lot of duplication between this and MySQLDatabase :: sqlQueryToString () . Perhaps they could both call a common
* helper function in Database ?
*/
public function sqlQueryToString ( SQLQuery $sqlQuery ) {
if ( ! $sqlQuery -> from ) return '' ;
$distinct = $sqlQuery -> distinct ? " DISTINCT " : " " ;
if ( $sqlQuery -> delete ) {
$text = " DELETE " ;
} else if ( $sqlQuery -> select ) {
$text = " SELECT $distinct " . implode ( " , " , $sqlQuery -> select );
}
$text .= " FROM " . implode ( " " , $sqlQuery -> from );
if ( $sqlQuery -> where ) $text .= " WHERE ( " . $sqlQuery -> getFilter () . " ) " ;
if ( $sqlQuery -> groupby ) $text .= " GROUP BY " . implode ( " , " , $sqlQuery -> groupby );
if ( $sqlQuery -> having ) $text .= " HAVING ( " . implode ( " ) AND ( " , $sqlQuery -> having ) . " ) " ;
2010-02-04 05:29:37 +01:00
if ( $sqlQuery -> orderby ) $text .= " ORDER BY " . $this -> orderMoreSpecifically ( $sqlQuery -> select , $sqlQuery -> orderby );
2008-11-24 00:29:09 +01:00
if ( $sqlQuery -> limit ) {
$limit = $sqlQuery -> limit ;
2009-10-02 04:35:32 +02:00
2008-11-24 00:29:09 +01:00
// Pass limit as array or SQL string value
if ( is_array ( $limit )) {
2009-09-30 22:44:28 +02:00
2009-10-02 04:35:32 +02:00
if ( isset ( $limit [ 'start' ]) && $limit [ 'start' ] != '' )
2009-09-30 22:44:28 +02:00
$text .= ' OFFSET ' . $limit [ 'start' ];
2009-10-02 04:35:32 +02:00
if ( isset ( $limit [ 'limit' ]) && $limit [ 'limit' ] != '' )
2009-09-30 22:44:28 +02:00
$text .= ' LIMIT ' . $limit [ 'limit' ];
2008-11-24 00:29:09 +01:00
} else {
2009-09-29 22:59:45 +02:00
if ( strpos ( $sqlQuery -> limit , ',' )){
$limit = str_replace ( ',' , ' LIMIT ' , $sqlQuery -> limit );
$text .= ' OFFSET ' . $limit ;
} else {
$text .= ' LIMIT ' . $sqlQuery -> limit ;
}
2008-11-24 00:29:09 +01:00
}
}
return $text ;
}
2009-03-12 03:48:48 +01:00
2010-02-04 05:29:37 +01:00
protected function orderMoreSpecifically ( $select , $order ) {
$altered = false ;
// split expression into order terms
$terms = explode ( ',' , $order );
foreach ( $terms as $i => $term ) {
$term = trim ( $term );
// check if table is unspecified
if ( ! preg_match ( '/\./' , $term )) {
$direction = '' ;
if ( preg_match ( '/( ASC)$|( DESC)$/i' , $term )) list ( $term , $direction ) = explode ( ' ' , $term );
// find a match in the SELECT array and replace
foreach ( $select as $s ) {
if ( preg_match ( '/"[a-z0-9_]+"\.[\'"]' . $term . '[\'"]/i' , trim ( $s ))) {
$terms [ $i ] = $s . ' ' . $direction ;
$altered = true ;
break ;
}
}
}
}
return implode ( ',' , $terms );
}
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 ){
return pg_escape_string ( $value );
}
2009-04-06 01:20:09 +02:00
/*
* This changes the index name depending on database requirements .
*/
function modifyIndex ( $index , $spec ){
if ( is_array ( $spec ) && $spec [ 'type' ] == 'fulltext' )
return 'ts_' . str_replace ( ',' , '_' , $index );
else
return str_replace ( '_' , ',' , $index );
}
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 .
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 ) {
2009-09-16 05:51:38 +02:00
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 );
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 );
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' );
2009-09-16 05:51:38 +02:00
$tables = Array ();
2009-10-05 22:26:41 +02:00
// Make column selection lists
$select = array (
'SiteTree' => array ( " \" ClassName \" " , " \" SiteTree \" . \" ID \" " , " \" ParentID \" " , " \" Title \" " , " \" URLSegment \" " , " \" Content \" " , " \" LastEdited \" " , " \" Created \" " , " NULL AS \" Filename \" " , " NULL AS \" Name \" " , " \" CanViewType \" " ),
'File' => array ( " \" ClassName \" " , " \" File \" . \" ID \" " , " NULL AS \" ParentID \" " , " \" Title \" " , " NULL AS \" URLSegment \" " , " \" Content \" " , " \" LastEdited \" " , " \" Created \" " , " \" Filename \" " , " \" Name \" " , " NULL AS \" CanViewType \" " ),
);
2009-10-05 04:03:00 +02:00
2009-09-16 05:51:38 +02:00
foreach ( $result as $row ){
2009-10-05 22:26:41 +02:00
if ( $row [ 'table_name' ] == 'SiteTree' )
2009-09-30 05:50:27 +02:00
$showInSearch = " AND \" ShowInSearch \" =1 " ;
2009-10-05 22:26:41 +02:00
else
2009-09-30 05:50:27 +02:00
$showInSearch = '' ;
2009-10-05 22:26:41 +02:00
//public function extendedSQL($filter = "", $sort = "", $limit = "", $join = "", $having = ""){
2010-11-11 20:13:52 +01:00
$query = singleton ( $row [ 'table_name' ]) -> extendedSql ( " \" " . $row [ 'table_name' ] . " \" . \" " . $row [ 'column_name' ] . " \" " . $this -> default_fts_search_method . ' q ' . $showInSearch , '' );
2009-10-05 22:26:41 +02:00
$query -> select = $select [ $row [ 'table_name' ]];
$query -> from [ 'tsearch' ] = " , to_tsquery('english', ' $keywords ') AS q " ;
2010-11-11 20:13:52 +01:00
$query -> select [] = " ts_rank( \" { $row [ 'table_name' ] } \" . \" { $row [ 'column_name' ] } \" , q) AS \" Relevance \" " ;
2009-10-05 22:26:41 +02:00
$query -> orderby = null ;
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
}
2009-10-05 22:26:41 +02:00
2009-09-16 05:51:38 +02:00
$doSet = new DataObjectSet ();
2009-09-30 05:50:27 +02:00
$limit = $pageLength ;
$offset = $start ;
if ( $keywords )
$orderBy = " ORDER BY $sortBy " ;
else $orderBy = '' ;
$fullQuery = " SELECT * FROM ( " . implode ( " UNION " , $tables ) . " ) AS q1 $orderBy LIMIT $limit OFFSET $offset " ;
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
}
if ( isset ( $objects )) $doSet = new DataObjectSet ( $objects );
else $doSet = new DataObjectSet ();
$doSet -> setPageLimits ( $start , $pageLength , $totalCount );
return $doSet ;
2009-10-05 22:26:41 +02:00
2009-09-16 05:51:38 +02:00
}
2009-10-01 23:01:23 +02:00
/*
* Does this database support transactions ?
*/
public function supportsTransactions (){
return $this -> supportsTransactions ;
}
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 ;
}
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
*/
public function startTransaction ( $transaction_mode = false , $session_characteristics = false ){
DB :: query ( 'BEGIN;' );
if ( $transaction_mode )
DB :: query ( 'SET TRANSACTION ' . $transaction_mode . ';' );
if ( $session_characteristics )
DB :: query ( 'SET SESSION CHARACTERISTICS AS TRANSACTION ' . $session_characteristics . ';' );
}
/*
* Create a savepoint that you can jump back to if you encounter problems
*/
public function transactionSavepoint ( $savepoint ){
DB :: query ( " SAVEPOINT $savepoint ; " );
}
/*
* 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 ){
if ( $savepoint )
DB :: query ( " ROLLBACK TO $savepoint ; " );
else
DB :: query ( 'ROLLBACK;' );
}
/*
* Commit everything inside this transaction so far
*/
public function endTransaction (){
DB :: query ( 'COMMIT;' );
}
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 ();
//NOTE: this location must be empty for this to work
//We can't seem to change the location of the tablespace through any ALTER commands :(
//If a tablespace with this name exists, but the location has changed, then drop the current one
2009-11-24 23:46:55 +01:00
//if($existing && $location!=$existing['spclocation'])
// DB::query("DROP TABLESPACE $name;");
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 '; " );
}
2009-11-24 23:46:55 +01:00
public function createOrReplacePartition ( $tableName , $partitions , $indexes , $extensions ){
//We need the plpgsql language to be installed for this to work:
$this -> createLanguage ( 'plpgsql' );
2009-10-08 03:19:15 +02:00
$trigger = 'CREATE OR REPLACE FUNCTION ' . $tableName . '_insert_trigger() RETURNS TRIGGER AS $$ BEGIN ' ;
$first = true ;
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' ];
} else
$tableSpace = '' ;
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 );
}
2009-11-24 23:46:55 +01:00
DB :: query ( " ALTER TABLE \" $partition_name\ " ADD CONSTRAINT \ " { $partition_name } _pkey \" PRIMARY KEY ( \" ID \" ); " );
2009-10-08 03:19:15 +02:00
if ( $first ){
$trigger .= 'IF' ;
$first = false ;
} else
$trigger .= 'ELSIF' ;
2009-11-24 23:46:55 +01:00
$trigger .= " ( $partition_value ) THEN INSERT INTO \" $partition_name\ " VALUES ( NEW .* ); " ;
if ( $indexes ){
// We need to propogate the indexes through to the child pages.
// Some of this code is duplicated, and could be tidied up
foreach ( $indexes as $name => $this_index ){
if ( $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' ];
DB :: query ( " CREATE INDEX \" ix_ { $partition_name } _ { $this_index [ 'name' ] } \" ON \" " . $partition_name . " \" USING " . $this -> default_fts_cluster_method . " ( \" ts_ " . $name . " \" ) $fillfactor $where " );
$ts_details = $this -> fulltext ( $this_index , $partition_name , $name );
DB :: query ( $ts_details [ 'triggers' ]);
} else {
if ( is_array ( $this_index ))
$index_name = $this_index [ 'name' ];
else $index_name = trim ( $this_index , '()' );
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
}
}
}
//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
}
$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();' ;
DB :: query ( $trigger );
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 ();
if ( ! $result ){
DB :: query ( " CREATE LANGUAGE $language ; " );
}
2009-10-08 03:19:15 +02: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 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 )) " ;
2010-02-04 05:29:37 +01:00
return " to_char( $date , TEXT ' $format ') " ;
}
/**
* Function to return an SQL datetime expression that can be used with Postgres
* used for querying a datetime addition
* @ param string $date , can be either 'now' , literal datetime like '1973-10-14 10:30:00' or field name , e . g . '"SiteTree"."Created"'
* @ param string $interval to be added , use the format [ sign ][ integer ] [ qualifier ], e . g . - 1 Day , + 15 minutes , + 1 YEAR
* supported qualifiers :
* - years
* - months
* - days
* - hours
* - minutes
* - seconds
* This includes the singular forms as well
* @ return string SQL datetime expression to query for a datetime ( YYYY - MM - DD hh : mm : ss ) which is the result of the addition
*/
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
}
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 .
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
}
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 ;
/**
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 ;
}
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
}
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
}
public function numRecords () {
return pg_num_rows ( $this -> handle );
}
public function nextRecord () {
// Coalesce rather than replace common fields.
if ( $data = pg_fetch_row ( $this -> handle )) {
foreach ( $data as $columnIdx => $value ) {
$columnName = pg_field_name ( $this -> handle , $columnIdx );
// $value || !$ouput[$columnName] means that the *last* occurring value is shown
// !$ouput[$columnName] means that the *first* occurring value is shown
if ( isset ( $value ) || ! isset ( $output [ $columnName ])) {
$output [ $columnName ] = $value ;
}
}
return $output ;
} else {
return false ;
}
}
}
2010-11-01 02:11:31 +01:00
?>