Merge "tests: Remove verbose logging settings from DevelopmentSettings.php"
[lhc/web/wiklou.git] / includes / libs / rdbms / database / DatabasePostgres.php
index bdac06c..7d34641 100644 (file)
@@ -24,7 +24,7 @@ namespace Wikimedia\Rdbms;
 
 use Wikimedia\Timestamp\ConvertibleTimestamp;
 use Wikimedia\WaitConditionLoop;
-use MediaWiki;
+use Wikimedia;
 use Exception;
 
 /**
@@ -35,18 +35,16 @@ class DatabasePostgres extends Database {
        protected $port;
 
        /** @var resource */
-       protected $mLastResult = null;
+       protected $lastResultHandle = null;
        /** @var int The number of rows affected as an integer */
-       protected $mAffectedRows = null;
+       protected $lastAffectedRowCount = null;
 
-       /** @var int */
-       private $mInsertId = null;
        /** @var float|string */
        private $numericVersion = null;
        /** @var string Connect string to open a PostgreSQL connection */
        private $connectString;
        /** @var string */
-       private $mCoreSchema;
+       private $coreSchema;
        /** @var string[] Map of (reserved table name => alternate table name) */
        private $keywordTableMap = [];
 
@@ -99,10 +97,10 @@ class DatabasePostgres extends Database {
                        );
                }
 
-               $this->mServer = $server;
-               $this->mUser = $user;
-               $this->mPassword = $password;
-               $this->mDBname = $dbName;
+               $this->server = $server;
+               $this->user = $user;
+               $this->password = $password;
+               $this->dbName = $dbName;
 
                $connectVars = [
                        // pg_connect() user $user as the default database. Since a database is *required*,
@@ -118,7 +116,7 @@ class DatabasePostgres extends Database {
                if ( (int)$this->port > 0 ) {
                        $connectVars['port'] = (int)$this->port;
                }
-               if ( $this->mFlags & self::DBO_SSL ) {
+               if ( $this->flags & self::DBO_SSL ) {
                        $connectVars['sslmode'] = 1;
                }
 
@@ -128,7 +126,7 @@ class DatabasePostgres extends Database {
 
                try {
                        // Use new connections to let LoadBalancer/LBFactory handle reuse
-                       $this->mConn = pg_connect( $this->connectString, PGSQL_CONNECT_FORCE_NEW );
+                       $this->conn = pg_connect( $this->connectString, PGSQL_CONNECT_FORCE_NEW );
                } catch ( Exception $ex ) {
                        $this->restoreErrorHandler();
                        throw $ex;
@@ -136,7 +134,7 @@ class DatabasePostgres extends Database {
 
                $phpError = $this->restoreErrorHandler();
 
-               if ( !$this->mConn ) {
+               if ( !$this->conn ) {
                        $this->queryLogger->debug(
                                "DB connection error\n" .
                                "Server: $server, Database: $dbName, User: $user, Password: " .
@@ -146,7 +144,7 @@ class DatabasePostgres extends Database {
                        throw new DBConnectionError( $this, str_replace( "\n", ' ', $phpError ) );
                }
 
-               $this->mOpened = true;
+               $this->opened = true;
 
                # If called from the command-line (e.g. importDump), only show errors
                if ( $this->cliMode ) {
@@ -161,11 +159,11 @@ class DatabasePostgres extends Database {
                        $this->query( "SET bytea_output = 'escape'", __METHOD__ ); // PHP bug 53127
                }
 
-               $this->determineCoreSchema( $this->mSchema );
+               $this->determineCoreSchema( $this->schema );
                // The schema to be used is now in the search path; no need for explicit qualification
-               $this->mSchema = '';
+               $this->schema = '';
 
-               return $this->mConn;
+               return $this->conn;
        }
 
        public function databasesAreIndependent() {
@@ -180,8 +178,8 @@ class DatabasePostgres extends Database {
         * @throws DBUnexpectedError
         */
        public function selectDB( $db ) {
-               if ( $this->mDBname !== $db ) {
-                       return (bool)$this->open( $this->mServer, $this->mUser, $this->mPassword, $db );
+               if ( $this->dbName !== $db ) {
+                       return (bool)$this->open( $this->server, $this->user, $this->password, $db );
                } else {
                        return true;
                }
@@ -201,7 +199,12 @@ class DatabasePostgres extends Database {
        }
 
        protected function closeConnection() {
-               return $this->mConn ? pg_close( $this->mConn ) : true;
+               return $this->conn ? pg_close( $this->conn ) : true;
+       }
+
+       protected function isTransactableQuery( $sql ) {
+               return parent::isTransactableQuery( $sql ) &&
+                       !preg_match( '/^SELECT\s+pg_(try_|)advisory_\w+\(/', $sql );
        }
 
        public function doQuery( $sql ) {
@@ -215,13 +218,13 @@ class DatabasePostgres extends Database {
                if ( pg_send_query( $conn, $sql ) === false ) {
                        throw new DBUnexpectedError( $this, "Unable to post new query to PostgreSQL\n" );
                }
-               $this->mLastResult = pg_get_result( $conn );
-               $this->mAffectedRows = null;
-               if ( pg_result_error( $this->mLastResult ) ) {
+               $this->lastResultHandle = pg_get_result( $conn );
+               $this->lastAffectedRowCount = null;
+               if ( pg_result_error( $this->lastResultHandle ) ) {
                        return false;
                }
 
-               return $this->mLastResult;
+               return $this->lastResultHandle;
        }
 
        protected function dumpError() {
@@ -241,7 +244,7 @@ class DatabasePostgres extends Database {
                ];
                foreach ( $diags as $d ) {
                        $this->queryLogger->debug( sprintf( "PgSQL ERROR(%d): %s\n",
-                               $d, pg_result_error_field( $this->mLastResult, $d ) ) );
+                               $d, pg_result_error_field( $this->lastResultHandle, $d ) ) );
                }
        }
 
@@ -255,7 +258,7 @@ class DatabasePostgres extends Database {
                        }
                }
                /* Transaction stays in the ERROR state until rolled back */
-               if ( $this->mTrxLevel ) {
+               if ( $this->trxLevel ) {
                        // Throw away the transaction state, then raise the error as normal.
                        // Note that if this connection is managed by LBFactory, it's already expected
                        // that the other transactions LBFactory manages will be rolled back.
@@ -268,9 +271,9 @@ class DatabasePostgres extends Database {
                if ( $res instanceof ResultWrapper ) {
                        $res = $res->result;
                }
-               MediaWiki\suppressWarnings();
+               Wikimedia\suppressWarnings();
                $ok = pg_free_result( $res );
-               MediaWiki\restoreWarnings();
+               Wikimedia\restoreWarnings();
                if ( !$ok ) {
                        throw new DBUnexpectedError( $this, "Unable to free Postgres result\n" );
                }
@@ -280,9 +283,9 @@ class DatabasePostgres extends Database {
                if ( $res instanceof ResultWrapper ) {
                        $res = $res->result;
                }
-               MediaWiki\suppressWarnings();
+               Wikimedia\suppressWarnings();
                $row = pg_fetch_object( $res );
-               MediaWiki\restoreWarnings();
+               Wikimedia\restoreWarnings();
                # @todo FIXME: HACK HACK HACK HACK debug
 
                # @todo hashar: not sure if the following test really trigger if the object
@@ -302,9 +305,9 @@ class DatabasePostgres extends Database {
                if ( $res instanceof ResultWrapper ) {
                        $res = $res->result;
                }
-               MediaWiki\suppressWarnings();
+               Wikimedia\suppressWarnings();
                $row = pg_fetch_array( $res );
-               MediaWiki\restoreWarnings();
+               Wikimedia\restoreWarnings();
 
                $conn = $this->getBindingHandle();
                if ( pg_last_error( $conn ) ) {
@@ -321,9 +324,9 @@ class DatabasePostgres extends Database {
                if ( $res instanceof ResultWrapper ) {
                        $res = $res->result;
                }
-               MediaWiki\suppressWarnings();
+               Wikimedia\suppressWarnings();
                $n = pg_num_rows( $res );
-               MediaWiki\restoreWarnings();
+               Wikimedia\restoreWarnings();
 
                $conn = $this->getBindingHandle();
                if ( pg_last_error( $conn ) ) {
@@ -352,14 +355,10 @@ class DatabasePostgres extends Database {
                return pg_field_name( $res, $n );
        }
 
-       /**
-        * Return the result of the last call to nextSequenceValue();
-        * This must be called after nextSequenceValue().
-        *
-        * @return int|null
-        */
        public function insertId() {
-               return $this->mInsertId;
+               $res = $this->query( "SELECT lastval()" );
+               $row = $this->fetchRow( $res );
+               return is_null( $row[0] ) ? null : (int)$row[0];
        }
 
        public function dataSeek( $res, $row ) {
@@ -371,9 +370,9 @@ class DatabasePostgres extends Database {
        }
 
        public function lastError() {
-               if ( $this->mConn ) {
-                       if ( $this->mLastResult ) {
-                               return pg_result_error( $this->mLastResult );
+               if ( $this->conn ) {
+                       if ( $this->lastResultHandle ) {
+                               return pg_result_error( $this->lastResultHandle );
                        } else {
                                return pg_last_error();
                        }
@@ -383,23 +382,23 @@ class DatabasePostgres extends Database {
        }
 
        public function lastErrno() {
-               if ( $this->mLastResult ) {
-                       return pg_result_error_field( $this->mLastResult, PGSQL_DIAG_SQLSTATE );
+               if ( $this->lastResultHandle ) {
+                       return pg_result_error_field( $this->lastResultHandle, PGSQL_DIAG_SQLSTATE );
                } else {
                        return false;
                }
        }
 
-       public function affectedRows() {
-               if ( !is_null( $this->mAffectedRows ) ) {
+       protected function fetchAffectedRowCount() {
+               if ( !is_null( $this->lastAffectedRowCount ) ) {
                        // Forced result for simulated queries
-                       return $this->mAffectedRows;
+                       return $this->lastAffectedRowCount;
                }
-               if ( empty( $this->mLastResult ) ) {
+               if ( empty( $this->lastResultHandle ) ) {
                        return 0;
                }
 
-               return pg_affected_rows( $this->mLastResult );
+               return pg_affected_rows( $this->lastResultHandle );
        }
 
        /**
@@ -521,6 +520,10 @@ __INDEXATTR__;
        public function selectSQLText(
                $table, $vars, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
        ) {
+               if ( is_string( $options ) ) {
+                       $options = [ $options ];
+               }
+
                // Change the FOR UPDATE option as necessary based on the join conditions. Then pass
                // to the parent function to get the actual SQL text.
                // In Postgres when using FOR UPDATE, only the main table and tables that are inner joined
@@ -532,10 +535,30 @@ __INDEXATTR__;
                        $forUpdateKey = array_search( 'FOR UPDATE', $options, true );
                        if ( $forUpdateKey !== false && $join_conds ) {
                                unset( $options[$forUpdateKey] );
+                               $options['FOR UPDATE'] = [];
+
+                               $toCheck = $table;
+                               reset( $toCheck );
+                               while ( $toCheck ) {
+                                       $alias = key( $toCheck );
+                                       $name = $toCheck[$alias];
+                                       unset( $toCheck[$alias] );
+
+                                       $hasAlias = !is_numeric( $alias );
+                                       if ( !$hasAlias && is_string( $name ) ) {
+                                               $alias = $name;
+                                       }
 
-                               foreach ( $join_conds as $table_cond => $join_cond ) {
-                                       if ( 0 === preg_match( '/^(?:LEFT|RIGHT|FULL)(?: OUTER)? JOIN$/i', $join_cond[0] ) ) {
-                                               $options['FOR UPDATE'][] = $table_cond;
+                                       if ( !isset( $join_conds[$alias] ) ||
+                                               !preg_match( '/^(?:LEFT|RIGHT|FULL)(?: OUTER)? JOIN$/i', $join_conds[$alias][0] )
+                                       ) {
+                                               if ( is_array( $name ) ) {
+                                                       // It's a parenthesized group, process all the tables inside the group.
+                                                       $toCheck = array_merge( $toCheck, $name );
+                                               } else {
+                                                       // Quote alias names so $this->tableName() won't mangle them
+                                                       $options['FOR UPDATE'][] = $hasAlias ? $this->addIdentifierQuotes( $alias ) : $alias;
+                                               }
                                        }
                                }
                        }
@@ -583,6 +606,7 @@ __INDEXATTR__;
                }
 
                // If IGNORE is set, we use savepoints to emulate mysql's behavior
+               // @todo If PostgreSQL 9.5+, we could use ON CONFLICT DO NOTHING instead
                $savepoint = $olde = null;
                $numrowsinserted = 0;
                if ( in_array( 'IGNORE', $options ) ) {
@@ -620,7 +644,7 @@ __INDEXATTR__;
                                        $tempres = (bool)$this->query( $tempsql, $fname, $savepoint );
 
                                        if ( $savepoint ) {
-                                               $bar = pg_result_error( $this->mLastResult );
+                                               $bar = pg_result_error( $this->lastResultHandle );
                                                if ( $bar != false ) {
                                                        $savepoint->rollback();
                                                } else {
@@ -645,7 +669,7 @@ __INDEXATTR__;
                        $sql .= '(' . $this->makeList( $args ) . ')';
                        $res = (bool)$this->query( $sql, $fname, $savepoint );
                        if ( $savepoint ) {
-                               $bar = pg_result_error( $this->mLastResult );
+                               $bar = pg_result_error( $this->lastResultHandle );
                                if ( $bar != false ) {
                                        $savepoint->rollback();
                                } else {
@@ -659,7 +683,7 @@ __INDEXATTR__;
                        $savepoint->commit();
 
                        // Set the affected row count for the whole operation
-                       $this->mAffectedRows = $numrowsinserted;
+                       $this->lastAffectedRowCount = $numrowsinserted;
 
                        // IGNORE always returns true
                        return true;
@@ -696,39 +720,17 @@ __INDEXATTR__;
                }
 
                /*
-                * If IGNORE is set, we use savepoints to emulate mysql's behavior
-                * Ignore LOW PRIORITY option, since it is MySQL-specific
+                * If IGNORE is set, use the non-native version.
+                * @todo If PostgreSQL 9.5+, we could use ON CONFLICT DO NOTHING
                 */
-               $savepoint = $olde = null;
-               $numrowsinserted = 0;
                if ( in_array( 'IGNORE', $insertOptions ) ) {
-                       $savepoint = new SavepointPostgres( $this, 'mw', $this->queryLogger );
-                       $olde = error_reporting( 0 );
-                       $savepoint->savepoint();
+                       return $this->nonNativeInsertSelect(
+                               $destTable, $srcTable, $varMap, $conds, $fname, $insertOptions, $selectOptions, $selectJoinConds
+                       );
                }
 
-               $res = parent::nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname,
+               return parent::nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname,
                        $insertOptions, $selectOptions, $selectJoinConds );
-
-               if ( $savepoint ) {
-                       $bar = pg_result_error( $this->mLastResult );
-                       if ( $bar != false ) {
-                               $savepoint->rollback();
-                       } else {
-                               $savepoint->release();
-                               $numrowsinserted++;
-                       }
-                       error_reporting( $olde );
-                       $savepoint->commit();
-
-                       // Set the affected row count for the whole operation
-                       $this->mAffectedRows = $numrowsinserted;
-
-                       // IGNORE always returns true
-                       return true;
-               }
-
-               return $res;
        }
 
        public function tableName( $name, $format = 'quoted' ) {
@@ -756,12 +758,7 @@ __INDEXATTR__;
        }
 
        public function nextSequenceValue( $seqName ) {
-               $safeseq = str_replace( "'", "''", $seqName );
-               $res = $this->query( "SELECT nextval('$safeseq')" );
-               $row = $this->fetchRow( $res );
-               $this->mInsertId = is_null( $row[0] ) ? null : (int)$row[0];
-
-               return $this->mInsertId;
+               return new NextSequenceValue;
        }
 
        /**
@@ -971,7 +968,7 @@ __INDEXATTR__;
                $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
                if ( $this->schemaExists( $desiredSchema ) ) {
                        if ( in_array( $desiredSchema, $this->getSchemas() ) ) {
-                               $this->mCoreSchema = $desiredSchema;
+                               $this->coreSchema = $desiredSchema;
                                $this->queryLogger->debug(
                                        "Schema \"" . $desiredSchema . "\" already in the search path\n" );
                        } else {
@@ -984,15 +981,15 @@ __INDEXATTR__;
                                array_unshift( $search_path,
                                        $this->addIdentifierQuotes( $desiredSchema ) );
                                $this->setSearchPath( $search_path );
-                               $this->mCoreSchema = $desiredSchema;
+                               $this->coreSchema = $desiredSchema;
                                $this->queryLogger->debug(
                                        "Schema \"" . $desiredSchema . "\" added to the search path\n" );
                        }
                } else {
-                       $this->mCoreSchema = $this->getCurrentSchema();
+                       $this->coreSchema = $this->getCurrentSchema();
                        $this->queryLogger->debug(
                                "Schema \"" . $desiredSchema . "\" not found, using current \"" .
-                               $this->mCoreSchema . "\"\n" );
+                               $this->coreSchema . "\"\n" );
                }
                /* Commit SET otherwise it will be rollbacked on error or IGNORE SELECT */
                $this->commit( __METHOD__, self::FLUSHING_INTERNAL );
@@ -1005,7 +1002,7 @@ __INDEXATTR__;
         * @return string Core schema name
         */
        public function getCoreSchema() {
-               return $this->mCoreSchema;
+               return $this->coreSchema;
        }
 
        public function getServerVersion() {
@@ -1149,8 +1146,8 @@ SQL;
        }
 
        /**
-        * @var string $table
-        * @var string $field
+        * @param string $table
+        * @param string $field
         * @return PostgresField|null
         */
        public function fieldInfo( $table, $field ) {
@@ -1187,7 +1184,7 @@ SQL;
 
        public function strencode( $s ) {
                // Should not be called by us
-               return pg_escape_string( $this->getBindingHandle(), $s );
+               return pg_escape_string( $this->getBindingHandle(), (string)$s );
        }
 
        public function addQuotes( $s ) {
@@ -1204,9 +1201,11 @@ SQL;
                                $s = pg_escape_bytea( $conn, $s->fetch() );
                        }
                        return "'$s'";
+               } elseif ( $s instanceof NextSequenceValue ) {
+                       return 'DEFAULT';
                }
 
-               return "'" . pg_escape_string( $conn, $s ) . "'";
+               return "'" . pg_escape_string( $conn, (string)$s ) . "'";
        }
 
        /**
@@ -1261,11 +1260,11 @@ SQL;
        }
 
        public function getDBname() {
-               return $this->mDBname;
+               return $this->dbName;
        }
 
        public function getServer() {
-               return $this->mServer;
+               return $this->server;
        }
 
        public function buildConcat( $stringList ) {
@@ -1325,6 +1324,9 @@ SQL;
        }
 
        public function lockIsFree( $lockName, $method ) {
+               if ( !parent::lockIsFree( $lockName, $method ) ) {
+                       return false; // already held
+               }
                // http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
                $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
                $result = $this->query( "SELECT (CASE(pg_try_advisory_lock($key))