Merge "Never prefix table names with $wgSharedDB when used in foreign DB"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Fri, 9 Aug 2013 00:19:48 +0000 (00:19 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 9 Aug 2013 00:19:48 +0000 (00:19 +0000)
1  2 
includes/db/Database.php
includes/db/LoadBalancer.php

diff --combined includes/db/Database.php
@@@ -184,7 -184,7 +184,7 @@@ interface DatabaseType 
         *
         * @return string: wikitext of a link to the server software's web site
         */
 -      static function getSoftwareLink();
 +      function getSoftwareLink();
  
        /**
         * A string describing the current software version, like from
        function getServerInfo();
  }
  
 +/**
 + * Interface for classes that implement or wrap DatabaseBase
 + * @ingroup Database
 + */
 +interface IDatabase {}
 +
  /**
   * Database abstraction object
   * @ingroup Database
   */
 -abstract class DatabaseBase implements DatabaseType {
 +abstract class DatabaseBase implements IDatabase, DatabaseType {
        /** Number of times to re-try an operation in case of deadlock */
        const DEADLOCK_TRIES = 4;
        /** Minimum time to wait before retry, in microseconds */
        protected $mConn = null;
        protected $mOpened = false;
  
 -      /** @var array of Closure */
 +      /** @var callable[] */
        protected $mTrxIdleCallbacks = array();
 -      /** @var array of Closure */
 +      /** @var callable[] */
        protected $mTrxPreCommitCallbacks = array();
  
        protected $mTablePrefix;
        protected $mFlags;
+       protected $mForeign;
        protected $mTrxLevel = 0;
        protected $mErrorCount = 0;
        protected $mLBInfo = array();
         * @param string $dbName database name
         * @param $flags
         * @param string $tablePrefix database table prefixes. By default use the prefix gave in LocalSettings.php
+        * @param bool $foreign disable some operations specific to local databases
         */
        function __construct( $server = false, $user = false, $password = false, $dbName = false,
-               $flags = 0, $tablePrefix = 'get from global'
+               $flags = 0, $tablePrefix = 'get from global', $foreign = false
        ) {
                global $wgDBprefix, $wgCommandLineMode, $wgDebugDBTransactions;
  
                        $this->mTablePrefix = $tablePrefix;
                }
  
+               $this->mForeign = $foreign;
                if ( $user ) {
                        $this->open( $server, $user, $password, $dbName );
                }
                                isset( $p['password'] ) ? $p['password'] : false,
                                isset( $p['dbname'] ) ? $p['dbname'] : false,
                                isset( $p['flags'] ) ? $p['flags'] : 0,
-                               isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global'
+                               isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global',
+                               isset( $p['foreign'] ) ? $p['foreign'] : false
                        );
                } else {
                        return null;
                # Keep track of whether the transaction has write queries pending
                if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $this->isWriteQuery( $sql ) ) {
                        $this->mTrxDoneWrites = true;
 +                      Profiler::instance()->transactionWritingIn( $this->mServer, $this->mDBname );
                }
  
                if ( $this->debug() ) {
                        $startOpts .= ' SQL_NO_CACHE';
                }
  
 -              if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
 +              if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
                        $useIndex = $this->useIndexClause( $options['USE INDEX'] );
                } else {
                        $useIndex = '';
                }
  
                $options = (array)$options;
 +              $useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
 +                      ? $options['USE INDEX']
 +                      : array();
  
                if ( is_array( $table ) ) {
 -                      $useIndex = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
 -                              ? $options['USE INDEX']
 -                              : array();
 -                      if ( count( $join_conds ) || count( $useIndex ) ) {
 -                              $from = ' FROM ' .
 -                                      $this->tableNamesWithUseIndexOrJOIN( $table, $useIndex, $join_conds );
 -                      } else {
 -                              $from = ' FROM ' . implode( ',', $this->tableNamesWithAlias( $table ) );
 -                      }
 +                      $from = ' FROM ' .
 +                              $this->tableNamesWithUseIndexOrJOIN( $table, $useIndexes, $join_conds );
                } elseif ( $table != '' ) {
                        if ( $table[0] == ' ' ) {
                                $from = ' FROM ' . $table;
                        } else {
 -                              $from = ' FROM ' . $this->tableName( $table );
 +                              $from = ' FROM ' .
 +                                      $this->tableNamesWithUseIndexOrJOIN( array( $table ), $useIndexes, array() );
                        }
                } else {
                        $from = '';
                }
  
 -              list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options );
 +              list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) =
 +                      $this->makeSelectOptions( $options );
  
                if ( !empty( $conds ) ) {
                        if ( is_array( $conds ) ) {
                } else {
                        list( $table ) = $dbDetails;
                        if ( $wgSharedDB !== null # We have a shared database
+                               && $this->mForeign == false # We're not working on a foreign database
                                && !$this->isQuotedIdentifier( $table ) # Paranoia check to prevent shared tables listing '`table`'
                                && in_array( $table, $wgSharedTables ) # A shared table is selected
                        ) {
                }
  
                // We can't separate explicit JOIN clauses with ',', use ' ' for those
 -              $straightJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
 -              $otherJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
 +              $implicitJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
 +              $explicitJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
  
                // Compile our final table clause
 -              return implode( ' ', array( $straightJoins, $otherJoins ) );
 +              return implode( ' ', array( $implicitJoins, $explicitJoins ) );
        }
  
        /**
                return $this->query( $sql, $fname );
        }
  
 +      /**
 +       * INSERT ON DUPLICATE KEY UPDATE wrapper, upserts an array into a table.
 +       *
 +       * This updates any conflicting rows (according to the unique indexes) using
 +       * the provided SET clause and inserts any remaining (non-conflicted) rows.
 +       *
 +       * $rows may be either:
 +       *   - A single associative array. The array keys are the field names, and
 +       *     the values are the values to insert. The values are treated as data
 +       *     and will be quoted appropriately. If NULL is inserted, this will be
 +       *     converted to a database NULL.
 +       *   - An array with numeric keys, holding a list of associative arrays.
 +       *     This causes a multi-row INSERT on DBMSs that support it. The keys in
 +       *     each subarray must be identical to each other, and in the same order.
 +       *
 +       * It may be more efficient to leave off unique indexes which are unlikely
 +       * to collide. However if you do this, you run the risk of encountering
 +       * errors which wouldn't have occurred in MySQL.
 +       *
 +       * Usually throws a DBQueryError on failure. If errors are explicitly ignored,
 +       * returns success.
 +       *
 +       * @param string $table Table name. This will be passed through DatabaseBase::tableName().
 +       * @param array $rows A single row or list of rows to insert
 +       * @param array $uniqueIndexes List of single field names or field name tuples
 +       * @param array $set An array of values to SET. For each array element,
 +       *                the key gives the field name, and the value gives the data
 +       *                to set that field to. The data will be quoted by
 +       *                DatabaseBase::addQuotes().
 +       * @param string $fname Calling function name (use __METHOD__) for logs/profiling
 +       * @param array $options of options
 +       *
 +       * @return bool
 +       * @since 1.22
 +       */
 +      public function upsert(
 +              $table, array $rows, array $uniqueIndexes, array $set, $fname = __METHOD__
 +      ) {
 +              if ( !count( $rows ) ) {
 +                      return true; // nothing to do
 +              }
 +              $rows = is_array( reset( $rows ) ) ? $rows : array( $rows );
 +
 +              if ( count( $uniqueIndexes ) ) {
 +                      $clauses = array(); // list WHERE clauses that each identify a single row
 +                      foreach ( $rows as $row ) {
 +                              foreach ( $uniqueIndexes as $index ) {
 +                                      $index = is_array( $index ) ? $index : array( $index ); // columns
 +                                      $rowKey = array(); // unique key to this row
 +                                      foreach ( $index as $column ) {
 +                                              $rowKey[$column] = $row[$column];
 +                                      }
 +                                      $clauses[] = $this->makeList( $rowKey, LIST_AND );
 +                              }
 +                      }
 +                      $where = array( $this->makeList( $clauses, LIST_OR ) );
 +              } else {
 +                      $where = false;
 +              }
 +
 +              $useTrx = !$this->mTrxLevel;
 +              if ( $useTrx ) {
 +                      $this->begin( $fname );
 +              }
 +              try {
 +                      # Update any existing conflicting row(s)
 +                      if ( $where !== false ) {
 +                              $ok = $this->update( $table, $set, $where, $fname );
 +                      } else {
 +                              $ok = true;
 +                      }
 +                      # Now insert any non-conflicting row(s)
 +                      $ok = $this->insert( $table, $rows, $fname, array( 'IGNORE' ) ) && $ok;
 +              } catch ( Exception $e ) {
 +                      if ( $useTrx ) {
 +                              $this->rollback( $fname );
 +                      }
 +                      throw $e;
 +              }
 +              if ( $useTrx ) {
 +                      $this->commit( $fname );
 +              }
 +
 +              return $ok;
 +      }
 +
        /**
         * DELETE where the condition is a join.
         *
                $sql = "DELETE FROM $table";
  
                if ( $conds != '*' ) {
 -                      $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
 +                      if ( is_array( $conds ) ) {
 +                              $conds = $this->makeList( $conds, LIST_AND );
 +                      }
 +                      $sql .= ' WHERE ' . $conds;
                }
  
                return $this->query( $sql, $fname );
         * after the database is updated so that the jobs will see the data when they actually run.
         * It can also be used for updates that easily cause deadlocks if locks are held too long.
         *
 -       * @param Closure $callback
 +       * @param callable $callback
         * @since 1.20
         */
 -      final public function onTransactionIdle( Closure $callback ) {
 +      final public function onTransactionIdle( $callback ) {
                $this->mTrxIdleCallbacks[] = $callback;
                if ( !$this->mTrxLevel ) {
                        $this->runOnTransactionIdleCallbacks();
         * This is useful for updates that easily cause deadlocks if locks are held too long
         * but where atomicity is strongly desired for these updates and some related updates.
         *
 -       * @param Closure $callback
 +       * @param callable $callback
         * @since 1.22
         */
 -      final public function onTransactionPreCommitOrIdle( Closure $callback ) {
 +      final public function onTransactionPreCommitOrIdle( $callback ) {
                if ( $this->mTrxLevel ) {
                        $this->mTrxPreCommitCallbacks[] = $callback;
                } else {
  
                        $this->runOnTransactionPreCommitCallbacks();
                        $this->doCommit( $fname );
 +                      if ( $this->mTrxDoneWrites ) {
 +                              Profiler::instance()->transactionWritingOut( $this->mServer, $this->mDBname );
 +                      }
                        $this->runOnTransactionIdleCallbacks();
                }
  
  
                $this->runOnTransactionPreCommitCallbacks();
                $this->doCommit( $fname );
 +              if ( $this->mTrxDoneWrites ) {
 +                      Profiler::instance()->transactionWritingOut( $this->mServer, $this->mDBname );
 +              }
                $this->runOnTransactionIdleCallbacks();
        }
  
                $this->doRollback( $fname );
                $this->mTrxIdleCallbacks = array(); // cancel
                $this->mTrxPreCommitCallbacks = array(); // cancel
 +              if ( $this->mTrxDoneWrites ) {
 +                      Profiler::instance()->transactionWritingOut( $this->mServer, $this->mDBname );
 +              }
        }
  
        /**
@@@ -117,7 -117,7 +117,7 @@@ class LoadBalancer 
         * Given an array of non-normalised probabilities, this function will select
         * an element and return the appropriate key
         *
 -       * @deprecated 1.21, use ArrayUtils::pickRandom()
 +       * @deprecated since 1.21, use ArrayUtils::pickRandom()
         *
         * @param $weights array
         *
  
                # Operation-based index
                if ( $i == DB_SLAVE ) {
 +                      $this->mLastError = 'Unknown error'; // reset error string
                        $i = $this->getReaderIndex( false, $wiki );
                        # Couldn't find a working server in getReaderIndex()?
                        if ( $i === false ) {
                }
        }
  
 +      /**
 +       * Get a database connection handle reference
 +       *
 +       * The handle's methods wrap simply wrap those of a DatabaseBase handle
 +       *
 +       * @see LoadBalancer::getConnection() for parameter information
 +       *
 +       * @param integer $db
 +       * @param mixed $groups
 +       * @param string $wiki
 +       * @return DBConnRef
 +       */
 +      public function getConnectionRef( $db, $groups = array(), $wiki = false ) {
 +              return new DBConnRef( $this, $this->getConnection( $db, $groups, $wiki ) );
 +      }
 +
        /**
         * Open a connection to the server given by the specified index
         * Index must be an actual index into the array.
                        $server = $this->mServers[$i];
                        $server['serverIndex'] = $i;
                        $server['foreignPoolRefCount'] = 0;
+                       $server['foreign'] = true;
                        $conn = $this->reallyOpenConnection( $server, $dbName );
                        if ( !$conn->isOpen() ) {
                                wfDebug( __METHOD__ . ": error opening connection for $i/$wiki\n" );
                $this->mLagTimes = null;
        }
  }
 +
 +/**
 + * Helper class to handle automatically marking connectons as reusable (via RAII pattern)
 + *
 + * @ingroup Database
 + * @since 1.22
 + */
 +class DBConnRef implements IDatabase {
 +      /** @var LoadBalancer */
 +      protected $lb;
 +      /** @var DatabaseBase */
 +      protected $conn;
 +
 +      /**
 +       * @param $lb LoadBalancer
 +       * @param $conn DatabaseBase
 +       */
 +      public function __construct( LoadBalancer $lb, DatabaseBase $conn ) {
 +              $this->lb = $lb;
 +              $this->conn = $conn;
 +      }
 +
 +      public function __call( $name, $arguments ) {
 +              return call_user_func_array( array( $this->conn, $name ), $arguments );
 +      }
 +
 +      function __destruct() {
 +              $this->lb->reuseConnection( $this->conn );
 +      }
 +}