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