MediaWiki
master
|
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