Follow-up 36f4daf32c: allow no-write database connections to use transactions
[lhc/web/wiklou.git] / includes / libs / rdbms / database / Database.php
index c04e167..3d40417 100644 (file)
@@ -236,6 +236,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        /** @var TransactionProfiler */
        protected $trxProfiler;
 
+       /**
+        * @var bool Whether writing is allowed on this connection.
+        *      Should be false for connections to replicas.
+        */
+       protected $allowWrite = true;
+
        /**
         * Constructor and database handle and attempt to connect to the DB server
         *
@@ -277,6 +283,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                $this->connLogger = $params['connLogger'];
                $this->queryLogger = $params['queryLogger'];
                $this->errorLogger = $params['errorLogger'];
+               $this->allowWrite = empty( $params['noWrite'] );
 
                // Set initial dummy domain until open() sets the final DB/prefix
                $this->currentDomain = DatabaseDomain::newUnspecified();
@@ -461,9 +468,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        protected function ignoreErrors( $ignoreErrors = null ) {
                $res = $this->getFlag( self::DBO_IGNORE );
                if ( $ignoreErrors !== null ) {
-                       $ignoreErrors
-                               ? $this->setFlag( self::DBO_IGNORE )
-                               : $this->clearFlag( self::DBO_IGNORE );
+                       // setFlag()/clearFlag() do not allow DBO_IGNORE changes for sanity
+                       if ( $ignoreErrors ) {
+                               $this->mFlags |= self::DBO_IGNORE;
+                       } else {
+                               $this->mFlags &= ~self::DBO_IGNORE;
+                       }
                }
 
                return $res;
@@ -621,6 +631,10 @@ 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." );
+               }
+
                if ( $remember === self::REMEMBER_PRIOR ) {
                        array_push( $this->priorFlags, $this->mFlags );
                }
@@ -628,6 +642,10 @@ 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." );
+               }
+
                if ( $remember === self::REMEMBER_PRIOR ) {
                        array_push( $this->priorFlags, $this->mFlags );
                }
@@ -897,6 +915,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                }
 
                if ( $isWrite ) {
+                       if ( !$this->allowWrite ) {
+                               throw new DBError(
+                                       $this,
+                                       'Write operations are not allowed on this database connection!'
+                               );
+                       }
+
                        # In theory, non-persistent writes are allowed in read-only mode, but due to things
                        # like https://bugs.mysql.com/bug.php?id=33669 that might not work anyway...
                        $reason = $this->getReadOnlyReason();
@@ -945,10 +970,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        # Update state tracking to reflect transaction loss due to disconnection
                        $this->handleSessionLoss();
                        if ( $this->reconnect() ) {
-                               $msg = __METHOD__ . ": lost connection to {$this->getServer()}; reconnected";
-                               $this->connLogger->warning( $msg );
-                               $this->queryLogger->warning(
-                                       "$msg:\n" . ( new RuntimeException() )->getTraceAsString() );
+                               $msg = __METHOD__ . ': lost connection to {dbserver}; reconnected';
+                               $params = [ 'dbserver' => $this->getServer() ];
+                               $this->connLogger->warning( $msg, $params );
+                               $this->queryLogger->warning( $msg, $params +
+                                       [ 'trace' => ( new RuntimeException() )->getTraceAsString() ] );
 
                                if ( !$recoverable ) {
                                        # Callers may catch the exception and continue to use the DB
@@ -958,8 +984,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                        $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
                                }
                        } else {
-                               $msg = __METHOD__ . ": lost connection to {$this->getServer()} permanently";
-                               $this->connLogger->error( $msg );
+                               $msg = __METHOD__ . ': lost connection to {dbserver} permanently';
+                               $this->connLogger->error( $msg, [ 'dbserver' => $this->getServer() ] );
                        }
                }
 
@@ -2018,9 +2044,19 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
 
                        if ( is_array( $table ) ) {
                                // A parenthesized group
-                               $joinedTable = '('
-                                       . $this->tableNamesWithIndexClauseOrJOIN( $table, $use_index, $ignore_index, $join_conds )
-                                       . ')';
+                               if ( count( $table ) > 1 ) {
+                                       $joinedTable = '('
+                                               . $this->tableNamesWithIndexClauseOrJOIN( $table, $use_index, $ignore_index, $join_conds )
+                                               . ')';
+                               } else {
+                                       // Degenerate case
+                                       $innerTable = reset( $table );
+                                       $innerAlias = key( $table );
+                                       $joinedTable = $this->tableNameWithAlias(
+                                               $innerTable,
+                                               is_string( $innerAlias ) ? $innerAlias : $innerTable
+                                       );
+                               }
                        } else {
                                $joinedTable = $this->tableNameWithAlias( $table, $alias );
                        }
@@ -3272,14 +3308,15 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * @see WANObjectCache::getWithSetCallback()
         *
         * @param IDatabase $db1
-        * @param IDatabase $dbs,...
+        * @param IDatabase $db2 [optional]
         * @return array Map of values:
         *   - lag: highest lag of any of the DBs or false on error (e.g. replication stopped)
         *   - since: oldest UNIX timestamp of any of the DB lag estimates
         *   - pending: whether any of the DBs have uncommitted changes
+        * @throws DBError
         * @since 1.27
         */
-       public static function getCacheSetOptions( IDatabase $db1 ) {
+       public static function getCacheSetOptions( IDatabase $db1, IDatabase $db2 = null ) {
                $res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
                foreach ( func_get_args() as $db ) {
                        /** @var IDatabase $db */