Merge "Clean up handling of 'infinity'"
[lhc/web/wiklou.git] / includes / db / Database.php
index 0b022d1..0bef28a 100644 (file)
@@ -46,9 +46,6 @@ abstract class DatabaseBase implements IDatabase {
        /** Maximum time to wait before retry */
        const DEADLOCK_DELAY_MAX = 1500000;
 
-       /** How many row changes in a write query trigger a log entry */
-       const LOG_WRITE_THRESHOLD = 300;
-
        protected $mLastQuery = '';
        protected $mDoneWrites = false;
        protected $mPHPError = false;
@@ -156,6 +153,9 @@ abstract class DatabaseBase implements IDatabase {
         */
        protected $allViews = null;
 
+       /** @var TransactionProfiler */
+       protected $trxProfiler;
+
        /**
         * A string describing the current software version, and possibly
         * other details in a user-friendly way. Will be listed on Special:Version, etc.
@@ -348,6 +348,15 @@ abstract class DatabaseBase implements IDatabase {
        public function setFakeMaster( $enabled = true ) {
        }
 
+       /**
+        * @return TransactionProfiler
+        */
+       protected function getTransactionProfiler() {
+               return $this->trxProfiler
+                       ? $this->trxProfiler
+                       : Profiler::instance()->getTransactionProfiler();
+       }
+
        /**
         * Returns true if this database supports (and uses) cascading deletes
         *
@@ -756,32 +765,19 @@ abstract class DatabaseBase implements IDatabase {
         *
         * @param array $params Parameters passed from DatabaseBase::factory()
         */
-       function __construct( $params = null ) {
+       function __construct( array $params ) {
                global $wgDBprefix, $wgDBmwschema, $wgCommandLineMode, $wgDebugDBTransactions;
 
                $this->mTrxAtomicLevels = new SplStack;
 
-               if ( is_array( $params ) ) { // MW 1.22
-                       $server = $params['host'];
-                       $user = $params['user'];
-                       $password = $params['password'];
-                       $dbName = $params['dbname'];
-                       $flags = $params['flags'];
-                       $tablePrefix = $params['tablePrefix'];
-                       $schema = $params['schema'];
-                       $foreign = $params['foreign'];
-               } else { // legacy calling pattern
-                       wfDeprecated( __METHOD__ . " method called without parameter array.", "1.23" );
-                       $args = func_get_args();
-                       $server = isset( $args[0] ) ? $args[0] : false;
-                       $user = isset( $args[1] ) ? $args[1] : false;
-                       $password = isset( $args[2] ) ? $args[2] : false;
-                       $dbName = isset( $args[3] ) ? $args[3] : false;
-                       $flags = isset( $args[4] ) ? $args[4] : 0;
-                       $tablePrefix = isset( $args[5] ) ? $args[5] : 'get from global';
-                       $schema = 'get from global';
-                       $foreign = isset( $args[6] ) ? $args[6] : false;
-               }
+               $server = $params['host'];
+               $user = $params['user'];
+               $password = $params['password'];
+               $dbName = $params['dbname'];
+               $flags = $params['flags'];
+               $tablePrefix = $params['tablePrefix'];
+               $schema = $params['schema'];
+               $foreign = $params['foreign'];
 
                $this->mFlags = $flags;
                if ( $this->mFlags & DBO_DEFAULT ) {
@@ -814,6 +810,10 @@ abstract class DatabaseBase implements IDatabase {
 
                $this->mForeign = $foreign;
 
+               if ( isset( $params['trxProfiler'] ) ) {
+                       $this->trxProfiler = $params['trxProfiler']; // override
+               }
+
                if ( $user ) {
                        $this->open( $server, $user, $password, $dbName );
                }
@@ -901,18 +901,17 @@ abstract class DatabaseBase implements IDatabase {
 
                $class = 'Database' . ucfirst( $driver );
                if ( class_exists( $class ) && is_subclass_of( $class, 'DatabaseBase' ) ) {
-                       $params = array(
-                               'host' => isset( $p['host'] ) ? $p['host'] : false,
-                               'user' => isset( $p['user'] ) ? $p['user'] : false,
-                               'password' => isset( $p['password'] ) ? $p['password'] : false,
-                               'dbname' => isset( $p['dbname'] ) ? $p['dbname'] : false,
-                               'flags' => isset( $p['flags'] ) ? $p['flags'] : 0,
-                               'tablePrefix' => isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global',
-                               'schema' => isset( $p['schema'] ) ? $p['schema'] : $defaultSchemas[$dbType],
-                               'foreign' => isset( $p['foreign'] ) ? $p['foreign'] : false
-                       );
-
-                       return new $class( $params );
+                       // Resolve some defaults for b/c
+                       $p['host'] = isset( $p['host'] ) ? $p['host'] : false;
+                       $p['user'] = isset( $p['user'] ) ? $p['user'] : false;
+                       $p['password'] = isset( $p['password'] ) ? $p['password'] : false;
+                       $p['dbname'] = isset( $p['dbname'] ) ? $p['dbname'] : false;
+                       $p['flags'] = isset( $p['flags'] ) ? $p['flags'] : 0;
+                       $p['tablePrefix'] = isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global';
+                       $p['schema'] = isset( $p['schema'] ) ? $p['schema'] : $defaultSchemas[$dbType];
+                       $p['foreign'] = isset( $p['foreign'] ) ? $p['foreign'] : false;
+
+                       return new $class( $p );
                } else {
                        return null;
                }
@@ -1082,8 +1081,11 @@ abstract class DatabaseBase implements IDatabase {
 
                $isWriteQuery = $this->isWriteQuery( $sql );
                if ( $isWriteQuery ) {
+                       if ( !$this->mDoneWrites ) {
+                               wfDebug( __METHOD__ . ': Writes done: ' .
+                                       DatabaseBase::generalizeSQL( $sql ) . "\n" );
+                       }
                        # Set a flag indicating that writes have been done
-                       wfDebug( __METHOD__ . ': Writes done: ' . DatabaseBase::generalizeSQL( $sql ) . "\n" );
                        $this->mDoneWrites = microtime( true );
                }
 
@@ -1113,28 +1115,25 @@ abstract class DatabaseBase implements IDatabase {
                # Keep track of whether the transaction has write queries pending
                if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWriteQuery ) {
                        $this->mTrxDoneWrites = true;
-                       Profiler::instance()->getTransactionProfiler()->transactionWritingIn(
+                       $this->getTransactionProfiler()->transactionWritingIn(
                                $this->mServer, $this->mDBname, $this->mTrxShortId );
                }
 
-               $queryProf = '';
-               $totalProf = '';
                $isMaster = !is_null( $this->getLBInfo( 'master' ) );
+               # generalizeSQL will probably cut down the query to reasonable
+               # logging size most of the time. The substr is really just a sanity check.
+               if ( $isMaster ) {
+                       $queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
+                       $totalProf = 'DatabaseBase::query-master';
+               } else {
+                       $queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
+                       $totalProf = 'DatabaseBase::query';
+               }
+               # Include query transaction state
+               $queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : "";
 
                $profiler = Profiler::instance();
                if ( !$profiler instanceof ProfilerStub ) {
-                       # generalizeSQL will probably cut down the query to reasonable
-                       # logging size most of the time. The substr is really just a sanity check.
-                       if ( $isMaster ) {
-                               $queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
-                               $totalProf = 'DatabaseBase::query-master';
-                       } else {
-                               $queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
-                               $totalProf = 'DatabaseBase::query';
-                       }
-                       # Include query transaction state
-                       $queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : "";
-
                        $totalProfSection = $profiler->scopedProfileIn( $totalProf );
                        $queryProfSection = $profiler->scopedProfileIn( $queryProf );
                }
@@ -1158,19 +1157,12 @@ abstract class DatabaseBase implements IDatabase {
                        throw new DBUnexpectedError( $this, "DB connection was already closed." );
                }
 
-               # Log the query time and feed it into the DB trx profiler
-               if ( $queryProf != '' ) {
-                       $queryStartTime = microtime( true );
-                       $queryProfile = new ScopedCallback(
-                               function () use ( $queryStartTime, $queryProf, $isMaster ) {
-                                       $trxProfiler = Profiler::instance()->getTransactionProfiler();
-                                       $trxProfiler->recordQueryCompletion( $queryProf, $queryStartTime, $isMaster );
-                               }
-                       );
-               }
-
                # Do the query and handle errors
+               $startTime = microtime( true );
                $ret = $this->doQuery( $commentedSql );
+               # Log the query time and feed it into the DB trx profiler
+               $this->getTransactionProfiler()->recordQueryCompletion(
+                       $queryProf, $startTime, $isWriteQuery, $this->affectedRows() );
 
                MWDebug::queryTime( $queryId );
 
@@ -1197,7 +1189,11 @@ abstract class DatabaseBase implements IDatabase {
                                        $this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $tempIgnore );
                                } else {
                                        # Should be safe to silently retry (no trx and thus no callbacks)
+                                       $startTime = microtime( true );
                                        $ret = $this->doQuery( $commentedSql );
+                                       # Log the query time and feed it into the DB trx profiler
+                                       $this->getTransactionProfiler()->recordQueryCompletion(
+                                               $queryProf, $startTime, $isWriteQuery, $this->affectedRows() );
                                }
                        } else {
                                wfDebug( "Failed\n" );
@@ -1205,14 +1201,8 @@ abstract class DatabaseBase implements IDatabase {
                }
 
                if ( false === $ret ) {
-                       $this->reportQueryError( $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
-               } else {
-                       $n = $this->affectedRows();
-                       if ( $isWriteQuery && $n > self::LOG_WRITE_THRESHOLD && PHP_SAPI !== 'cli' ) {
-                               wfDebugLog( 'DBPerformance',
-                                       "Query affected $n rows:\n" .
-                                       DatabaseBase::generalizeSQL( $sql ) . "\n" . wfBacktrace( true ) );
-                       }
+                       $this->reportQueryError(
+                               $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
                }
 
                $res = $this->resultObject( $ret );
@@ -1236,13 +1226,10 @@ abstract class DatabaseBase implements IDatabase {
         * @throws DBQueryError
         */
        public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
-               # Ignore errors during error handling to avoid infinite recursion
-               $ignore = $this->ignoreErrors( true );
                ++$this->mErrorCount;
 
-               if ( $ignore || $tempIgnore ) {
+               if ( $this->ignoreErrors() || $tempIgnore ) {
                        wfDebug( "SQL ERROR (ignored): $error\n" );
-                       $this->ignoreErrors( $ignore );
                } else {
                        $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
                        wfLogDBError(
@@ -1391,9 +1378,13 @@ abstract class DatabaseBase implements IDatabase {
         *
         * @return bool|mixed The value from the field, or false on failure.
         */
-       public function selectField( $table, $var, $cond = '', $fname = __METHOD__,
-               $options = array()
+       public function selectField(
+               $table, $var, $cond = '', $fname = __METHOD__, $options = array()
        ) {
+               if ( $var === '*' ) { // sanity
+                       throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
+               }
+
                if ( !is_array( $options ) ) {
                        $options = array( $options );
                }
@@ -1401,7 +1392,6 @@ abstract class DatabaseBase implements IDatabase {
                $options['LIMIT'] = 1;
 
                $res = $this->select( $table, $var, $cond, $fname, $options );
-
                if ( $res === false || !$this->numRows( $res ) ) {
                        return false;
                }
@@ -1415,6 +1405,48 @@ abstract class DatabaseBase implements IDatabase {
                }
        }
 
+       /**
+        * A SELECT wrapper which returns a list of single field values from result rows.
+        *
+        * Usually throws a DBQueryError on failure. If errors are explicitly
+        * ignored, returns false on failure.
+        *
+        * If no result rows are returned from the query, false is returned.
+        *
+        * @param string|array $table Table name. See DatabaseBase::select() for details.
+        * @param string $var The field name to select. This must be a valid SQL
+        *   fragment: do not use unvalidated user input.
+        * @param string|array $cond The condition array. See DatabaseBase::select() for details.
+        * @param string $fname The function name of the caller.
+        * @param string|array $options The query options. See DatabaseBase::select() for details.
+        *
+        * @return bool|array The values from the field, or false on failure
+        * @since 1.25
+        */
+       public function selectFieldValues(
+               $table, $var, $cond = '', $fname = __METHOD__, $options = array()
+       ) {
+               if ( $var === '*' ) { // sanity
+                       throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
+               }
+
+               if ( !is_array( $options ) ) {
+                       $options = array( $options );
+               }
+
+               $res = $this->select( $table, $var, $cond, $fname, $options );
+               if ( $res === false ) {
+                       return false;
+               }
+
+               $values = array();
+               foreach ( $res as $row ) {
+                       $values[] = $row->$var;
+               }
+
+               return $values;
+       }
+
        /**
         * Returns an optional USE INDEX clause to go after the table, and a
         * string to go at the end of the query.
@@ -1601,9 +1633,10 @@ abstract class DatabaseBase implements IDatabase {
         *     - If the value of such an array element is a scalar (such as a
         *       string), it will be treated as data and thus quoted appropriately.
         *       If it is null, an IS NULL clause will be added.
-        *     - If the value is an array, an IN(...) clause will be constructed,
-        *       such that the field name may match any of the elements in the
-        *       array. The elements of the array will be quoted.
+        *     - If the value is an array, an IN (...) clause will be constructed
+        *       from its non-null elements, and an IS NULL clause will be added
+        *       if null is present, such that the field may match any of the
+        *       elements in the array. The non-null elements will be quoted.
         *
         * Note that expressions are often DBMS-dependent in their syntax.
         * DBMS-independent wrappers are provided for constructing several types of
@@ -1822,7 +1855,7 @@ abstract class DatabaseBase implements IDatabase {
 
                if ( $res ) {
                        $row = $this->fetchRow( $res );
-                       $rows = ( isset( $row['rowcount'] ) ) ? $row['rowcount'] : 0;
+                       $rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
                }
 
                return $rows;
@@ -1852,7 +1885,7 @@ abstract class DatabaseBase implements IDatabase {
 
                if ( $res ) {
                        $row = $this->fetchRow( $res );
-                       $rows = ( isset( $row['rowcount'] ) ) ? $row['rowcount'] : 0;
+                       $rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
                }
 
                return $rows;
@@ -2642,10 +2675,13 @@ abstract class DatabaseBase implements IDatabase {
        /**
         * Adds quotes and backslashes.
         *
-        * @param string $s
+        * @param string|Blob $s
         * @return string
         */
        public function addQuotes( $s ) {
+               if ( $s instanceof Blob ) {
+                       $s = $s->fetch();
+               }
                if ( $s === null ) {
                        return 'NULL';
                } else {
@@ -3285,41 +3321,40 @@ abstract class DatabaseBase implements IDatabase {
         * @return bool
         */
        public function deadlockLoop() {
-               $this->begin( __METHOD__ );
                $args = func_get_args();
                $function = array_shift( $args );
-               $oldIgnore = $this->ignoreErrors( true );
                $tries = self::DEADLOCK_TRIES;
-
                if ( is_array( $function ) ) {
                        $fname = $function[0];
                } else {
                        $fname = $function;
                }
 
-               do {
-                       $retVal = call_user_func_array( $function, $args );
-                       $error = $this->lastError();
-                       $errno = $this->lastErrno();
-                       $sql = $this->lastQuery();
+               $this->begin( __METHOD__ );
 
-                       if ( $errno ) {
+               $e = null;
+               do {
+                       try {
+                               $retVal = call_user_func_array( $function, $args );
+                               break;
+                       } catch ( DBQueryError $e ) {
+                               $error = $this->lastError();
+                               $errno = $this->lastErrno();
+                               $sql = $this->lastQuery();
                                if ( $this->wasDeadlock() ) {
-                                       # Retry
+                                       // Retry after a randomized delay
                                        usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
                                } else {
-                                       $this->reportQueryError( $error, $errno, $sql, $fname );
+                                       // Throw the error back up
+                                       throw $e;
                                }
                        }
-               } while ( $this->wasDeadlock() && --$tries > 0 );
-
-               $this->ignoreErrors( $oldIgnore );
+               } while ( --$tries > 0 );
 
                if ( $tries <= 0 ) {
+                       // Too many deadlocks; give up
                        $this->rollback( __METHOD__ );
-                       $this->reportQueryError( $error, $errno, $sql, $fname );
-
-                       return false;
+                       throw $e;
                } else {
                        $this->commit( __METHOD__ );
 
@@ -3490,7 +3525,11 @@ abstract class DatabaseBase implements IDatabase {
                if ( !$this->mTrxLevel ) {
                        $this->begin( $fname );
                        $this->mTrxAutomatic = true;
-                       $this->mTrxAutomaticAtomic = true;
+                       // If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
+                       // in all changes being in one transaction to keep requests transactional.
+                       if ( !$this->getFlag( DBO_TRX ) ) {
+                               $this->mTrxAutomaticAtomic = true;
+                       }
                }
 
                $this->mTrxAtomicLevels->push( $fname );
@@ -3572,7 +3611,8 @@ abstract class DatabaseBase implements IDatabase {
                        $this->runOnTransactionPreCommitCallbacks();
                        $this->doCommit( $fname );
                        if ( $this->mTrxDoneWrites ) {
-                               Profiler::instance()->getTransactionProfiler()->transactionWritingOut(
+                               $this->mDoneWrites = microtime( true );
+                               $this->getTransactionProfiler()->transactionWritingOut(
                                        $this->mServer, $this->mDBname, $this->mTrxShortId );
                        }
                        $this->runOnTransactionIdleCallbacks();
@@ -3652,7 +3692,8 @@ abstract class DatabaseBase implements IDatabase {
                $this->runOnTransactionPreCommitCallbacks();
                $this->doCommit( $fname );
                if ( $this->mTrxDoneWrites ) {
-                       Profiler::instance()->getTransactionProfiler()->transactionWritingOut(
+                       $this->mDoneWrites = microtime( true );
+                       $this->getTransactionProfiler()->transactionWritingOut(
                                $this->mServer, $this->mDBname, $this->mTrxShortId );
                }
                $this->runOnTransactionIdleCallbacks();
@@ -3711,7 +3752,7 @@ abstract class DatabaseBase implements IDatabase {
                $this->mTrxPreCommitCallbacks = array(); // cancel
                $this->mTrxAtomicLevels = new SplStack;
                if ( $this->mTrxDoneWrites ) {
-                       Profiler::instance()->getTransactionProfiler()->transactionWritingOut(
+                       $this->getTransactionProfiler()->transactionWritingOut(
                                $this->mServer, $this->mDBname, $this->mTrxShortId );
                }
        }
@@ -3909,10 +3950,13 @@ abstract class DatabaseBase implements IDatabase {
         * in result objects. Pass the object through this function to return the
         * original string.
         *
-        * @param string $b
+        * @param string|Blob $b
         * @return string
         */
        public function decodeBlob( $b ) {
+               if ( $b instanceof Blob ) {
+                       $b = $b->fetch();
+               }
                return $b;
        }
 
@@ -3962,7 +4006,7 @@ abstract class DatabaseBase implements IDatabase {
 
                try {
                        $error = $this->sourceStream( $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
-               } catch ( MWException $e ) {
+               } catch ( Exception $e ) {
                        fclose( $fp );
                        throw $e;
                }
@@ -4288,7 +4332,7 @@ abstract class DatabaseBase implements IDatabase {
         * @return string
         */
        public function decodeExpiry( $expiry, $format = TS_MW ) {
-               return ( $expiry == '' || $expiry == $this->getInfinity() )
+               return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
                        ? 'infinity'
                        : wfTimestamp( $format, $expiry );
        }