);
$class = MWLBFactory::getLBFactoryClass( $lbConf );
- return new $class( $lbConf );
+ $instance = new $class( $lbConf );
+ MWLBFactory::setSchemaAliases( $instance );
+
+ return $instance;
},
'DBLoadBalancer' => function ( MediaWikiServices $services ) {
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\MediaWikiServices;
+use Wikimedia\Rdbms\LBFactory;
use Wikimedia\Rdbms\DatabaseDomain;
/**
return $class;
}
+
+ public static function setSchemaAliases( LBFactory $lbFactory ) {
+ $mainLB = $lbFactory->getMainLB();
+ $masterType = $mainLB->getServerType( $mainLB->getWriterIndex() );
+ if ( $masterType === 'mysql' ) {
+ /**
+ * When SQLite indexes were introduced in r45764, it was noted that
+ * SQLite requires index names to be unique within the whole database,
+ * not just within a schema. As discussed in CR r45819, to avoid the
+ * need for a schema change on existing installations, the indexes
+ * were implicitly mapped from the new names to the old names.
+ *
+ * This mapping can be removed if DB patches are introduced to alter
+ * the relevant tables in existing installations. Note that because
+ * this index mapping applies to table creation, even new installations
+ * of MySQL have the old names (except for installations created during
+ * a period where this mapping was inappropriately removed, see
+ * T154872).
+ */
+ $lbFactory->setIndexAliases( [
+ 'ar_usertext_timestamp' => 'usertext_timestamp',
+ 'un_user_id' => 'user_id',
+ 'un_user_ip' => 'user_ip',
+ ] );
+ }
+ }
}
return $this->__call( __FUNCTION__, func_get_args() );
}
+ public function setIndexAliases( array $aliases ) {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
+
/**
* Clean up the connection when out of scope
*/
protected $password;
/** @var string */
protected $dbName;
- /** @var array[] $aliases Map of (table => (dbname, schema, prefix) map) */
+ /** @var array[] Map of (table => (dbname, schema, prefix) map) */
protected $tableAliases = [];
+ /** @var string[] Map of (index alias => index) */
+ protected $indexAliases = [];
/** @var bool Whether this PHP instance is for a CLI script */
protected $cliMode;
/** @var string Agent name for query profiling */
* @return string
*/
protected function indexName( $index ) {
- return $index;
+ return isset( $this->indexAliases[$index] )
+ ? $this->indexAliases[$index]
+ : $index;
}
public function addQuotes( $s ) {
$this->tableAliases = $aliases;
}
+ public function setIndexAliases( array $aliases ) {
+ $this->indexAliases = $aliases;
+ }
+
/**
* @return bool Whether a DB user is required to access the DB
* @since 1.28
return in_array( $name, $this->listViews( $prefix ) );
}
- /**
- * Allows for index remapping in queries where this is not consistent across DBMS
- *
- * @param string $index
- * @return string
- */
- protected function indexName( $index ) {
- /**
- * When SQLite indexes were introduced in r45764, it was noted that
- * SQLite requires index names to be unique within the whole database,
- * not just within a schema. As discussed in CR r45819, to avoid the
- * need for a schema change on existing installations, the indexes
- * were implicitly mapped from the new names to the old names.
- *
- * This mapping can be removed if DB patches are introduced to alter
- * the relevant tables in existing installations. Note that because
- * this index mapping applies to table creation, even new installations
- * of MySQL have the old names (except for installations created during
- * a period where this mapping was inappropriately removed, see
- * T154872).
- */
- $renamed = [
- 'ar_usertext_timestamp' => 'usertext_timestamp',
- 'un_user_id' => 'user_id',
- 'un_user_ip' => 'user_ip',
- ];
-
- if ( isset( $renamed[$index] ) ) {
- return $renamed[$index];
- } else {
- return $index;
- }
- }
-
protected function isTransactableQuery( $sql ) {
return parent::isTransactableQuery( $sql ) &&
!preg_match( '/^SELECT\s+(GET|RELEASE|IS_FREE)_LOCK\(/', $sql );
* @since 1.28
*/
public function setTableAliases( array $aliases );
+
+ /**
+ * Convert certain index names to alternative names before querying the DB
+ *
+ * Note that this applies to indexes regardless of the table they belong to.
+ *
+ * This can be employed when an index was renamed X => Y in code, but the new Y-named
+ * indexes were not yet built on all DBs. After all the Y-named ones are added by the DBA,
+ * the aliases can be removed, and then the old X-named indexes dropped.
+ *
+ * @param string[] $aliases
+ * @return mixed
+ * @since 1.31
+ */
+ public function setIndexAliases( array $aliases );
}
class_alias( IDatabase::class, 'IDatabase' );
*
* @param array $conf Array with keys:
* - localDomain: A DatabaseDomain or domain ID string.
- * - readOnlyReason : Reason the master DB is read-only if so [optional]
- * - srvCache : BagOStuff object for server cache [optional]
- * - memStash : BagOStuff object for cross-datacenter memory storage [optional]
- * - wanCache : WANObjectCache object [optional]
- * - hostname : The name of the current server [optional]
+ * - readOnlyReason: Reason the master DB is read-only if so [optional]
+ * - srvCache: BagOStuff object for server cache [optional]
+ * - memStash: BagOStuff object for cross-datacenter memory storage [optional]
+ * - wanCache: WANObjectCache object [optional]
+ * - hostname: The name of the current server [optional]
* - cliMode: Whether the execution context is a CLI script. [optional]
- * - profiler : Class name or instance with profileIn()/profileOut() methods. [optional]
+ * - profiler: Class name or instance with profileIn()/profileOut() methods. [optional]
* - trxProfiler: TransactionProfiler instance. [optional]
* - replLogger: PSR-3 logger instance. [optional]
* - connLogger: PSR-3 logger instance. [optional]
* - queryLogger: PSR-3 logger instance. [optional]
* - perfLogger: PSR-3 logger instance. [optional]
- * - errorLogger : Callback that takes an Exception and logs it. [optional]
+ * - errorLogger: Callback that takes an Exception and logs it. [optional]
* @throws InvalidArgumentException
*/
public function __construct( array $conf );
* - ChronologyPositionIndex: timestamp used to get up-to-date DB positions for the agent
*/
public function setRequestInfo( array $info );
+
+ /**
+ * Make certain table names use their own database, schema, and table prefix
+ * when passed into SQL queries pre-escaped and without a qualified database name
+ *
+ * For example, "user" can be converted to "myschema.mydbname.user" for convenience.
+ * Appearances like `user`, somedb.user, somedb.someschema.user will used literally.
+ *
+ * Calling this twice will completely clear any old table aliases. Also, note that
+ * callers are responsible for making sure the schemas and databases actually exist.
+ *
+ * @param array[] $aliases Map of (table => (dbname, schema, prefix) map)
+ * @since 1.31
+ */
+ public function setTableAliases( array $aliases );
+
+ /**
+ * Convert certain index names to alternative names before querying the DB
+ *
+ * Note that this applies to indexes regardless of the table they belong to.
+ *
+ * This can be employed when an index was renamed X => Y in code, but the new Y-named
+ * indexes were not yet built on all DBs. After all the Y-named ones are added by the DBA,
+ * the aliases can be removed, and then the old X-named indexes dropped.
+ *
+ * @param string[] $aliases
+ * @return mixed
+ * @since 1.31
+ */
+ public function setIndexAliases( array $aliases );
}
/** @var callable[] */
protected $replicationWaitCallbacks = [];
+ /** @var array[] $aliases Map of (table => (dbname, schema, prefix) map) */
+ protected $tableAliases = [];
+ /** @var string[] Map of (index alias => index) */
+ protected $indexAliases = [];
+
/** @var bool Whether this PHP instance is for a CLI script */
protected $cliMode;
/** @var string Agent name for query profiling */
if ( $this->trxRoundId !== false ) {
$lb->beginMasterChanges( $this->trxRoundId ); // set DBO_TRX
}
+
+ $lb->setTableAliases( $this->tableAliases );
+ $lb->setIndexAliases( $this->indexAliases );
+ }
+
+ public function setTableAliases( array $aliases ) {
+ $this->tableAliases = $aliases;
+ }
+
+ public function setIndexAliases( array $aliases ) {
+ $this->indexAliases = $aliases;
}
public function setDomainPrefix( $prefix ) {
* @param array[] $aliases Map of (table => (dbname, schema, prefix) map)
*/
public function setTableAliases( array $aliases );
+
+ /**
+ * Convert certain index names to alternative names before querying the DB
+ *
+ * Note that this applies to indexes regardless of the table they belong to.
+ *
+ * This can be employed when an index was renamed X => Y in code, but the new Y-named
+ * indexes were not yet built on all DBs. After all the Y-named ones are added by the DBA,
+ * the aliases can be removed, and then the old X-named indexes dropped.
+ *
+ * @param string[] $aliases
+ * @return mixed
+ * @since 1.31
+ */
+ public function setIndexAliases( array $aliases );
}
private $loadMonitorConfig;
/** @var array[] $aliases Map of (table => (dbname, schema, prefix) map) */
private $tableAliases = [];
+ /** @var string[] Map of (index alias => index) */
+ private $indexAliases = [];
/** @var ILoadMonitor */
private $loadMonitor;
$this->getLazyConnectionRef( self::DB_MASTER, [], $db->getDomainID() )
);
$db->setTableAliases( $this->tableAliases );
+ $db->setIndexAliases( $this->indexAliases );
if ( $server['serverIndex'] === $this->getWriterIndex() ) {
if ( $this->trxRoundId !== false ) {
$this->tableAliases = $aliases;
}
+ public function setIndexAliases( array $aliases ) {
+ $this->indexAliases = $aliases;
+ }
+
public function setDomainPrefix( $prefix ) {
// Find connections to explicit foreign domains still marked as in-use...
$domainsInUse = [];
$this->assertSame( 'CAST( fieldName AS SIGNED )', $output );
}
+ /*
+ * @covers Wikimedia\Rdbms\Database::setIndexAliases
+ */
+ public function testIndexAliases() {
+ $db = $this->getMockBuilder( DatabaseMysqli::class )
+ ->disableOriginalConstructor()
+ ->setMethods( [ 'mysqlRealEscapeString' ] )
+ ->getMock();
+ $db->method( 'mysqlRealEscapeString' )->willReturnCallback(
+ function ( $s ) {
+ return str_replace( "'", "\\'", $s );
+ }
+ );
+
+ $db->setIndexAliases( [ 'a_b_idx' => 'a_c_idx' ] );
+ $sql = $db->selectSQLText(
+ 'zend', 'field', [ 'a' => 'x' ], __METHOD__, [ 'USE INDEX' => 'a_b_idx' ] );
+
+ $this->assertEquals(
+ "SELECT field FROM `zend` FORCE INDEX (a_c_idx) WHERE a = 'x' ",
+ $sql
+ );
+
+ $db->setIndexAliases( [] );
+ $sql = $db->selectSQLText(
+ 'zend', 'field', [ 'a' => 'x' ], __METHOD__, [ 'USE INDEX' => 'a_b_idx' ] );
+
+ $this->assertEquals(
+ "SELECT field FROM `zend` FORCE INDEX (a_b_idx) WHERE a = 'x' ",
+ $sql
+ );
+ }
+
+ /**
+ * @covers Wikimedia\Rdbms\Database::setTableAliases
+ */
+ public function testTableAliases() {
+ $db = $this->getMockBuilder( DatabaseMysqli::class )
+ ->disableOriginalConstructor()
+ ->setMethods( [ 'mysqlRealEscapeString' ] )
+ ->getMock();
+ $db->method( 'mysqlRealEscapeString' )->willReturnCallback(
+ function ( $s ) {
+ return str_replace( "'", "\\'", $s );
+ }
+ );
+
+ $db->setTableAliases( [
+ 'meow' => [ 'dbname' => 'feline', 'schema' => null, 'prefix' => 'cat_' ]
+ ] );
+ $sql = $db->selectSQLText( 'meow', 'field', [ 'a' => 'x' ], __METHOD__ );
+
+ $this->assertEquals(
+ "SELECT field FROM `feline`.`cat_meow` WHERE a = 'x' ",
+ $sql
+ );
+
+ $db->setTableAliases( [] );
+ $sql = $db->selectSQLText( 'meow', 'field', [ 'a' => 'x' ], __METHOD__ );
+
+ $this->assertEquals(
+ "SELECT field FROM `meow` WHERE a = 'x' ",
+ $sql
+ );
+ }
}