MediaWiki  master
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                 }
00236         }
00237 
00238         public function commit() {
00239                 if ( $this->didbegin ) {
00240                         $this->dbw->commit();
00241                 }
00242         }
00243 
00244         protected function query( $keyword, $msg_ok, $msg_failed ) {
00245                 global $wgDebugDBTransactions;
00246                 if ( $this->dbw->doQuery( $keyword . " " . $this->id ) !== false ) {
00247                         if ( $wgDebugDBTransactions ) {
00248                                 wfDebug( sprintf ($msg_ok, $this->id ) );
00249                         }
00250                 } else {
00251                         wfDebug( sprintf ($msg_failed, $this->id ) );
00252                 }
00253         }
00254 
00255         public function savepoint() {
00256                 $this->query("SAVEPOINT",
00257                         "Transaction state: savepoint \"%s\" established.\n",
00258                         "Transaction state: establishment of savepoint \"%s\" FAILED.\n"
00259                 );
00260         }
00261 
00262         public function release() {
00263                 $this->query("RELEASE",
00264                         "Transaction state: savepoint \"%s\" released.\n",
00265                         "Transaction state: release of savepoint \"%s\" FAILED.\n"
00266                 );
00267         }
00268 
00269         public function rollback() {
00270                 $this->query("ROLLBACK TO",
00271                         "Transaction state: savepoint \"%s\" rolled back.\n",
00272                         "Transaction state: rollback of savepoint \"%s\" FAILED.\n"
00273                 );
00274         }
00275 
00276         public function __toString() {
00277                 return (string)$this->id;
00278         }
00279 }
00280 
00284 class DatabasePostgres extends DatabaseBase {
00285         var $mInsertId = null;
00286         var $mLastResult = null;
00287         var $numeric_version = null;
00288         var $mAffectedRows = null;
00289 
00290         function getType() {
00291                 return 'postgres';
00292         }
00293 
00294         function cascadingDeletes() {
00295                 return true;
00296         }
00297         function cleanupTriggers() {
00298                 return true;
00299         }
00300         function strictIPs() {
00301                 return true;
00302         }
00303         function realTimestamps() {
00304                 return true;
00305         }
00306         function implicitGroupby() {
00307                 return false;
00308         }
00309         function implicitOrderby() {
00310                 return false;
00311         }
00312         function searchableIPs() {
00313                 return true;
00314         }
00315         function functionalIndexes() {
00316                 return true;
00317         }
00318 
00319         function hasConstraint( $name ) {
00320                 $SQL = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n WHERE c.connamespace = n.oid AND conname = '" .
00321                                 pg_escape_string( $this->mConn, $name ) . "' AND n.nspname = '" . pg_escape_string( $this->mConn, $this->getCoreSchema() ) ."'";
00322                 $res = $this->doQuery( $SQL );
00323                 return $this->numRows( $res );
00324         }
00325 
00335         function open( $server, $user, $password, $dbName ) {
00336                 # Test for Postgres support, to avoid suppressed fatal error
00337                 if ( !function_exists( 'pg_connect' ) ) {
00338                         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" );
00339                 }
00340 
00341                 global $wgDBport;
00342 
00343                 if ( !strlen( $user ) ) { # e.g. the class is being loaded
00344                         return;
00345                 }
00346 
00347                 $this->mServer = $server;
00348                 $port = $wgDBport;
00349                 $this->mUser = $user;
00350                 $this->mPassword = $password;
00351                 $this->mDBname = $dbName;
00352 
00353                 $connectVars = array(
00354                         'dbname' => $dbName,
00355                         'user' => $user,
00356                         'password' => $password
00357                 );
00358                 if ( $server != false && $server != '' ) {
00359                         $connectVars['host'] = $server;
00360                 }
00361                 if ( $port != false && $port != '' ) {
00362                         $connectVars['port'] = $port;
00363                 }
00364                 if ( $this->mFlags & DBO_SSL ) {
00365                         $connectVars['sslmode'] = 1;
00366                 }
00367 
00368                 $this->connectString = $this->makeConnectionString( $connectVars, PGSQL_CONNECT_FORCE_NEW );
00369                 $this->close();
00370                 $this->installErrorHandler();
00371                 $this->mConn = pg_connect( $this->connectString );
00372                 $phpError = $this->restoreErrorHandler();
00373 
00374                 if ( !$this->mConn ) {
00375                         wfDebug( "DB connection error\n" );
00376                         wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" );
00377                         wfDebug( $this->lastError() . "\n" );
00378                         throw new DBConnectionError( $this, str_replace( "\n", ' ', $phpError ) );
00379                 }
00380 
00381                 $this->mOpened = true;
00382                 $this->mTransactionState = new PostgresTransactionState( $this->mConn );
00383 
00384                 global $wgCommandLineMode;
00385                 # If called from the command-line (e.g. importDump), only show errors
00386                 if ( $wgCommandLineMode ) {
00387                         $this->doQuery( "SET client_min_messages = 'ERROR'" );
00388                 }
00389 
00390                 $this->query( "SET client_encoding='UTF8'", __METHOD__ );
00391                 $this->query( "SET datestyle = 'ISO, YMD'", __METHOD__ );
00392                 $this->query( "SET timezone = 'GMT'", __METHOD__ );
00393                 $this->query( "SET standard_conforming_strings = on", __METHOD__ );
00394 
00395                 global $wgDBmwschema;
00396                 $this->determineCoreSchema( $wgDBmwschema );
00397 
00398                 return $this->mConn;
00399         }
00400 
00406         function selectDB( $db ) {
00407                 if ( $this->mDBname !== $db ) {
00408                         return (bool)$this->open( $this->mServer, $this->mUser, $this->mPassword, $db );
00409                 } else {
00410                         return true;
00411                 }
00412         }
00413 
00414         function makeConnectionString( $vars ) {
00415                 $s = '';
00416                 foreach ( $vars as $name => $value ) {
00417                         $s .= "$name='" . str_replace( "'", "\\'", $value ) . "' ";
00418                 }
00419                 return $s;
00420         }
00421 
00427         protected function closeConnection() {
00428                 return pg_close( $this->mConn );
00429         }
00430 
00431         public function doQuery( $sql ) {
00432                 if ( function_exists( 'mb_convert_encoding' ) ) {
00433                         $sql = mb_convert_encoding( $sql, 'UTF-8' );
00434                 }
00435                 $this->mTransactionState->check();
00436                 if( pg_send_query( $this->mConn, $sql ) === false ) {
00437                         throw new DBUnexpectedError( $this, "Unable to post new query to PostgreSQL\n" );
00438                 }
00439                 $this->mLastResult = pg_get_result( $this->mConn );
00440                 $this->mTransactionState->check();
00441                 $this->mAffectedRows = null;
00442                 if ( pg_result_error( $this->mLastResult ) ) {
00443                         return false;
00444                 }
00445                 return $this->mLastResult;
00446         }
00447 
00448         protected function dumpError () {
00449                 $diags = array( PGSQL_DIAG_SEVERITY,
00450                                 PGSQL_DIAG_SQLSTATE,
00451                                 PGSQL_DIAG_MESSAGE_PRIMARY,
00452                                 PGSQL_DIAG_MESSAGE_DETAIL,
00453                                 PGSQL_DIAG_MESSAGE_HINT,
00454                                 PGSQL_DIAG_STATEMENT_POSITION,
00455                                 PGSQL_DIAG_INTERNAL_POSITION,
00456                                 PGSQL_DIAG_INTERNAL_QUERY,
00457                                 PGSQL_DIAG_CONTEXT,
00458                                 PGSQL_DIAG_SOURCE_FILE,
00459                                 PGSQL_DIAG_SOURCE_LINE,
00460                                 PGSQL_DIAG_SOURCE_FUNCTION );
00461                 foreach ( $diags as $d ) {
00462                         wfDebug( sprintf("PgSQL ERROR(%d): %s\n", $d, pg_result_error_field( $this->mLastResult, $d ) ) );
00463                 }
00464         }
00465 
00466         function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
00467                 /* Transaction stays in the ERROR state until rolledback */
00468                 if ( $tempIgnore ) {
00469                         /* Check for constraint violation */
00470                         if ( $errno === '23505' ) {
00471                                 parent::reportQueryError( $error, $errno, $sql, $fname, $tempIgnore );
00472                                 return;
00473                         }
00474                 }
00475                 /* Don't ignore serious errors */
00476                 $this->rollback( __METHOD__ );
00477                 parent::reportQueryError( $error, $errno, $sql, $fname, false );
00478         }
00479 
00480 
00481         function queryIgnore( $sql, $fname = 'DatabasePostgres::queryIgnore' ) {
00482                 return $this->query( $sql, $fname, true );
00483         }
00484 
00485         function freeResult( $res ) {
00486                 if ( $res instanceof ResultWrapper ) {
00487                         $res = $res->result;
00488                 }
00489                 wfSuppressWarnings();
00490                 $ok = pg_free_result( $res );
00491                 wfRestoreWarnings();
00492                 if ( !$ok ) {
00493                         throw new DBUnexpectedError( $this, "Unable to free Postgres result\n" );
00494                 }
00495         }
00496 
00497         function fetchObject( $res ) {
00498                 if ( $res instanceof ResultWrapper ) {
00499                         $res = $res->result;
00500                 }
00501                 wfSuppressWarnings();
00502                 $row = pg_fetch_object( $res );
00503                 wfRestoreWarnings();
00504                 # @todo FIXME: HACK HACK HACK HACK debug
00505 
00506                 # @todo hashar: not sure if the following test really trigger if the object
00507                 #          fetching failed.
00508                 if( pg_last_error( $this->mConn ) ) {
00509                         throw new DBUnexpectedError( $this, 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) );
00510                 }
00511                 return $row;
00512         }
00513 
00514         function fetchRow( $res ) {
00515                 if ( $res instanceof ResultWrapper ) {
00516                         $res = $res->result;
00517                 }
00518                 wfSuppressWarnings();
00519                 $row = pg_fetch_array( $res );
00520                 wfRestoreWarnings();
00521                 if( pg_last_error( $this->mConn ) ) {
00522                         throw new DBUnexpectedError( $this, 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) );
00523                 }
00524                 return $row;
00525         }
00526 
00527         function numRows( $res ) {
00528                 if ( $res instanceof ResultWrapper ) {
00529                         $res = $res->result;
00530                 }
00531                 wfSuppressWarnings();
00532                 $n = pg_num_rows( $res );
00533                 wfRestoreWarnings();
00534                 if( pg_last_error( $this->mConn ) ) {
00535                         throw new DBUnexpectedError( $this, 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) );
00536                 }
00537                 return $n;
00538         }
00539 
00540         function numFields( $res ) {
00541                 if ( $res instanceof ResultWrapper ) {
00542                         $res = $res->result;
00543                 }
00544                 return pg_num_fields( $res );
00545         }
00546 
00547         function fieldName( $res, $n ) {
00548                 if ( $res instanceof ResultWrapper ) {
00549                         $res = $res->result;
00550                 }
00551                 return pg_field_name( $res, $n );
00552         }
00553 
00558         function insertId() {
00559                 return $this->mInsertId;
00560         }
00561 
00562         function dataSeek( $res, $row ) {
00563                 if ( $res instanceof ResultWrapper ) {
00564                         $res = $res->result;
00565                 }
00566                 return pg_result_seek( $res, $row );
00567         }
00568 
00569         function lastError() {
00570                 if ( $this->mConn ) {
00571                         if ( $this->mLastResult ) {
00572                                 return pg_result_error( $this->mLastResult );
00573                         } else {
00574                                 return pg_last_error();
00575                         }
00576                 } else {
00577                         return 'No database connection';
00578                 }
00579         }
00580         function lastErrno() {
00581                 if ( $this->mLastResult ) {
00582                         return pg_result_error_field( $this->mLastResult, PGSQL_DIAG_SQLSTATE );
00583                 } else {
00584                         return false;
00585                 }
00586         }
00587 
00588         function affectedRows() {
00589                 if ( !is_null( $this->mAffectedRows ) ) {
00590                         // Forced result for simulated queries
00591                         return $this->mAffectedRows;
00592                 }
00593                 if( empty( $this->mLastResult ) ) {
00594                         return 0;
00595                 }
00596                 return pg_affected_rows( $this->mLastResult );
00597         }
00598 
00607         function estimateRowCount( $table, $vars = '*', $conds='', $fname = 'DatabasePostgres::estimateRowCount', $options = array() ) {
00608                 $options['EXPLAIN'] = true;
00609                 $res = $this->select( $table, $vars, $conds, $fname, $options );
00610                 $rows = -1;
00611                 if ( $res ) {
00612                         $row = $this->fetchRow( $res );
00613                         $count = array();
00614                         if( preg_match( '/rows=(\d+)/', $row[0], $count ) ) {
00615                                 $rows = $count[1];
00616                         }
00617                 }
00618                 return $rows;
00619         }
00620 
00626         function indexInfo( $table, $index, $fname = 'DatabasePostgres::indexInfo' ) {
00627                 $sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'";
00628                 $res = $this->query( $sql, $fname );
00629                 if ( !$res ) {
00630                         return null;
00631                 }
00632                 foreach ( $res as $row ) {
00633                         if ( $row->indexname == $this->indexName( $index ) ) {
00634                                 return $row;
00635                         }
00636                 }
00637                 return false;
00638         }
00639 
00646         function indexAttributes ( $index, $schema = false ) {
00647                 if ( $schema === false )
00648                         $schema = $this->getCoreSchema();
00649                 /*
00650                  * A subquery would be not needed if we didn't care about the order
00651                  * of attributes, but we do
00652                  */
00653                 $sql = <<<__INDEXATTR__
00654 
00655                         SELECT opcname,
00656                                 attname,
00657                                 i.indoption[s.g] as option,
00658                                 pg_am.amname
00659                         FROM
00660                                 (SELECT generate_series(array_lower(isub.indkey,1), array_upper(isub.indkey,1)) AS g
00661                                         FROM
00662                                                 pg_index isub
00663                                         JOIN pg_class cis
00664                                                 ON cis.oid=isub.indexrelid
00665                                         JOIN pg_namespace ns
00666                                                 ON cis.relnamespace = ns.oid
00667                                         WHERE cis.relname='$index' AND ns.nspname='$schema') AS s,
00668                                 pg_attribute,
00669                                 pg_opclass opcls,
00670                                 pg_am,
00671                                 pg_class ci
00672                                 JOIN pg_index i
00673                                         ON ci.oid=i.indexrelid
00674                                 JOIN pg_class ct
00675                                         ON ct.oid = i.indrelid
00676                                 JOIN pg_namespace n
00677                                         ON ci.relnamespace = n.oid
00678                                 WHERE
00679                                         ci.relname='$index' AND n.nspname='$schema'
00680                                         AND     attrelid = ct.oid
00681                                         AND     i.indkey[s.g] = attnum
00682                                         AND     i.indclass[s.g] = opcls.oid
00683                                         AND     pg_am.oid = opcls.opcmethod
00684 __INDEXATTR__;
00685                 $res = $this->query($sql, __METHOD__);
00686                 $a = array();
00687                 if ( $res ) {
00688                         foreach ( $res as $row ) {
00689                                 $a[] = array(
00690                                         $row->attname,
00691                                         $row->opcname,
00692                                         $row->amname,
00693                                         $row->option);
00694                         }
00695                 } else {
00696                         return null;
00697                 }
00698                 return $a;
00699         }
00700 
00701 
00702         function indexUnique( $table, $index, $fname = 'DatabasePostgres::indexUnique' ) {
00703                 $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'".
00704                         " AND indexdef LIKE 'CREATE UNIQUE%(" .
00705                         $this->strencode( $this->indexName( $index ) ) .
00706                         ")'";
00707                 $res = $this->query( $sql, $fname );
00708                 if ( !$res ) {
00709                         return null;
00710                 }
00711                 foreach ( $res as $row ) {
00712                         return true;
00713                 }
00714                 return false;
00715         }
00716 
00730         function insert( $table, $args, $fname = 'DatabasePostgres::insert', $options = array() ) {
00731                 if ( !count( $args ) ) {
00732                         return true;
00733                 }
00734 
00735                 $table = $this->tableName( $table );
00736                 if (! isset( $this->numeric_version ) ) {
00737                         $this->getServerVersion();
00738                 }
00739 
00740                 if ( !is_array( $options ) ) {
00741                         $options = array( $options );
00742                 }
00743 
00744                 if ( isset( $args[0] ) && is_array( $args[0] ) ) {
00745                         $multi = true;
00746                         $keys = array_keys( $args[0] );
00747                 } else {
00748                         $multi = false;
00749                         $keys = array_keys( $args );
00750                 }
00751 
00752                 // If IGNORE is set, we use savepoints to emulate mysql's behavior
00753                 $savepoint = null;
00754                 if ( in_array( 'IGNORE', $options ) ) {
00755                         $savepoint = new SavepointPostgres( $this, 'mw' );
00756                         $olde = error_reporting( 0 );
00757                         // For future use, we may want to track the number of actual inserts
00758                         // Right now, insert (all writes) simply return true/false
00759                         $numrowsinserted = 0;
00760                 }
00761 
00762                 $sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES ';
00763 
00764                 if ( $multi ) {
00765                         if ( $this->numeric_version >= 8.2 && !$savepoint ) {
00766                                 $first = true;
00767                                 foreach ( $args as $row ) {
00768                                         if ( $first ) {
00769                                                 $first = false;
00770                                         } else {
00771                                                 $sql .= ',';
00772                                         }
00773                                         $sql .= '(' . $this->makeList( $row ) . ')';
00774                                 }
00775                                 $res = (bool)$this->query( $sql, $fname, $savepoint );
00776                         } else {
00777                                 $res = true;
00778                                 $origsql = $sql;
00779                                 foreach ( $args as $row ) {
00780                                         $tempsql = $origsql;
00781                                         $tempsql .= '(' . $this->makeList( $row ) . ')';
00782 
00783                                         if ( $savepoint ) {
00784                                                 $savepoint->savepoint();
00785                                         }
00786 
00787                                         $tempres = (bool)$this->query( $tempsql, $fname, $savepoint );
00788 
00789                                         if ( $savepoint ) {
00790                                                 $bar = pg_last_error();
00791                                                 if ( $bar != false ) {
00792                                                         $savepoint->rollback();
00793                                                 } else {
00794                                                         $savepoint->release();
00795                                                         $numrowsinserted++;
00796                                                 }
00797                                         }
00798 
00799                                         // If any of them fail, we fail overall for this function call
00800                                         // Note that this will be ignored if IGNORE is set
00801                                         if ( !$tempres ) {
00802                                                 $res = false;
00803                                         }
00804                                 }
00805                         }
00806                 } else {
00807                         // Not multi, just a lone insert
00808                         if ( $savepoint ) {
00809                                 $savepoint->savepoint();
00810                         }
00811 
00812                         $sql .= '(' . $this->makeList( $args ) . ')';
00813                         $res = (bool)$this->query( $sql, $fname, $savepoint );
00814                         if ( $savepoint ) {
00815                                 $bar = pg_last_error();
00816                                 if ( $bar != false ) {
00817                                         $savepoint->rollback();
00818                                 } else {
00819                                         $savepoint->release();
00820                                         $numrowsinserted++;
00821                                 }
00822                         }
00823                 }
00824                 if ( $savepoint ) {
00825                         $olde = error_reporting( $olde );
00826                         $savepoint->commit();
00827 
00828                         // Set the affected row count for the whole operation
00829                         $this->mAffectedRows = $numrowsinserted;
00830 
00831                         // IGNORE always returns true
00832                         return true;
00833                 }
00834 
00835                 return $res;
00836         }
00837 
00847         function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabasePostgres::insertSelect',
00848                 $insertOptions = array(), $selectOptions = array() )
00849         {
00850                 $destTable = $this->tableName( $destTable );
00851 
00852                 if( !is_array( $insertOptions ) ) {
00853                         $insertOptions = array( $insertOptions );
00854                 }
00855 
00856                 /*
00857                  * If IGNORE is set, we use savepoints to emulate mysql's behavior
00858                  * Ignore LOW PRIORITY option, since it is MySQL-specific
00859                  */
00860                 $savepoint = null;
00861                 if ( in_array( 'IGNORE', $insertOptions ) ) {
00862                         $savepoint = new SavepointPostgres( $this, 'mw' );
00863                         $olde = error_reporting( 0 );
00864                         $numrowsinserted = 0;
00865                         $savepoint->savepoint();
00866                 }
00867 
00868                 if( !is_array( $selectOptions ) ) {
00869                         $selectOptions = array( $selectOptions );
00870                 }
00871                 list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
00872                 if( is_array( $srcTable ) ) {
00873                         $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
00874                 } else {
00875                         $srcTable = $this->tableName( $srcTable );
00876                 }
00877 
00878                 $sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
00879                                 " SELECT $startOpts " . implode( ',', $varMap ) .
00880                                 " FROM $srcTable $useIndex";
00881 
00882                 if ( $conds != '*' ) {
00883                         $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
00884                 }
00885 
00886                 $sql .= " $tailOpts";
00887 
00888                 $res = (bool)$this->query( $sql, $fname, $savepoint );
00889                 if( $savepoint ) {
00890                         $bar = pg_last_error();
00891                         if( $bar != false ) {
00892                                 $savepoint->rollback();
00893                         } else {
00894                                 $savepoint->release();
00895                                 $numrowsinserted++;
00896                         }
00897                         $olde = error_reporting( $olde );
00898                         $savepoint->commit();
00899 
00900                         // Set the affected row count for the whole operation
00901                         $this->mAffectedRows = $numrowsinserted;
00902 
00903                         // IGNORE always returns true
00904                         return true;
00905                 }
00906 
00907                 return $res;
00908         }
00909 
00910         function tableName( $name, $format = 'quoted' ) {
00911                 # Replace reserved words with better ones
00912                 switch( $name ) {
00913                         case 'user':
00914                                 return $this->realTableName( 'mwuser', $format );
00915                         case 'text':
00916                                 return $this->realTableName( 'pagecontent', $format );
00917                         default:
00918                                 return $this->realTableName( $name, $format );
00919                 }
00920         }
00921 
00922         /* Don't cheat on installer */
00923         function realTableName( $name, $format = 'quoted' ) {
00924                 return parent::tableName( $name, $format );
00925         }
00926 
00931         function nextSequenceValue( $seqName ) {
00932                 $safeseq = str_replace( "'", "''", $seqName );
00933                 $res = $this->query( "SELECT nextval('$safeseq')" );
00934                 $row = $this->fetchRow( $res );
00935                 $this->mInsertId = $row[0];
00936                 return $this->mInsertId;
00937         }
00938 
00943         function currentSequenceValue( $seqName ) {
00944                 $safeseq = str_replace( "'", "''", $seqName );
00945                 $res = $this->query( "SELECT currval('$safeseq')" );
00946                 $row = $this->fetchRow( $res );
00947                 $currval = $row[0];
00948                 return $currval;
00949         }
00950 
00951         # Returns the size of a text field, or -1 for "unlimited"
00952         function textFieldSize( $table, $field ) {
00953                 $table = $this->tableName( $table );
00954                 $sql = "SELECT t.typname as ftype,a.atttypmod as size
00955                         FROM pg_class c, pg_attribute a, pg_type t
00956                         WHERE relname='$table' AND a.attrelid=c.oid AND
00957                                 a.atttypid=t.oid and a.attname='$field'";
00958                 $res =$this->query( $sql );
00959                 $row = $this->fetchObject( $res );
00960                 if ( $row->ftype == 'varchar' ) {
00961                         $size = $row->size - 4;
00962                 } else {
00963                         $size = $row->size;
00964                 }
00965                 return $size;
00966         }
00967 
00968         function limitResult( $sql, $limit, $offset = false ) {
00969                 return "$sql LIMIT $limit " . ( is_numeric( $offset ) ? " OFFSET {$offset} " : '' );
00970         }
00971 
00972         function wasDeadlock() {
00973                 return $this->lastErrno() == '40P01';
00974         }
00975 
00976         function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabasePostgres::duplicateTableStructure' ) {
00977                 $newName = $this->addIdentifierQuotes( $newName );
00978                 $oldName = $this->addIdentifierQuotes( $oldName );
00979                 return $this->query( 'CREATE ' . ( $temporary ? 'TEMPORARY ' : '' ) . " TABLE $newName (LIKE $oldName INCLUDING DEFAULTS)", $fname );
00980         }
00981 
00982         function listTables( $prefix = null, $fname = 'DatabasePostgres::listTables' ) {
00983                 $eschema = $this->addQuotes( $this->getCoreSchema() );
00984                 $result = $this->query( "SELECT tablename FROM pg_tables WHERE schemaname = $eschema", $fname );
00985                 $endArray = array();
00986 
00987                 foreach( $result as $table ) {
00988                         $vars = get_object_vars($table);
00989                         $table = array_pop( $vars );
00990                         if( !$prefix || strpos( $table, $prefix ) === 0 ) {
00991                                 $endArray[] = $table;
00992                         }
00993                 }
00994 
00995                 return $endArray;
00996         }
00997 
00998         function timestamp( $ts = 0 ) {
00999                 return wfTimestamp( TS_POSTGRES, $ts );
01000         }
01001 
01002         /*
01003          * Posted by cc[plus]php[at]c2se[dot]com on 25-Mar-2009 09:12
01004          * to http://www.php.net/manual/en/ref.pgsql.php
01005          *
01006          * Parsing a postgres array can be a tricky problem, he's my
01007          * take on this, it handles multi-dimensional arrays plus
01008          * escaping using a nasty regexp to determine the limits of each
01009          * data-item.
01010          *
01011          * This should really be handled by PHP PostgreSQL module
01012          *
01013          * @since 1.19
01014          * @param $text   string: postgreql array returned in a text form like {a,b}
01015          * @param $output string
01016          * @param $limit  int
01017          * @param $offset int
01018          * @return string
01019          */
01020         function pg_array_parse( $text, &$output, $limit = false, $offset = 1 ) {
01021                 if( false === $limit ) {
01022                         $limit = strlen( $text )-1;
01023                         $output = array();
01024                 }
01025                 if( '{}' == $text ) {
01026                         return $output;
01027                 }
01028                 do {
01029                         if ( '{' != $text{$offset} ) {
01030                                 preg_match( "/(\\{?\"([^\"\\\\]|\\\\.)*\"|[^,{}]+)+([,}]+)/",
01031                                         $text, $match, 0, $offset );
01032                                 $offset += strlen( $match[0] );
01033                                 $output[] = ( '"' != $match[1]{0}
01034                                                 ? $match[1]
01035                                                 : stripcslashes( substr( $match[1], 1, -1 ) ) );
01036                                 if ( '},' == $match[3] ) {
01037                                         return $output;
01038                                 }
01039                         } else {
01040                                 $offset = $this->pg_array_parse( $text, $output, $limit, $offset+1 );
01041                         }
01042                 } while ( $limit > $offset );
01043                 return $output;
01044         }
01045 
01049         public function aggregateValue( $valuedata, $valuename = 'value' ) {
01050                 return $valuedata;
01051         }
01052 
01056         public static function getSoftwareLink() {
01057                 return '[http://www.postgresql.org/ PostgreSQL]';
01058         }
01059 
01060 
01068         function getCurrentSchema() {
01069                 $res = $this->query( "SELECT current_schema()", __METHOD__);
01070                 $row = $this->fetchRow( $res );
01071                 return $row[0];
01072         }
01073 
01084         function getSchemas() {
01085                 $res = $this->query( "SELECT current_schemas(false)", __METHOD__);
01086                 $row = $this->fetchRow( $res );
01087                 $schemas = array();
01088                 /* PHP pgsql support does not support array type, "{a,b}" string is returned */
01089                 return $this->pg_array_parse($row[0], $schemas);
01090         }
01091 
01101         function getSearchPath() {
01102                 $res = $this->query( "SHOW search_path", __METHOD__);
01103                 $row = $this->fetchRow( $res );
01104                 /* PostgreSQL returns SHOW values as strings */
01105                 return explode(",", $row[0]);
01106         }
01107 
01115         function setSearchPath( $search_path ) {
01116                 $this->query( "SET search_path = " . implode(", ", $search_path) );
01117         }
01118 
01132         function determineCoreSchema( $desired_schema ) {
01133                 $this->begin( __METHOD__ );
01134                 if ( $this->schemaExists( $desired_schema ) ) {
01135                         if ( in_array( $desired_schema, $this->getSchemas() ) ) {
01136                                 $this->mCoreSchema = $desired_schema;
01137                                 wfDebug("Schema \"" . $desired_schema . "\" already in the search path\n");
01138                         } else {
01144                                 $search_path = $this->getSearchPath();
01145                                 array_unshift( $search_path,
01146                                         $this->addIdentifierQuotes( $desired_schema ));
01147                                 $this->setSearchPath( $search_path );
01148                                 $this->mCoreSchema = $desired_schema;
01149                                 wfDebug("Schema \"" . $desired_schema . "\" added to the search path\n");
01150                         }
01151                 } else {
01152                         $this->mCoreSchema = $this->getCurrentSchema();
01153                         wfDebug("Schema \"" . $desired_schema . "\" not found, using current \"". $this->mCoreSchema ."\"\n");
01154                 }
01155                 /* Commit SET otherwise it will be rollbacked on error or IGNORE SELECT */
01156                 $this->commit( __METHOD__ );
01157         }
01158 
01165         function getCoreSchema() {
01166                 return $this->mCoreSchema;
01167         }
01168 
01172         function getServerVersion() {
01173                 if ( !isset( $this->numeric_version ) ) {
01174                         $versionInfo = pg_version( $this->mConn );
01175                         if ( version_compare( $versionInfo['client'], '7.4.0', 'lt' ) ) {
01176                                 // Old client, abort install
01177                                 $this->numeric_version = '7.3 or earlier';
01178                         } elseif ( isset( $versionInfo['server'] ) ) {
01179                                 // Normal client
01180                                 $this->numeric_version = $versionInfo['server'];
01181                         } else {
01182                                 // Bug 16937: broken pgsql extension from PHP<5.3
01183                                 $this->numeric_version = pg_parameter_status( $this->mConn, 'server_version' );
01184                         }
01185                 }
01186                 return $this->numeric_version;
01187         }
01188 
01194         function relationExists( $table, $types, $schema = false ) {
01195                 if ( !is_array( $types ) ) {
01196                         $types = array( $types );
01197                 }
01198                 if ( !$schema ) {
01199                         $schema = $this->getCoreSchema();
01200                 }
01201                 $table = $this->realTableName( $table, 'raw' );
01202                 $etable = $this->addQuotes( $table );
01203                 $eschema = $this->addQuotes( $schema );
01204                 $SQL = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n "
01205                         . "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema "
01206                         . "AND c.relkind IN ('" . implode( "','", $types ) . "')";
01207                 $res = $this->query( $SQL );
01208                 $count = $res ? $res->numRows() : 0;
01209                 return (bool)$count;
01210         }
01211 
01217         function tableExists( $table, $fname = __METHOD__, $schema = false ) {
01218                 return $this->relationExists( $table, array( 'r', 'v' ), $schema );
01219         }
01220 
01221         function sequenceExists( $sequence, $schema = false ) {
01222                 return $this->relationExists( $sequence, 'S', $schema );
01223         }
01224 
01225         function triggerExists( $table, $trigger ) {
01226                 $q = <<<SQL
01227         SELECT 1 FROM pg_class, pg_namespace, pg_trigger
01228                 WHERE relnamespace=pg_namespace.oid AND relkind='r'
01229                           AND tgrelid=pg_class.oid
01230                           AND nspname=%s AND relname=%s AND tgname=%s
01231 SQL;
01232                 $res = $this->query(
01233                         sprintf(
01234                                 $q,
01235                                 $this->addQuotes( $this->getCoreSchema() ),
01236                                 $this->addQuotes( $table ),
01237                                 $this->addQuotes( $trigger )
01238                         )
01239                 );
01240                 if ( !$res ) {
01241                         return null;
01242                 }
01243                 $rows = $res->numRows();
01244                 return $rows;
01245         }
01246 
01247         function ruleExists( $table, $rule ) {
01248                 $exists = $this->selectField( 'pg_rules', 'rulename',
01249                         array(
01250                                 'rulename' => $rule,
01251                                 'tablename' => $table,
01252                                 'schemaname' => $this->getCoreSchema()
01253                         )
01254                 );
01255                 return $exists === $rule;
01256         }
01257 
01258         function constraintExists( $table, $constraint ) {
01259                 $SQL = sprintf( "SELECT 1 FROM information_schema.table_constraints ".
01260                            "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s",
01261                         $this->addQuotes( $this->getCoreSchema() ),
01262                         $this->addQuotes( $table ),
01263                         $this->addQuotes( $constraint )
01264                 );
01265                 $res = $this->query( $SQL );
01266                 if ( !$res ) {
01267                         return null;
01268                 }
01269                 $rows = $res->numRows();
01270                 return $rows;
01271         }
01272 
01277         function schemaExists( $schema ) {
01278                 $exists = $this->selectField( '"pg_catalog"."pg_namespace"', 1,
01279                         array( 'nspname' => $schema ), __METHOD__ );
01280                 return (bool)$exists;
01281         }
01282 
01287         function roleExists( $roleName ) {
01288                 $exists = $this->selectField( '"pg_catalog"."pg_roles"', 1,
01289                         array( 'rolname' => $roleName ), __METHOD__ );
01290                 return (bool)$exists;
01291         }
01292 
01293         function fieldInfo( $table, $field ) {
01294                 return PostgresField::fromText( $this, $table, $field );
01295         }
01296 
01301         function fieldType( $res, $index ) {
01302                 if ( $res instanceof ResultWrapper ) {
01303                         $res = $res->result;
01304                 }
01305                 return pg_field_type( $res, $index );
01306         }
01307 
01312         function encodeBlob( $b ) {
01313                 return new Blob( pg_escape_bytea( $this->mConn, $b ) );
01314         }
01315 
01316         function decodeBlob( $b ) {
01317                 if ( $b instanceof Blob ) {
01318                         $b = $b->fetch();
01319                 }
01320                 return pg_unescape_bytea( $b );
01321         }
01322 
01323         function strencode( $s ) { # Should not be called by us
01324                 return pg_escape_string( $this->mConn, $s );
01325         }
01326 
01331         function addQuotes( $s ) {
01332                 if ( is_null( $s ) ) {
01333                         return 'NULL';
01334                 } elseif ( is_bool( $s ) ) {
01335                         return intval( $s );
01336                 } elseif ( $s instanceof Blob ) {
01337                         return "'" . $s->fetch( $s ) . "'";
01338                 }
01339                 return "'" . pg_escape_string( $this->mConn, $s ) . "'";
01340         }
01341 
01352         protected function replaceVars( $ins ) {
01353                 $ins = parent::replaceVars( $ins );
01354 
01355                 if ( $this->numeric_version >= 8.3 ) {
01356                         // Thanks for not providing backwards-compatibility, 8.3
01357                         $ins = preg_replace( "/to_tsvector\s*\(\s*'default'\s*,/", 'to_tsvector(', $ins );
01358                 }
01359 
01360                 if ( $this->numeric_version <= 8.1 ) { // Our minimum version
01361                         $ins = str_replace( 'USING gin', 'USING gist', $ins );
01362                 }
01363 
01364                 return $ins;
01365         }
01366 
01376         function makeSelectOptions( $options ) {
01377                 $preLimitTail = $postLimitTail = '';
01378                 $startOpts = $useIndex = '';
01379 
01380                 $noKeyOptions = array();
01381                 foreach ( $options as $key => $option ) {
01382                         if ( is_numeric( $key ) ) {
01383                                 $noKeyOptions[$option] = true;
01384                         }
01385                 }
01386 
01387                 if ( isset( $options['GROUP BY'] ) ) {
01388                         $gb = is_array( $options['GROUP BY'] )
01389                                 ? implode( ',', $options['GROUP BY'] )
01390                                 : $options['GROUP BY'];
01391                         $preLimitTail .= " GROUP BY {$gb}";
01392                 }
01393 
01394                 if ( isset( $options['HAVING'] ) ) {
01395                         $preLimitTail .= " HAVING {$options['HAVING']}";
01396                 }
01397 
01398                 if ( isset( $options['ORDER BY'] ) ) {
01399                         $ob = is_array( $options['ORDER BY'] )
01400                                 ? implode( ',', $options['ORDER BY'] )
01401                                 : $options['ORDER BY'];
01402                         $preLimitTail .= " ORDER BY {$ob}";
01403                 }
01404 
01405                 //if ( isset( $options['LIMIT'] ) ) {
01406                 //      $tailOpts .= $this->limitResult( '', $options['LIMIT'],
01407                 //              isset( $options['OFFSET'] ) ? $options['OFFSET']
01408                 //              : false );
01409                 //}
01410 
01411                 if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
01412                         $postLimitTail .= ' FOR UPDATE';
01413                 }
01414                 if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
01415                         $startOpts .= 'DISTINCT';
01416                 }
01417 
01418                 return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
01419         }
01420 
01421         function setFakeMaster( $enabled = true ) {}
01422 
01423         function getDBname() {
01424                 return $this->mDBname;
01425         }
01426 
01427         function getServer() {
01428                 return $this->mServer;
01429         }
01430 
01431         function buildConcat( $stringList ) {
01432                 return implode( ' || ', $stringList );
01433         }
01434 
01435         public function getSearchEngine() {
01436                 return 'SearchPostgres';
01437         }
01438 
01439         public function streamStatementEnd( &$sql, &$newLine ) {
01440                 # Allow dollar quoting for function declarations
01441                 if ( substr( $newLine, 0, 4 ) == '$mw$' ) {
01442                         if ( $this->delimiter ) {
01443                                 $this->delimiter = false;
01444                         }
01445                         else {
01446                                 $this->delimiter = ';';
01447                         }
01448                 }
01449                 return parent::streamStatementEnd( $sql, $newLine );
01450         }
01451 } // end DatabasePostgres class