Merge "mediawiki.action.view.rightClickEdit: Remove redundanat dom-ready handler"
[lhc/web/wiklou.git] / includes / libs / rdbms / database / Database.php
index 8cb726e..523f7cd 100644 (file)
@@ -35,6 +35,7 @@ use BagOStuff;
 use HashBagOStuff;
 use LogicException;
 use InvalidArgumentException;
+use UnexpectedValueException;
 use Exception;
 use RuntimeException;
 
@@ -187,6 +188,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * @see Database::trxLevel
         */
        private $trxAutomatic = false;
+       /**
+        * Counter for atomic savepoint identifiers. Reset when a new transaction begins.
+        *
+        * @var int
+        */
+       private $trxAtomicCounter = 0;
        /**
         * Array of levels of atomicity within transactions
         *
@@ -276,6 +283,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                $this->flags |= self::DBO_TRX;
                        }
                }
+               // Disregard deprecated DBO_IGNORE flag (T189999)
+               $this->flags &= ~self::DBO_IGNORE;
 
                $this->sessionVars = $params['variables'];
 
@@ -531,32 +540,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                return $res;
        }
 
-       /**
-        * Turns on (false) or off (true) the automatic generation and sending
-        * of a "we're sorry, but there has been a database error" page on
-        * database errors. Default is on (false). When turned off, the
-        * code should use lastErrno() and lastError() to handle the
-        * situation as appropriate.
-        *
-        * Do not use this function outside of the Database classes.
-        *
-        * @param null|bool $ignoreErrors
-        * @return bool The previous value of the flag.
-        */
-       protected function ignoreErrors( $ignoreErrors = null ) {
-               $res = $this->getFlag( self::DBO_IGNORE );
-               if ( $ignoreErrors !== null ) {
-                       // setFlag()/clearFlag() do not allow DBO_IGNORE changes for sanity
-                       if ( $ignoreErrors ) {
-                               $this->flags |= self::DBO_IGNORE;
-                       } else {
-                               $this->flags &= ~self::DBO_IGNORE;
-                       }
-               }
-
-               return $res;
-       }
-
        public function trxLevel() {
                return $this->trxLevel;
        }
@@ -713,7 +696,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
 
        public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
                if ( ( $flag & self::DBO_IGNORE ) ) {
-                       throw new \UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
+                       throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
                }
 
                if ( $remember === self::REMEMBER_PRIOR ) {
@@ -724,7 +707,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
 
        public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
                if ( ( $flag & self::DBO_IGNORE ) ) {
-                       throw new \UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
+                       throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
                }
 
                if ( $remember === self::REMEMBER_PRIOR ) {
@@ -906,6 +889,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         */
        abstract protected function closeConnection();
 
+       /**
+        * @param string $error Fallback error message, used if none is given by DB
+        * @throws DBConnectionError
+        */
        public function reportConnectionError( $error = 'Unknown error' ) {
                $myError = $this->lastError();
                if ( $myError ) {
@@ -1241,6 +1228,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         */
        private function handleSessionLoss() {
                $this->trxLevel = 0;
+               $this->trxAtomicCounter = 0;
                $this->trxIdleCallbacks = []; // T67263; transaction already lost
                $this->trxPreCommitCallbacks = []; // T67263; transaction already lost
                $this->sessionTempTables = [];
@@ -1280,8 +1268,19 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                return false;
        }
 
+       /**
+        * Report a query error. Log the error, and if neither the object ignore
+        * flag nor the $tempIgnore flag is set, throw a DBQueryError.
+        *
+        * @param string $error
+        * @param int $errno
+        * @param string $sql
+        * @param string $fname
+        * @param bool $tempIgnore
+        * @throws DBQueryError
+        */
        public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
-               if ( $this->ignoreErrors() || $tempIgnore ) {
+               if ( $tempIgnore ) {
                        $this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
                } else {
                        $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
@@ -1588,8 +1587,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        public function estimateRowCount(
-               $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
+               $table, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
        ) {
+               $conds = $this->normalizeConditions( $conds, $fname );
+               $column = $this->extractSingleFieldFromList( $var );
+               if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
+                       $conds[] = "$column IS NOT NULL";
+               }
+
                $res = $this->select(
                        $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options, $join_conds
                );
@@ -1599,21 +1604,76 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        public function selectRowCount(
-               $tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
+               $tables, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
        ) {
-               $rows = 0;
-               $sql = $this->selectSQLText( $tables, '1', $conds, $fname, $options, $join_conds );
-               // The identifier quotes is primarily for MSSQL.
-               $rowCountCol = $this->addIdentifierQuotes( "rowcount" );
-               $tableName = $this->addIdentifierQuotes( "tmp_count" );
-               $res = $this->query( "SELECT COUNT(*) AS $rowCountCol FROM ($sql) $tableName", $fname );
+               $conds = $this->normalizeConditions( $conds, $fname );
+               $column = $this->extractSingleFieldFromList( $var );
+               if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
+                       $conds[] = "$column IS NOT NULL";
+               }
+
+               $res = $this->select(
+                       [
+                               'tmp_count' => $this->buildSelectSubquery(
+                                       $tables,
+                                       '1',
+                                       $conds,
+                                       $fname,
+                                       $options,
+                                       $join_conds
+                               )
+                       ],
+                       [ 'rowcount' => 'COUNT(*)' ],
+                       [],
+                       $fname
+               );
+               $row = $res ? $this->fetchRow( $res ) : [];
+
+               return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
+       }
+
+       /**
+        * @param array|string $conds
+        * @param string $fname
+        * @return array
+        */
+       final protected function normalizeConditions( $conds, $fname ) {
+               if ( $conds === null || $conds === false ) {
+                       $this->queryLogger->warning(
+                               __METHOD__
+                               . ' called from '
+                               . $fname
+                               . ' with incorrect parameters: $conds must be a string or an array'
+                       );
+                       $conds = '';
+               }
 
-               if ( $res ) {
-                       $row = $this->fetchRow( $res );
-                       $rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
+               if ( !is_array( $conds ) ) {
+                       $conds = ( $conds === '' ) ? [] : [ $conds ];
                }
 
-               return $rows;
+               return $conds;
+       }
+
+       /**
+        * @param array|string $var Field parameter in the style of select()
+        * @return string|null Column name or null; ignores aliases
+        * @throws DBUnexpectedError Errors out if multiple columns are given
+        */
+       final protected function extractSingleFieldFromList( $var ) {
+               if ( is_array( $var ) ) {
+                       if ( !$var ) {
+                               $column = null;
+                       } elseif ( count( $var ) == 1 ) {
+                               $column = isset( $var[0] ) ? $var[0] : reset( $var );
+                       } else {
+                               throw new DBUnexpectedError( $this, __METHOD__ . ': got multiple columns.' );
+                       }
+               } else {
+                       $column = $var;
+               }
+
+               return $column;
        }
 
        /**
@@ -1963,6 +2023,15 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                return 'CAST( ' . $field . ' AS INTEGER )';
        }
 
+       public function buildSelectSubquery(
+               $table, $vars, $conds = '', $fname = __METHOD__,
+               $options = [], $join_conds = []
+       ) {
+               return new Subquery(
+                       $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds )
+               );
+       }
+
        public function databasesAreIndependent() {
                return false;
        }
@@ -1985,6 +2054,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        public function tableName( $name, $format = 'quoted' ) {
+               if ( $name instanceof Subquery ) {
+                       throw new DBUnexpectedError(
+                               $this,
+                               __METHOD__ . ': got Subquery instance when expecting a string.'
+                       );
+               }
+
                # Skip the entire process when we have a string quoted on both ends.
                # Note that we check the end so that we will still quote any use of
                # use of `database`.table. But won't break things if someone wants
@@ -2001,6 +2077,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                # any remote case where a word like on may be inside of a table name
                # surrounded by symbols which may be considered word breaks.
                if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
+                       $this->queryLogger->warning(
+                               __METHOD__ . ": use of subqueries is not supported this way.",
+                               [ 'trace' => ( new RuntimeException() )->getTraceAsString() ]
+                       );
+
                        return $name;
                }
 
@@ -2105,17 +2186,32 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
 
        /**
         * Get an aliased table name
-        * e.g. tableName AS newTableName
         *
-        * @param string $name Table name, see tableName()
-        * @param string|bool $alias Alias (optional)
+        * This returns strings like "tableName AS newTableName" for aliased tables
+        * and "(SELECT * from tableA) newTablename" for subqueries (e.g. derived tables)
+        *
+        * @see Database::tableName()
+        * @param string|Subquery $table Table name or object with a 'sql' field
+        * @param string|bool $alias Table alias (optional)
         * @return string SQL name for aliased table. Will not alias a table to its own name
         */
-       protected function tableNameWithAlias( $name, $alias = false ) {
-               if ( !$alias || $alias == $name ) {
-                       return $this->tableName( $name );
+       protected function tableNameWithAlias( $table, $alias = false ) {
+               if ( is_string( $table ) ) {
+                       $quotedTable = $this->tableName( $table );
+               } elseif ( $table instanceof Subquery ) {
+                       $quotedTable = (string)$table;
+               } else {
+                       throw new InvalidArgumentException( "Table must be a string or Subquery." );
+               }
+
+               if ( !strlen( $alias ) || $alias === $table ) {
+                       if ( $table instanceof Subquery ) {
+                               throw new InvalidArgumentException( "Subquery table missing alias." );
+                       }
+
+                       return $quotedTable;
                } else {
-                       return $this->tableName( $name ) . ' ' . $this->addIdentifierQuotes( $alias );
+                       return $quotedTable . ' ' . $this->addIdentifierQuotes( $alias );
                }
        }
 
@@ -2414,7 +2510,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                }
 
                try {
-                       $this->startAtomic( $fname );
+                       $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
                        $affectedRowCount = 0;
                        foreach ( $rows as $row ) {
                                // Delete rows which collide with this one
@@ -2450,7 +2546,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $this->endAtomic( $fname );
                        $this->affectedRowCount = $affectedRowCount;
                } catch ( Exception $e ) {
-                       $this->rollback( $fname, self::FLUSHING_INTERNAL );
+                       $this->cancelAtomic( $fname );
                        throw $e;
                }
        }
@@ -2519,7 +2615,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
 
                $affectedRowCount = 0;
                try {
-                       $this->startAtomic( $fname );
+                       $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
                        # Update any existing conflicting row(s)
                        if ( $where !== false ) {
                                $ok = $this->update( $table, $set, $where, $fname );
@@ -2533,7 +2629,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $this->endAtomic( $fname );
                        $this->affectedRowCount = $affectedRowCount;
                } catch ( Exception $e ) {
-                       $this->rollback( $fname, self::FLUSHING_INTERNAL );
+                       $this->cancelAtomic( $fname );
                        throw $e;
                }
 
@@ -2675,7 +2771,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
 
                try {
                        $affectedRowCount = 0;
-                       $this->startAtomic( $fname );
+                       $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
                        $rows = [];
                        $ok = true;
                        foreach ( $res as $row ) {
@@ -2701,11 +2797,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                $this->endAtomic( $fname );
                                $this->affectedRowCount = $affectedRowCount;
                        } else {
-                               $this->rollback( $fname, self::FLUSHING_INTERNAL );
+                               $this->cancelAtomic( $fname );
                        }
                        return $ok;
                } catch ( Exception $e ) {
-                       $this->rollback( $fname, self::FLUSHING_INTERNAL );
+                       $this->cancelAtomic( $fname );
                        throw $e;
                }
        }
@@ -2991,12 +3087,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $this->trxPreCommitCallbacks[] = [ $callback, $fname ];
                } else {
                        // No transaction is active nor will start implicitly, so make one for this callback
-                       $this->startAtomic( __METHOD__ );
+                       $this->startAtomic( __METHOD__, self::ATOMIC_CANCELABLE );
                        try {
                                call_user_func( $callback );
                                $this->endAtomic( __METHOD__ );
                        } catch ( Exception $e ) {
-                               $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
+                               $this->cancelAtomic( __METHOD__ );
                                throw $e;
                        }
                }
@@ -3133,7 +3229,52 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                }
        }
 
-       final public function startAtomic( $fname = __METHOD__ ) {
+       /**
+        * Create a savepoint
+        *
+        * This is used internally to implement atomic sections. It should not be
+        * used otherwise.
+        *
+        * @since 1.31
+        * @param string $identifier Identifier for the savepoint
+        * @param string $fname Calling function name
+        */
+       protected function doSavepoint( $identifier, $fname ) {
+               $this->query( 'SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
+       }
+
+       /**
+        * Release a savepoint
+        *
+        * This is used internally to implement atomic sections. It should not be
+        * used otherwise.
+        *
+        * @since 1.31
+        * @param string $identifier Identifier for the savepoint
+        * @param string $fname Calling function name
+        */
+       protected function doReleaseSavepoint( $identifier, $fname ) {
+               $this->query( 'RELEASE SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
+       }
+
+       /**
+        * Rollback to a savepoint
+        *
+        * This is used internally to implement atomic sections. It should not be
+        * used otherwise.
+        *
+        * @since 1.31
+        * @param string $identifier Identifier for the savepoint
+        * @param string $fname Calling function name
+        */
+       protected function doRollbackToSavepoint( $identifier, $fname ) {
+               $this->query( 'ROLLBACK TO SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
+       }
+
+       final public function startAtomic(
+               $fname = __METHOD__, $cancelable = self::ATOMIC_NOT_CANCELABLE
+       ) {
+               $savepointId = $cancelable === self::ATOMIC_CANCELABLE ? 'n/a' : null;
                if ( !$this->trxLevel ) {
                        $this->begin( $fname, self::TRANSACTION_INTERNAL );
                        // If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
@@ -3141,32 +3282,70 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        if ( !$this->getFlag( self::DBO_TRX ) ) {
                                $this->trxAutomaticAtomic = true;
                        }
+               } elseif ( $cancelable === self::ATOMIC_CANCELABLE ) {
+                       $savepointId = 'wikimedia_rdbms_atomic' . ++$this->trxAtomicCounter;
+                       if ( strlen( $savepointId ) > 30 ) { // 30 == Oracle's identifier length limit (pre 12c)
+                               $this->queryLogger->warning(
+                                       'There have been an excessively large number of atomic sections in a transaction'
+                                       . " started by $this->trxFname, reusing IDs (at $fname)",
+                                       [ 'trace' => ( new RuntimeException() )->getTraceAsString() ]
+                               );
+                               $this->trxAtomicCounter = 0;
+                               $savepointId = 'wikimedia_rdbms_atomic' . ++$this->trxAtomicCounter;
+                       }
+                       $this->doSavepoint( $savepointId, $fname );
                }
 
-               $this->trxAtomicLevels[] = $fname;
+               $this->trxAtomicLevels[] = [ $fname, $savepointId ];
        }
 
        final public function endAtomic( $fname = __METHOD__ ) {
                if ( !$this->trxLevel ) {
                        throw new DBUnexpectedError( $this, "No atomic transaction is open (got $fname)." );
                }
-               if ( !$this->trxAtomicLevels ||
-                       array_pop( $this->trxAtomicLevels ) !== $fname
-               ) {
+
+               list( $savedFname, $savepointId ) = $this->trxAtomicLevels
+                       ? array_pop( $this->trxAtomicLevels ) : [ null, null ];
+               if ( $savedFname !== $fname ) {
                        throw new DBUnexpectedError( $this, "Invalid atomic section ended (got $fname)." );
                }
 
                if ( !$this->trxAtomicLevels && $this->trxAutomaticAtomic ) {
                        $this->commit( $fname, self::FLUSHING_INTERNAL );
+               } elseif ( $savepointId && $savepointId !== 'n/a' ) {
+                       $this->doReleaseSavepoint( $savepointId, $fname );
                }
        }
 
+       final public function cancelAtomic( $fname = __METHOD__ ) {
+               if ( !$this->trxLevel ) {
+                       throw new DBUnexpectedError( $this, "No atomic transaction is open (got $fname)." );
+               }
+
+               list( $savedFname, $savepointId ) = $this->trxAtomicLevels
+                       ? array_pop( $this->trxAtomicLevels ) : [ null, null ];
+               if ( $savedFname !== $fname ) {
+                       throw new DBUnexpectedError( $this, "Invalid atomic section ended (got $fname)." );
+               }
+               if ( !$savepointId ) {
+                       throw new DBUnexpectedError( $this, "Uncancelable atomic section canceled (got $fname)." );
+               }
+
+               if ( !$this->trxAtomicLevels && $this->trxAutomaticAtomic ) {
+                       $this->rollback( $fname, self::FLUSHING_INTERNAL );
+               } elseif ( $savepointId !== 'n/a' ) {
+                       $this->doRollbackToSavepoint( $savepointId, $fname );
+               }
+
+               $this->affectedRowCount = 0; // for the sake of consistency
+       }
+
        final public function doAtomicSection( $fname, callable $callback ) {
-               $this->startAtomic( $fname );
+               $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
                try {
                        $res = call_user_func_array( $callback, [ $this, $fname ] );
                } catch ( Exception $e ) {
-                       $this->rollback( $fname, self::FLUSHING_INTERNAL );
+                       $this->cancelAtomic( $fname );
                        throw $e;
                }
                $this->endAtomic( $fname );
@@ -3178,7 +3357,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                // Protect against mismatched atomic section, transaction nesting, and snapshot loss
                if ( $this->trxLevel ) {
                        if ( $this->trxAtomicLevels ) {
-                               $levels = implode( ', ', $this->trxAtomicLevels );
+                               $levels = array_reduce( $this->trxAtomicLevels, function ( $accum, $v ) {
+                                       return $accum === null ? $v[0] : "$accum, " . $v[0];
+                               } );
                                $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
                                throw new DBUnexpectedError( $this, $msg );
                        } elseif ( !$this->trxAutomatic ) {
@@ -3197,6 +3378,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                $this->assertOpen();
 
                $this->doBegin( $fname );
+               $this->trxAtomicCounter = 0;
                $this->trxTimestamp = microtime( true );
                $this->trxFname = $fname;
                $this->trxDoneWrites = false;
@@ -3210,10 +3392,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                $this->trxWriteAdjQueryCount = 0;
                $this->trxWriteCallers = [];
                // First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
-               // Get an estimate of the replica DB lag before then, treating estimate staleness
-               // as lag itself just to be safe
-               $status = $this->getApproximateLagStatus();
-               $this->trxReplicaLag = $status['lag'] + ( microtime( true ) - $status['since'] );
+               // Get an estimate of the replication lag before any such queries.
+               $this->trxReplicaLag = $this->getApproximateLagStatus()['lag'];
                // T147697: make explicitTrxActive() return true until begin() finishes. This way, no
                // caller will think its OK to muck around with the transaction just because startAtomic()
                // has not yet completed (e.g. setting trxAtomicLevels).
@@ -3234,7 +3414,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        final public function commit( $fname = __METHOD__, $flush = '' ) {
                if ( $this->trxLevel && $this->trxAtomicLevels ) {
                        // There are still atomic sections open. This cannot be ignored
-                       $levels = implode( ', ', $this->trxAtomicLevels );
+                       $levels = array_reduce( $this->trxAtomicLevels, function ( $accum, $v ) {
+                               return $accum === null ? $v[0] : "$accum, " . $v[0];
+                       } );
                        throw new DBUnexpectedError(
                                $this,
                                "$fname: Got COMMIT while atomic sections $levels are still open."
@@ -3298,16 +3480,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        final public function rollback( $fname = __METHOD__, $flush = '' ) {
-               if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
-                       if ( !$this->trxLevel ) {
-                               return; // nothing to do
-                       }
-               } else {
-                       if ( !$this->trxLevel ) {
-                               $this->queryLogger->error(
-                                       "$fname: No transaction to rollback, something got out of sync." );
-                               return; // nothing to do
-                       } elseif ( $this->getFlag( self::DBO_TRX ) ) {
+               $trxActive = $this->trxLevel;
+
+               if ( $flush !== self::FLUSHING_INTERNAL && $flush !== self::FLUSHING_ALL_PEERS ) {
+                       if ( $this->getFlag( self::DBO_TRX ) ) {
                                throw new DBUnexpectedError(
                                        $this,
                                        "$fname: Expected mass rollback of all peer transactions (DBO_TRX set)."
@@ -3315,33 +3491,40 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        }
                }
 
-               // Avoid fatals if close() was called
-               $this->assertOpen();
+               if ( $trxActive ) {
+                       // Avoid fatals if close() was called
+                       $this->assertOpen();
 
-               $this->doRollback( $fname );
-               $this->trxAtomicLevels = [];
-               if ( $this->trxDoneWrites ) {
-                       $this->trxProfiler->transactionWritingOut(
-                               $this->server,
-                               $this->dbName,
-                               $this->trxShortId
-                       );
+                       $this->doRollback( $fname );
+                       $this->trxAtomicLevels = [];
+                       if ( $this->trxDoneWrites ) {
+                               $this->trxProfiler->transactionWritingOut(
+                                       $this->server,
+                                       $this->dbName,
+                                       $this->trxShortId
+                               );
+                       }
                }
 
-               $this->trxIdleCallbacks = []; // clear
-               $this->trxPreCommitCallbacks = []; // clear
-               try {
-                       $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
-               } catch ( Exception $e ) {
-                       // already logged; finish and let LoadBalancer move on during mass-rollback
-               }
-               try {
-                       $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
-               } catch ( Exception $e ) {
-                       // already logged; let LoadBalancer move on during mass-rollback
-               }
+               // Clear any commit-dependant callbacks. They might even be present
+               // only due to transaction rounds, with no SQL transaction being active
+               $this->trxIdleCallbacks = [];
+               $this->trxPreCommitCallbacks = [];
 
-               $this->affectedRowCount = 0; // for the sake of consistency
+               if ( $trxActive ) {
+                       try {
+                               $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
+                       } catch ( Exception $e ) {
+                               // already logged; finish and let LoadBalancer move on during mass-rollback
+                       }
+                       try {
+                               $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
+                       } catch ( Exception $e ) {
+                               // already logged; let LoadBalancer move on during mass-rollback
+                       }
+
+                       $this->affectedRowCount = 0; // for the sake of consistency
+               }
        }
 
        /**