MediaWiki  REL1_21
DatabasePostgres.php
Go to the documentation of this file.
00001 <?php
00024 class PostgresField implements Field {
00025         private $name, $tablename, $type, $nullable, $max_length, $deferred, $deferrable, $conname,
00026                 $has_default, $default;
00027 
00034         static function fromText( $db, $table, $field ) {
00035                 $q = <<<SQL
00036 SELECT
00037  attnotnull, attlen, conname AS conname,
00038  atthasdef,
00039  adsrc,
00040  COALESCE(condeferred, 'f') AS deferred,
00041  COALESCE(condeferrable, 'f') AS deferrable,
00042  CASE WHEN typname = 'int2' THEN 'smallint'
00043   WHEN typname = 'int4' THEN 'integer'
00044   WHEN typname = 'int8' THEN 'bigint'
00045   WHEN typname = 'bpchar' THEN 'char'
00046  ELSE typname END AS typname
00047 FROM pg_class c
00048 JOIN pg_namespace n ON (n.oid = c.relnamespace)
00049 JOIN pg_attribute a ON (a.attrelid = c.oid)
00050 JOIN pg_type t ON (t.oid = a.atttypid)
00051 LEFT JOIN pg_constraint o ON (o.conrelid = c.oid AND a.attnum = ANY(o.conkey) AND o.contype = 'f')
00052 LEFT JOIN pg_attrdef d on c.oid=d.adrelid and a.attnum=d.adnum
00053 WHERE relkind = 'r'
00054 AND nspname=%s
00055 AND relname=%s
00056 AND attname=%s;
00057 SQL;
00058 
00059                 $table = $db->tableName( $table, 'raw' );
00060                 $res = $db->query(
00061                         sprintf( $q,
00062                                 $db->addQuotes( $db->getCoreSchema() ),
00063                                 $db->addQuotes( $table ),
00064                                 $db->addQuotes( $field )
00065                         )
00066                 );
00067                 $row = $db->fetchObject( $res );
00068                 if ( !$row ) {
00069                         return null;
00070                 }
00071                 $n = new PostgresField;
00072                 $n->type = $row->typname;
00073                 $n->nullable = ( $row->attnotnull == 'f' );
00074                 $n->name = $field;
00075                 $n->tablename = $table;
00076                 $n->max_length = $row->attlen;
00077                 $n->deferrable = ( $row->deferrable == 't' );
00078                 $n->deferred = ( $row->deferred == 't' );
00079                 $n->conname = $row->conname;
00080                 $n->has_default = ( $row->atthasdef === 't' );
00081                 $n->default = $row->adsrc;
00082                 return $n;
00083         }
00084 
00085         function name() {
00086                 return $this->name;
00087         }
00088 
00089         function tableName() {
00090                 return $this->tablename;
00091         }
00092 
00093         function type() {
00094                 return $this->type;
00095         }
00096 
00097         function isNullable() {
00098                 return $this->nullable;
00099         }
00100 
00101         function maxLength() {
00102                 return $this->max_length;
00103         }
00104 
00105         function is_deferrable() {
00106                 return $this->deferrable;
00107         }
00108 
00109         function is_deferred() {
00110                 return $this->deferred;
00111         }
00112 
00113         function conname() {
00114                 return $this->conname;
00115         }
00119         function defaultValue() {
00120                 if( $this->has_default ) {
00121                         return $this->default;
00122                 } else {
00123                         return false;
00124                 }
00125         }
00126 
00127 }
00128 
00136 class PostgresTransactionState {
00137 
00138         static $WATCHED = array(
00139                 array(
00140                         "desc" => "%s: Connection state changed from %s -> %s\n",
00141                         "states" => array(
00142                                 PGSQL_CONNECTION_OK       => "OK",
00143                                 PGSQL_CONNECTION_BAD      => "BAD"
00144                         )
00145                 ),
00146                 array(
00147                         "desc" => "%s: Transaction state changed from %s -> %s\n",
00148                         "states" => array(
00149                                 PGSQL_TRANSACTION_IDLE    => "IDLE",
00150                                 PGSQL_TRANSACTION_ACTIVE  => "ACTIVE",
00151                                 PGSQL_TRANSACTION_INTRANS => "TRANS",
00152                                 PGSQL_TRANSACTION_INERROR => "ERROR",
00153                                 PGSQL_TRANSACTION_UNKNOWN => "UNKNOWN"
00154                         )
00155                 )
00156         );
00157 
00158         public function __construct( $conn ) {
00159                 $this->mConn = $conn;
00160                 $this->update();
00161                 $this->mCurrentState = $this->mNewState;
00162         }
00163 
00164         public function update() {
00165                 $this->mNewState = array(
00166                         pg_connection_status( $this->mConn ),
00167                         pg_transaction_status( $this->mConn )
00168                 );
00169         }
00170 
00171         public function check() {
00172                 global $wgDebugDBTransactions;
00173                 $this->update();
00174                 if ( $wgDebugDBTransactions ) {
00175                         if ( $this->mCurrentState !== $this->mNewState ) {
00176                                 $old = reset( $this->mCurrentState );
00177                                 $new = reset( $this->mNewState );
00178                                 foreach ( self::$WATCHED as $watched ) {
00179                                         if ( $old !== $new ) {
00180                                                 $this->log_changed( $old, $new, $watched );
00181                                         }
00182                                         $old = next( $this->mCurrentState );
00183                                         $new = next( $this->mNewState );
00184 
00185                                 }
00186                         }
00187                 }
00188                 $this->mCurrentState = $this->mNewState;
00189         }
00190 
00191         protected function describe_changed( $status, $desc_table ) {
00192                 if( isset( $desc_table[$status] ) ) {
00193                         return $desc_table[$status];
00194                 } else {
00195                         return "STATUS " . $status;
00196                 }
00197         }
00198 
00199         protected function log_changed( $old, $new, $watched ) {
00200                 wfDebug( sprintf( $watched["desc"],
00201                         $this->mConn,
00202                         $this->describe_changed( $old, $watched["states"] ),
00203                         $this->describe_changed( $new, $watched["states"] )
00204                 ) );
00205         }
00206 }
00207 
00213 class SavepointPostgres {
00217         protected $dbw;
00218         protected $id;
00219         protected $didbegin;
00220 
00221         public function __construct ( $dbw, $id ) {
00222                 $this->dbw = $dbw;
00223                 $this->id = $id;
00224                 $this->didbegin = false;
00225                 /* If we are not in a transaction, we need to be for savepoint trickery */
00226                 if ( !$dbw->trxLevel() ) {
00227                                 $dbw->begin( "FOR SAVEPOINT" );
00228                                 $this->didbegin = true;
00229                 }
00230         }
00231 
00232         public function __destruct() {
00233                 if ( $this->didbegin ) {
00234                         $this->dbw->rollback();
00235                         $this->didbegin = false;
00236                 }
00237         }
00238 
00239         public function commit() {
00240                 if ( $this->didbegin ) {
00241                         $this->dbw->commit();
00242                         $this->didbegin = false;
00243                 }
00244         }
00245 
00246         protected function query( $keyword, $msg_ok, $msg_failed ) {
00247                 global $wgDebugDBTransactions;
00248                 if ( $this->dbw->doQuery( $keyword . " " . $this->id ) !== false ) {
00249                         if ( $wgDebugDBTransactions ) {
00250                                 wfDebug( sprintf ( $msg_ok, $this->id ) );
00251                         }
00252                 } else {
00253                         wfDebug( sprintf ( $msg_failed, $this->id ) );
00254                 }
00255         }
00256 
00257         public function savepoint() {
00258                 $this->query( "SAVEPOINT",
00259                         "Transaction state: savepoint \"%s\" established.\n",
00260                         "Transaction state: establishment of savepoint \"%s\" FAILED.\n"
00261                 );
00262         }
00263 
00264         public function release() {
00265                 $this->query( "RELEASE",
00266                         "Transaction state: savepoint \"%s\" released.\n",
00267                         "Transaction state: release of savepoint \"%s\" FAILED.\n"
00268                 );
00269         }
00270 
00271         public function rollback() {
00272                 $this->query( "ROLLBACK TO",
00273                         "Transaction state: savepoint \"%s\" rolled back.\n",
00274                         "Transaction state: rollback of savepoint \"%s\" FAILED.\n"
00275                 );
00276         }
00277 
00278         public function __toString() {
00279                 return (string)$this->id;
00280         }
00281 }
00282 
00286 class DatabasePostgres extends DatabaseBase {
00287         var $mInsertId = null;
00288         var $mLastResult = null;
00289         var $numeric_version = null;
00290         var $mAffectedRows = null;
00291 
00292         function getType() {
00293                 return 'postgres';
00294         }
00295 
00296         function cascadingDeletes() {
00297                 return true;
00298         }
00299         function cleanupTriggers() {
00300                 return true;
00301         }
00302         function strictIPs() {
00303                 return true;
00304         }
00305         function realTimestamps() {
00306                 return true;
00307         }
00308         function implicitGroupby() {
00309                 return false;
00310         }
00311         function implicitOrderby() {
00312                 return false;
00313         }
00314         function searchableIPs() {
00315                 return true;
00316         }
00317         function functionalIndexes() {
00318                 return true;
00319         }
00320 
00321         function hasConstraint( $name ) {
00322                 $SQL = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n WHERE c.connamespace = n.oid AND conname = '" .
00323                                 pg_escape_string( $this->mConn, $name ) . "' AND n.nspname = '" . pg_escape_string( $this->mConn, $this->getCoreSchema() ) ."'";
00324                 $res = $this->doQuery( $SQL );
00325                 return $this->numRows( $res );
00326         }
00327 
00337         function open( $server, $user, $password, $dbName ) {
00338                 # Test for Postgres support, to avoid suppressed fatal error
00339                 if ( !function_exists( 'pg_connect' ) ) {
00340                         throw new DBConnectionError( $this, "Postgres functions missing, have you compiled PHP with the --with-pgsql option?\n (Note: if you recently installed PHP, you may need to restart your webserver and database)\n" );
00341                 }
00342 
00343                 global $wgDBport;
00344 
00345                 if ( !strlen( $user ) ) { # e.g. the class is being loaded
00346                         return;
00347                 }
00348 
00349                 $this->mServer = $server;
00350                 $port = $wgDBport;
00351                 $this->mUser = $user;
00352                 $this->mPassword = $password;
00353                 $this->mDBname = $dbName;
00354 
00355                 $connectVars = array(
00356                         'dbname' => $dbName,
00357                         'user' => $user,
00358                         'password' => $password
00359                 );
00360                 if ( $server != false && $server != '' ) {
00361                         $connectVars['host'] = $server;
00362                 }
00363                 if ( $port != false && $port != '' ) {
00364                         $connectVars['port'] = $port;
00365                 }
00366                 if ( $this->mFlags & DBO_SSL ) {
00367                         $connectVars['sslmode'] = 1;
00368                 }
00369 
00370                 $this->connectString = $this->makeConnectionString( $connectVars, PGSQL_CONNECT_FORCE_NEW );
00371                 $this->close();
00372                 $this->installErrorHandler();
00373                 $this->mConn = pg_connect( $this->connectString );
00374                 $phpError = $this->restoreErrorHandler();
00375 
00376                 if ( !$this->mConn ) {
00377                         wfDebug( "DB connection error\n" );
00378                         wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" );
00379                         wfDebug( $this->lastError() . "\n" );
00380                         throw new DBConnectionError( $this, str_replace( "\n", ' ', $phpError ) );
00381                 }
00382 
00383                 $this->mOpened = true;
00384                 $this->mTransactionState = new PostgresTransactionState( $this->mConn );
00385 
00386                 global $wgCommandLineMode;
00387                 # If called from the command-line (e.g. importDump), only show errors
00388                 if ( $wgCommandLineMode ) {
00389                         $this->doQuery( "SET client_min_messages = 'ERROR'" );
00390                 }
00391 
00392                 $this->query( "SET client_encoding='UTF8'", __METHOD__ );
00393                 $this->query( "SET datestyle = 'ISO, YMD'", __METHOD__ );
00394                 $this->query( "SET timezone = 'GMT'", __METHOD__ );
00395                 $this->query( "SET standard_conforming_strings = on", __METHOD__ );
00396                 if ( $this->getServerVersion() >= 9.0 ) {
00397                         $this->query( "SET bytea_output = 'escape'", __METHOD__ ); // PHP bug 53127
00398                 }
00399 
00400                 global $wgDBmwschema;
00401                 $this->determineCoreSchema( $wgDBmwschema );
00402 
00403                 return $this->mConn;
00404         }
00405 
00411         function selectDB( $db ) {
00412                 if ( $this->mDBname !== $db ) {
00413                         return (bool)$this->open( $this->mServer, $this->mUser, $this->mPassword, $db );
00414                 } else {
00415                         return true;
00416                 }
00417         }
00418 
00419         function makeConnectionString( $vars ) {
00420                 $s = '';
00421                 foreach ( $vars as $name => $value ) {
00422                         $s .= "$name='" . str_replace( "'", "\\'", $value ) . "' ";
00423                 }
00424                 return $s;
00425         }
00426 
00432         protected function closeConnection() {
00433                 return pg_close( $this->mConn );
00434         }
00435 
00436         public function doQuery( $sql ) {
00437                 if ( function_exists( 'mb_convert_encoding' ) ) {
00438                         $sql = mb_convert_encoding( $sql, 'UTF-8' );
00439                 }
00440                 $this->mTransactionState->check();
00441                 if( pg_send_query( $this->mConn, $sql ) === false ) {
00442                         throw new DBUnexpectedError( $this, "Unable to post new query to PostgreSQL\n" );
00443                 }
00444                 $this->mLastResult = pg_get_result( $this->mConn );
00445                 $this->mTransactionState->check();
00446                 $this->mAffectedRows = null;
00447                 if ( pg_result_error( $this->mLastResult ) ) {
00448                         return false;
00449                 }
00450                 return $this->mLastResult;
00451         }
00452 
00453         protected function dumpError () {
00454                 $diags = array( PGSQL_DIAG_SEVERITY,
00455                                 PGSQL_DIAG_SQLSTATE,
00456                                 PGSQL_DIAG_MESSAGE_PRIMARY,
00457                                 PGSQL_DIAG_MESSAGE_DETAIL,
00458                                 PGSQL_DIAG_MESSAGE_HINT,
00459                                 PGSQL_DIAG_STATEMENT_POSITION,
00460                                 PGSQL_DIAG_INTERNAL_POSITION,
00461                                 PGSQL_DIAG_INTERNAL_QUERY,
00462                                 PGSQL_DIAG_CONTEXT,
00463                                 PGSQL_DIAG_SOURCE_FILE,
00464                                 PGSQL_DIAG_SOURCE_LINE,
00465                                 PGSQL_DIAG_SOURCE_FUNCTION );
00466                 foreach ( $diags as $d ) {
00467                         wfDebug( sprintf("PgSQL ERROR(%d): %s\n", $d, pg_result_error_field( $this->mLastResult, $d ) ) );
00468                 }
00469         }
00470 
00471         function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
00472                 /* Transaction stays in the ERROR state until rolledback */
00473                 if ( $tempIgnore ) {
00474                         /* Check for constraint violation */
00475                         if ( $errno === '23505' ) {
00476                                 parent::reportQueryError( $error, $errno, $sql, $fname, $tempIgnore );
00477                                 return;
00478                         }
00479                 }
00480                 /* Don't ignore serious errors */
00481                 $this->rollback( __METHOD__ );
00482                 parent::reportQueryError( $error, $errno, $sql, $fname, false );
00483         }
00484 
00485         function queryIgnore( $sql, $fname = 'DatabasePostgres::queryIgnore' ) {
00486                 return $this->query( $sql, $fname, true );
00487         }
00488 
00489         function freeResult( $res ) {
00490                 if ( $res instanceof ResultWrapper ) {
00491                         $res = $res->result;
00492                 }
00493                 wfSuppressWarnings();
00494                 $ok = pg_free_result( $res );
00495                 wfRestoreWarnings();
00496                 if ( !$ok ) {
00497                         throw new DBUnexpectedError( $this, "Unable to free Postgres result\n" );
00498                 }
00499         }
00500 
00501         function fetchObject( $res ) {
00502                 if ( $res instanceof ResultWrapper ) {
00503                         $res = $res->result;
00504                 }
00505                 wfSuppressWarnings();
00506                 $row = pg_fetch_object( $res );
00507                 wfRestoreWarnings();
00508                 # @todo FIXME: HACK HACK HACK HACK debug
00509 
00510                 # @todo hashar: not sure if the following test really trigger if the object
00511                 #          fetching failed.
00512                 if( pg_last_error( $this->mConn ) ) {
00513                         throw new DBUnexpectedError( $this, 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) );
00514                 }
00515                 return $row;
00516         }
00517 
00518         function fetchRow( $res ) {
00519                 if ( $res instanceof ResultWrapper ) {
00520                         $res = $res->result;
00521                 }
00522                 wfSuppressWarnings();
00523                 $row = pg_fetch_array( $res );
00524                 wfRestoreWarnings();
00525                 if( pg_last_error( $this->mConn ) ) {
00526                         throw new DBUnexpectedError( $this, 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) );
00527                 }
00528                 return $row;
00529         }
00530 
00531         function numRows( $res ) {
00532                 if ( $res instanceof ResultWrapper ) {
00533                         $res = $res->result;
00534                 }
00535                 wfSuppressWarnings();
00536                 $n = pg_num_rows( $res );
00537                 wfRestoreWarnings();
00538                 if( pg_last_error( $this->mConn ) ) {
00539                         throw new DBUnexpectedError( $this, 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) );
00540                 }
00541                 return $n;
00542         }
00543 
00544         function numFields( $res ) {
00545                 if ( $res instanceof ResultWrapper ) {
00546                         $res = $res->result;
00547                 }
00548                 return pg_num_fields( $res );
00549         }
00550 
00551         function fieldName( $res, $n ) {
00552                 if ( $res instanceof ResultWrapper ) {
00553                         $res = $res->result;
00554                 }
00555                 return pg_field_name( $res, $n );
00556         }
00557 
00562         function insertId() {
00563                 return $this->mInsertId;
00564         }
00565 
00566         function dataSeek( $res, $row ) {
00567                 if ( $res instanceof ResultWrapper ) {
00568                         $res = $res->result;
00569                 }
00570                 return pg_result_seek( $res, $row );
00571         }
00572 
00573         function lastError() {
00574                 if ( $this->mConn ) {
00575                         if ( $this->mLastResult ) {
00576                                 return pg_result_error( $this->mLastResult );
00577                         } else {
00578                                 return pg_last_error();
00579                         }
00580                 } else {
00581                         return 'No database connection';
00582                 }
00583         }
00584         function lastErrno() {
00585                 if ( $this->mLastResult ) {
00586                         return pg_result_error_field( $this->mLastResult, PGSQL_DIAG_SQLSTATE );
00587                 } else {
00588                         return false;
00589                 }
00590         }
00591 
00592         function affectedRows() {
00593                 if ( !is_null( $this->mAffectedRows ) ) {
00594                         // Forced result for simulated queries
00595                         return $this->mAffectedRows;
00596                 }
00597                 if( empty( $this->mLastResult ) ) {
00598                         return 0;
00599                 }
00600                 return pg_affected_rows( $this->mLastResult );
00601         }
00602 
00611         function estimateRowCount( $table, $vars = '*', $conds = '', $fname = 'DatabasePostgres::estimateRowCount', $options = array() ) {
00612                 $options['EXPLAIN'] = true;
00613                 $res = $this->select( $table, $vars, $conds, $fname, $options );
00614                 $rows = -1;
00615                 if ( $res ) {
00616                         $row = $this->fetchRow( $res );
00617                         $count = array();
00618                         if( preg_match( '/rows=(\d+)/', $row[0], $count ) ) {
00619                                 $rows = $count[1];
00620                         }
00621                 }
00622                 return $rows;
00623         }
00624 
00630         function indexInfo( $table, $index, $fname = 'DatabasePostgres::indexInfo' ) {
00631                 $sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'";
00632                 $res = $this->query( $sql, $fname );
00633                 if ( !$res ) {
00634                         return null;
00635                 }
00636                 foreach ( $res as $row ) {
00637                         if ( $row->indexname == $this->indexName( $index ) ) {
00638                                 return $row;
00639                         }
00640                 }
00641                 return false;
00642         }
00643 
00650         function indexAttributes ( $index, $schema = false ) {
00651                 if ( $schema === false )
00652                         $schema = $this->getCoreSchema();
00653                 /*
00654                  * A subquery would be not needed if we didn't care about the order
00655                  * of attributes, but we do
00656                  */
00657                 $sql = <<<__INDEXATTR__
00658 
00659                         SELECT opcname,
00660                                 attname,
00661                                 i.indoption[s.g] as option,
00662                                 pg_am.amname
00663                         FROM
00664                                 (SELECT generate_series(array_lower(isub.indkey,1), array_upper(isub.indkey,1)) AS g
00665                                         FROM
00666                                                 pg_index isub
00667                                         JOIN pg_class cis
00668                                                 ON cis.oid=isub.indexrelid
00669                                         JOIN pg_namespace ns
00670                                                 ON cis.relnamespace = ns.oid
00671                                         WHERE cis.relname='$index' AND ns.nspname='$schema') AS s,
00672                                 pg_attribute,
00673                                 pg_opclass opcls,
00674                                 pg_am,
00675                                 pg_class ci
00676                                 JOIN pg_index i
00677                                         ON ci.oid=i.indexrelid
00678                                 JOIN pg_class ct
00679                                         ON ct.oid = i.indrelid
00680                                 JOIN pg_namespace n
00681                                         ON ci.relnamespace = n.oid
00682                                 WHERE
00683                                         ci.relname='$index' AND n.nspname='$schema'
00684                                         AND     attrelid = ct.oid
00685                                         AND     i.indkey[s.g] = attnum
00686                                         AND     i.indclass[s.g] = opcls.oid
00687                                         AND     pg_am.oid = opcls.opcmethod
00688 __INDEXATTR__;
00689                 $res = $this->query( $sql, __METHOD__ );
00690                 $a = array();
00691                 if ( $res ) {
00692                         foreach ( $res as $row ) {
00693                                 $a[] = array(
00694                                         $row->attname,
00695                                         $row->opcname,
00696                                         $row->amname,
00697                                         $row->option );
00698                         }
00699                 } else {
00700                         return null;
00701                 }
00702                 return $a;
00703         }
00704 
00705         function indexUnique( $table, $index, $fname = 'DatabasePostgres::indexUnique' ) {
00706                 $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'".
00707                         " AND indexdef LIKE 'CREATE UNIQUE%(" .
00708                         $this->strencode( $this->indexName( $index ) ) .
00709                         ")'";
00710                 $res = $this->query( $sql, $fname );
00711                 if ( !$res ) {
00712                         return null;
00713                 }
00714                 foreach ( $res as $row ) {
00715                         return true;
00716                 }
00717                 return false;
00718         }
00719 
00733         function insert( $table, $args, $fname = 'DatabasePostgres::insert', $options = array() ) {
00734                 if ( !count( $args ) ) {
00735                         return true;
00736                 }
00737 
00738                 $table = $this->tableName( $table );
00739                 if ( !isset( $this->numeric_version ) ) {
00740                         $this->getServerVersion();
00741                 }
00742 
00743                 if ( !is_array( $options ) ) {
00744                         $options = array( $options );
00745                 }
00746 
00747                 if ( isset( $args[0] ) && is_array( $args[0] ) ) {
00748                         $multi = true;
00749                         $keys = array_keys( $args[0] );
00750                 } else {
00751                         $multi = false;
00752                         $keys = array_keys( $args );
00753                 }
00754 
00755                 // If IGNORE is set, we use savepoints to emulate mysql's behavior
00756                 $savepoint = null;
00757                 if ( in_array( 'IGNORE', $options ) ) {
00758                         $savepoint = new SavepointPostgres( $this, 'mw' );
00759                         $olde = error_reporting( 0 );
00760                         // For future use, we may want to track the number of actual inserts
00761                         // Right now, insert (all writes) simply return true/false
00762                         $numrowsinserted = 0;
00763                 }
00764 
00765                 $sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES ';
00766 
00767                 if ( $multi ) {
00768                         if ( $this->numeric_version >= 8.2 && !$savepoint ) {
00769                                 $first = true;
00770                                 foreach ( $args as $row ) {
00771                                         if ( $first ) {
00772                                                 $first = false;
00773                                         } else {
00774                                                 $sql .= ',';
00775                                         }
00776                                         $sql .= '(' . $this->makeList( $row ) . ')';
00777                                 }
00778                                 $res = (bool)$this->query( $sql, $fname, $savepoint );
00779                         } else {
00780                                 $res = true;
00781                                 $origsql = $sql;
00782                                 foreach ( $args as $row ) {
00783                                         $tempsql = $origsql;
00784                                         $tempsql .= '(' . $this->makeList( $row ) . ')';
00785 
00786                                         if ( $savepoint ) {
00787                                                 $savepoint->savepoint();
00788                                         }
00789 
00790                                         $tempres = (bool)$this->query( $tempsql, $fname, $savepoint );
00791 
00792                                         if ( $savepoint ) {
00793                                                 $bar = pg_last_error();
00794                                                 if ( $bar != false ) {
00795                                                         $savepoint->rollback();
00796                                                 } else {
00797                                                         $savepoint->release();
00798                                                         $numrowsinserted++;
00799                                                 }
00800                                         }
00801 
00802                                         // If any of them fail, we fail overall for this function call
00803                                         // Note that this will be ignored if IGNORE is set
00804                                         if ( !$tempres ) {
00805                                                 $res = false;
00806                                         }
00807                                 }
00808                         }
00809                 } else {
00810                         // Not multi, just a lone insert
00811                         if ( $savepoint ) {
00812                                 $savepoint->savepoint();
00813                         }
00814 
00815                         $sql .= '(' . $this->makeList( $args ) . ')';
00816                         $res = (bool)$this->query( $sql, $fname, $savepoint );
00817                         if ( $savepoint ) {
00818                                 $bar = pg_last_error();
00819                                 if ( $bar != false ) {
00820                                         $savepoint->rollback();
00821                                 } else {
00822                                         $savepoint->release();
00823                                         $numrowsinserted++;
00824                                 }
00825                         }
00826                 }
00827                 if ( $savepoint ) {
00828                         $olde = error_reporting( $olde );
00829                         $savepoint->commit();
00830 
00831                         // Set the affected row count for the whole operation
00832                         $this->mAffectedRows = $numrowsinserted;
00833 
00834                         // IGNORE always returns true
00835                         return true;
00836                 }
00837 
00838                 return $res;
00839         }
00840 
00850         function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabasePostgres::insertSelect',
00851                 $insertOptions = array(), $selectOptions = array() )
00852         {
00853                 $destTable = $this->tableName( $destTable );
00854 
00855                 if( !is_array( $insertOptions ) ) {
00856                         $insertOptions = array( $insertOptions );
00857                 }
00858 
00859                 /*
00860                  * If IGNORE is set, we use savepoints to emulate mysql's behavior
00861                  * Ignore LOW PRIORITY option, since it is MySQL-specific
00862                  */
00863                 $savepoint = null;
00864                 if ( in_array( 'IGNORE', $insertOptions ) ) {
00865                         $savepoint = new SavepointPostgres( $this, 'mw' );
00866                         $olde = error_reporting( 0 );
00867                         $numrowsinserted = 0;
00868                         $savepoint->savepoint();
00869                 }
00870 
00871                 if( !is_array( $selectOptions ) ) {
00872                         $selectOptions = array( $selectOptions );
00873                 }
00874                 list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
00875                 if( is_array( $srcTable ) ) {
00876                         $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
00877                 } else {
00878                         $srcTable = $this->tableName( $srcTable );
00879                 }
00880 
00881                 $sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
00882                                 " SELECT $startOpts " . implode( ',', $varMap ) .
00883                                 " FROM $srcTable $useIndex";
00884 
00885                 if ( $conds != '*' ) {
00886                         $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
00887                 }
00888 
00889                 $sql .= " $tailOpts";
00890 
00891                 $res = (bool)$this->query( $sql, $fname, $savepoint );
00892                 if( $savepoint ) {
00893                         $bar = pg_last_error();
00894                         if( $bar != false ) {
00895                                 $savepoint->rollback();
00896                         } else {
00897                                 $savepoint->release();
00898                                 $numrowsinserted++;
00899                         }
00900                         $olde = error_reporting( $olde );
00901                         $savepoint->commit();
00902 
00903                         // Set the affected row count for the whole operation
00904                         $this->mAffectedRows = $numrowsinserted;
00905 
00906                         // IGNORE always returns true
00907                         return true;
00908                 }
00909 
00910                 return $res;
00911         }
00912 
00913         function tableName( $name, $format = 'quoted' ) {
00914                 # Replace reserved words with better ones
00915                 switch( $name ) {
00916                         case 'user':
00917                                 return $this->realTableName( 'mwuser', $format );
00918                         case 'text':
00919                                 return $this->realTableName( 'pagecontent', $format );
00920                         default:
00921                                 return $this->realTableName( $name, $format );
00922                 }
00923         }
00924 
00925         /* Don't cheat on installer */
00926         function realTableName( $name, $format = 'quoted' ) {
00927                 return parent::tableName( $name, $format );
00928         }
00929 
00934         function nextSequenceValue( $seqName ) {
00935                 $safeseq = str_replace( "'", "''", $seqName );
00936                 $res = $this->query( "SELECT nextval('$safeseq')" );
00937                 $row = $this->fetchRow( $res );
00938                 $this->mInsertId = $row[0];
00939                 return $this->mInsertId;
00940         }
00941 
00946         function currentSequenceValue( $seqName ) {
00947                 $safeseq = str_replace( "'", "''", $seqName );
00948                 $res = $this->query( "SELECT currval('$safeseq')" );
00949                 $row = $this->fetchRow( $res );
00950                 $currval = $row[0];
00951                 return $currval;
00952         }
00953 
00954         # Returns the size of a text field, or -1 for "unlimited"
00955         function textFieldSize( $table, $field ) {
00956                 $table = $this->tableName( $table );
00957                 $sql = "SELECT t.typname as ftype,a.atttypmod as size
00958                         FROM pg_class c, pg_attribute a, pg_type t
00959                         WHERE relname='$table' AND a.attrelid=c.oid AND
00960                                 a.atttypid=t.oid and a.attname='$field'";
00961                 $res =$this->query( $sql );
00962                 $row = $this->fetchObject( $res );
00963                 if ( $row->ftype == 'varchar' ) {
00964                         $size = $row->size - 4;
00965                 } else {
00966                         $size = $row->size;
00967                 }
00968                 return $size;
00969         }
00970 
00971         function limitResult( $sql, $limit, $offset = false ) {
00972                 return "$sql LIMIT $limit " . ( is_numeric( $offset ) ? " OFFSET {$offset} " : '' );
00973         }
00974 
00975         function wasDeadlock() {
00976                 return $this->lastErrno() == '40P01';
00977         }
00978 
00979         function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabasePostgres::duplicateTableStructure' ) {
00980                 $newName = $this->addIdentifierQuotes( $newName );
00981                 $oldName = $this->addIdentifierQuotes( $oldName );
00982                 return $this->query( 'CREATE ' . ( $temporary ? 'TEMPORARY ' : '' ) . " TABLE $newName (LIKE $oldName INCLUDING DEFAULTS)", $fname );
00983         }
00984 
00985         function listTables( $prefix = null, $fname = 'DatabasePostgres::listTables' ) {
00986                 $eschema = $this->addQuotes( $this->getCoreSchema() );
00987                 $result = $this->query( "SELECT tablename FROM pg_tables WHERE schemaname = $eschema", $fname );
00988                 $endArray = array();
00989 
00990                 foreach( $result as $table ) {
00991                         $vars = get_object_vars( $table );
00992                         $table = array_pop( $vars );
00993                         if( !$prefix || strpos( $table, $prefix ) === 0 ) {
00994                                 $endArray[] = $table;
00995                         }
00996                 }
00997 
00998                 return $endArray;
00999         }
01000 
01001         function timestamp( $ts = 0 ) {
01002                 return wfTimestamp( TS_POSTGRES, $ts );
01003         }
01004 
01005         /*
01006          * Posted by cc[plus]php[at]c2se[dot]com on 25-Mar-2009 09:12
01007          * to http://www.php.net/manual/en/ref.pgsql.php
01008          *
01009          * Parsing a postgres array can be a tricky problem, he's my
01010          * take on this, it handles multi-dimensional arrays plus
01011          * escaping using a nasty regexp to determine the limits of each
01012          * data-item.
01013          *
01014          * This should really be handled by PHP PostgreSQL module
01015          *
01016          * @since 1.19
01017          * @param $text   string: postgreql array returned in a text form like {a,b}
01018          * @param $output string
01019          * @param $limit  int
01020          * @param $offset int
01021          * @return string
01022          */
01023         function pg_array_parse( $text, &$output, $limit = false, $offset = 1 ) {
01024                 if( false === $limit ) {
01025                         $limit = strlen( $text )-1;
01026                         $output = array();
01027                 }
01028                 if( '{}' == $text ) {
01029                         return $output;
01030                 }
01031                 do {
01032                         if ( '{' != $text{$offset} ) {
01033                                 preg_match( "/(\\{?\"([^\"\\\\]|\\\\.)*\"|[^,{}]+)+([,}]+)/",
01034                                         $text, $match, 0, $offset );
01035                                 $offset += strlen( $match[0] );
01036                                 $output[] = ( '"' != $match[1]{0}
01037                                                 ? $match[1]
01038                                                 : stripcslashes( substr( $match[1], 1, -1 ) ) );
01039                                 if ( '},' == $match[3] ) {
01040                                         return $output;
01041                                 }
01042                         } else {
01043                                 $offset = $this->pg_array_parse( $text, $output, $limit, $offset+1 );
01044                         }
01045                 } while ( $limit > $offset );
01046                 return $output;
01047         }
01048 
01052         public function aggregateValue( $valuedata, $valuename = 'value' ) {
01053                 return $valuedata;
01054         }
01055 
01059         public static function getSoftwareLink() {
01060                 return '[http://www.postgresql.org/ PostgreSQL]';
01061         }
01062 
01070         function getCurrentSchema() {
01071                 $res = $this->query( "SELECT current_schema()", __METHOD__ );
01072                 $row = $this->fetchRow( $res );
01073                 return $row[0];
01074         }
01075 
01086         function getSchemas() {
01087                 $res = $this->query( "SELECT current_schemas(false)", __METHOD__ );
01088                 $row = $this->fetchRow( $res );
01089                 $schemas = array();
01090                 /* PHP pgsql support does not support array type, "{a,b}" string is returned */
01091                 return $this->pg_array_parse( $row[0], $schemas );
01092         }
01093 
01103         function getSearchPath() {
01104                 $res = $this->query( "SHOW search_path", __METHOD__ );
01105                 $row = $this->fetchRow( $res );
01106                 /* PostgreSQL returns SHOW values as strings */
01107                 return explode( ",", $row[0] );
01108         }
01109 
01117         function setSearchPath( $search_path ) {
01118                 $this->query( "SET search_path = " . implode( ", ", $search_path ) );
01119         }
01120 
01134         function determineCoreSchema( $desired_schema ) {
01135                 $this->begin( __METHOD__ );
01136                 if ( $this->schemaExists( $desired_schema ) ) {
01137                         if ( in_array( $desired_schema, $this->getSchemas() ) ) {
01138                                 $this->mCoreSchema = $desired_schema;
01139                                 wfDebug( "Schema \"" . $desired_schema . "\" already in the search path\n" );
01140                         } else {
01146                                 $search_path = $this->getSearchPath();
01147                                 array_unshift( $search_path,
01148                                         $this->addIdentifierQuotes( $desired_schema ));
01149                                 $this->setSearchPath( $search_path );
01150                                 $this->mCoreSchema = $desired_schema;
01151                                 wfDebug( "Schema \"" . $desired_schema . "\" added to the search path\n" );
01152                         }
01153                 } else {
01154                         $this->mCoreSchema = $this->getCurrentSchema();
01155                         wfDebug( "Schema \"" . $desired_schema . "\" not found, using current \"" . $this->mCoreSchema . "\"\n" );
01156                 }
01157                 /* Commit SET otherwise it will be rollbacked on error or IGNORE SELECT */
01158                 $this->commit( __METHOD__ );
01159         }
01160 
01167         function getCoreSchema() {
01168                 return $this->mCoreSchema;
01169         }
01170 
01174         function getServerVersion() {
01175                 if ( !isset( $this->numeric_version ) ) {
01176                         $versionInfo = pg_version( $this->mConn );
01177                         if ( version_compare( $versionInfo['client'], '7.4.0', 'lt' ) ) {
01178                                 // Old client, abort install
01179                                 $this->numeric_version = '7.3 or earlier';
01180                         } elseif ( isset( $versionInfo['server'] ) ) {
01181                                 // Normal client
01182                                 $this->numeric_version = $versionInfo['server'];
01183                         } else {
01184                                 // Bug 16937: broken pgsql extension from PHP<5.3
01185                                 $this->numeric_version = pg_parameter_status( $this->mConn, 'server_version' );
01186                         }
01187                 }
01188                 return $this->numeric_version;
01189         }
01190 
01196         function relationExists( $table, $types, $schema = false ) {
01197                 if ( !is_array( $types ) ) {
01198                         $types = array( $types );
01199                 }
01200                 if ( !$schema ) {
01201                         $schema = $this->getCoreSchema();
01202                 }
01203                 $table = $this->realTableName( $table, 'raw' );
01204                 $etable = $this->addQuotes( $table );
01205                 $eschema = $this->addQuotes( $schema );
01206                 $SQL = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n "
01207                         . "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema "
01208                         . "AND c.relkind IN ('" . implode( "','", $types ) . "')";
01209                 $res = $this->query( $SQL );
01210                 $count = $res ? $res->numRows() : 0;
01211                 return (bool)$count;
01212         }
01213 
01219         function tableExists( $table, $fname = __METHOD__, $schema = false ) {
01220                 return $this->relationExists( $table, array( 'r', 'v' ), $schema );
01221         }
01222 
01223         function sequenceExists( $sequence, $schema = false ) {
01224                 return $this->relationExists( $sequence, 'S', $schema );
01225         }
01226 
01227         function triggerExists( $table, $trigger ) {
01228                 $q = <<<SQL
01229         SELECT 1 FROM pg_class, pg_namespace, pg_trigger
01230                 WHERE relnamespace=pg_namespace.oid AND relkind='r'
01231                           AND tgrelid=pg_class.oid
01232                           AND nspname=%s AND relname=%s AND tgname=%s
01233 SQL;
01234                 $res = $this->query(
01235                         sprintf(
01236                                 $q,
01237                                 $this->addQuotes( $this->getCoreSchema() ),
01238                                 $this->addQuotes( $table ),
01239                                 $this->addQuotes( $trigger )
01240                         )
01241                 );
01242                 if ( !$res ) {
01243                         return null;
01244                 }
01245                 $rows = $res->numRows();
01246                 return $rows;
01247         }
01248 
01249         function ruleExists( $table, $rule ) {
01250                 $exists = $this->selectField( 'pg_rules', 'rulename',
01251                         array(
01252                                 'rulename' => $rule,
01253                                 'tablename' => $table,
01254                                 'schemaname' => $this->getCoreSchema()
01255                         )
01256                 );
01257                 return $exists === $rule;
01258         }
01259 
01260         function constraintExists( $table, $constraint ) {
01261                 $SQL = sprintf( "SELECT 1 FROM information_schema.table_constraints " .
01262                                 "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s",
01263                         $this->addQuotes( $this->getCoreSchema() ),
01264                         $this->addQuotes( $table ),
01265                         $this->addQuotes( $constraint )
01266                 );
01267                 $res = $this->query( $SQL );
01268                 if ( !$res ) {
01269                         return null;
01270                 }
01271                 $rows = $res->numRows();
01272                 return $rows;
01273         }
01274 
01279         function schemaExists( $schema ) {
01280                 $exists = $this->selectField( '"pg_catalog"."pg_namespace"', 1,
01281                         array( 'nspname' => $schema ), __METHOD__ );
01282                 return (bool)$exists;
01283         }
01284 
01289         function roleExists( $roleName ) {
01290                 $exists = $this->selectField( '"pg_catalog"."pg_roles"', 1,
01291                         array( 'rolname' => $roleName ), __METHOD__ );
01292                 return (bool)$exists;
01293         }
01294 
01295         function fieldInfo( $table, $field ) {
01296                 return PostgresField::fromText( $this, $table, $field );
01297         }
01298 
01303         function fieldType( $res, $index ) {
01304                 if ( $res instanceof ResultWrapper ) {
01305                         $res = $res->result;
01306                 }
01307                 return pg_field_type( $res, $index );
01308         }
01309 
01314         function encodeBlob( $b ) {
01315                 return new Blob( pg_escape_bytea( $this->mConn, $b ) );
01316         }
01317 
01318         function decodeBlob( $b ) {
01319                 if ( $b instanceof Blob ) {
01320                         $b = $b->fetch();
01321                 }
01322                 return pg_unescape_bytea( $b );
01323         }
01324 
01325         function strencode( $s ) { # Should not be called by us
01326                 return pg_escape_string( $this->mConn, $s );
01327         }
01328 
01333         function addQuotes( $s ) {
01334                 if ( is_null( $s ) ) {
01335                         return 'NULL';
01336                 } elseif ( is_bool( $s ) ) {
01337                         return intval( $s );
01338                 } elseif ( $s instanceof Blob ) {
01339                         return "'" . $s->fetch( $s ) . "'";
01340                 }
01341                 return "'" . pg_escape_string( $this->mConn, $s ) . "'";
01342         }
01343 
01354         protected function replaceVars( $ins ) {
01355                 $ins = parent::replaceVars( $ins );
01356 
01357                 if ( $this->numeric_version >= 8.3 ) {
01358                         // Thanks for not providing backwards-compatibility, 8.3
01359                         $ins = preg_replace( "/to_tsvector\s*\(\s*'default'\s*,/", 'to_tsvector(', $ins );
01360                 }
01361 
01362                 if ( $this->numeric_version <= 8.1 ) { // Our minimum version
01363                         $ins = str_replace( 'USING gin', 'USING gist', $ins );
01364                 }
01365 
01366                 return $ins;
01367         }
01368 
01378         function makeSelectOptions( $options ) {
01379                 $preLimitTail = $postLimitTail = '';
01380                 $startOpts = $useIndex = '';
01381 
01382                 $noKeyOptions = array();
01383                 foreach ( $options as $key => $option ) {
01384                         if ( is_numeric( $key ) ) {
01385                                 $noKeyOptions[$option] = true;
01386                         }
01387                 }
01388 
01389                 $preLimitTail .= $this->makeGroupByWithHaving( $options );
01390 
01391                 $preLimitTail .= $this->makeOrderBy( $options );
01392 
01393                 //if ( isset( $options['LIMIT'] ) ) {
01394                 //      $tailOpts .= $this->limitResult( '', $options['LIMIT'],
01395                 //              isset( $options['OFFSET'] ) ? $options['OFFSET']
01396                 //              : false );
01397                 //}
01398 
01399                 if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
01400                         $postLimitTail .= ' FOR UPDATE';
01401                 }
01402                 if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
01403                         $startOpts .= 'DISTINCT';
01404                 }
01405 
01406                 return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
01407         }
01408 
01409         function setFakeMaster( $enabled = true ) {}
01410 
01411         function getDBname() {
01412                 return $this->mDBname;
01413         }
01414 
01415         function getServer() {
01416                 return $this->mServer;
01417         }
01418 
01419         function buildConcat( $stringList ) {
01420                 return implode( ' || ', $stringList );
01421         }
01422 
01423         public function getSearchEngine() {
01424                 return 'SearchPostgres';
01425         }
01426 
01427         public function streamStatementEnd( &$sql, &$newLine ) {
01428                 # Allow dollar quoting for function declarations
01429                 if ( substr( $newLine, 0, 4 ) == '$mw$' ) {
01430                         if ( $this->delimiter ) {
01431                                 $this->delimiter = false;
01432                         }
01433                         else {
01434                                 $this->delimiter = ';';
01435                         }
01436                 }
01437                 return parent::streamStatementEnd( $sql, $newLine );
01438         }
01439 
01449         public function lockIsFree( $lockName, $method ) {
01450                 $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
01451                 $result = $this->query( "SELECT (CASE(pg_try_advisory_lock($key))
01452                         WHEN 'f' THEN 'f' ELSE pg_advisory_unlock($key) END) AS lockstatus", $method );
01453                 $row = $this->fetchObject( $result );
01454                 return ( $row->lockstatus === 't' );
01455         }
01456 
01464         public function lock( $lockName, $method, $timeout = 5 ) {
01465                 $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
01466                 for ( $attempts = 1; $attempts <= $timeout; ++$attempts ) {
01467                         $result = $this->query(
01468                                 "SELECT pg_try_advisory_lock($key) AS lockstatus", $method );
01469                         $row = $this->fetchObject( $result );
01470                         if ( $row->lockstatus === 't' ) {
01471                                 return true;
01472                         } else {
01473                                 sleep( 1 );
01474                         }
01475                 }
01476                 wfDebug( __METHOD__." failed to acquire lock\n" );
01477                 return false;
01478         }
01479 
01486         public function unlock( $lockName, $method ) {
01487                 $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
01488                 $result = $this->query( "SELECT pg_advisory_unlock($key) as lockstatus", $method );
01489                 $row = $this->fetchObject( $result );
01490                 return ( $row->lockstatus === 't' );
01491         }
01492 
01497         private function bigintFromLockName( $lockName ) {
01498                 return wfBaseConvert( substr( sha1( $lockName ), 0, 15 ), 16, 10 );
01499         }
01500 } // end DatabasePostgres class