use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
use Wikimedia\ScopedCallback;
use Wikimedia\Timestamp\ConvertibleTimestamp;
use Wikimedia;
const SLOW_WRITE_SEC = 0.500;
const SMALL_WRITE_ROWS = 100;
+ /** @var string Whether lock granularity is on the level of the entire database */
+ const ATTR_DB_LEVEL_LOCKING = 'db-level-locking';
+
/** @var string SQL query */
protected $lastQuery = '';
/** @var float|bool UNIX timestamp of last write query */
/** @var TransactionProfiler */
protected $trxProfiler;
+ /** @var int */
+ protected $nonNativeInsertSelectBatchSize = 10000;
+
/**
* Constructor and database handle and attempt to connect to the DB server
*
$this->queryLogger = $params['queryLogger'];
$this->errorLogger = $params['errorLogger'];
+ if ( isset( $params['nonNativeInsertSelectBatchSize'] ) ) {
+ $this->nonNativeInsertSelectBatchSize = $params['nonNativeInsertSelectBatchSize'];
+ }
+
// Set initial dummy domain until open() sets the final DB/prefix
$this->currentDomain = DatabaseDomain::newUnspecified();
*
* This also connects to the database immediately upon object construction
*
- * @param string $dbType A possible DB type (sqlite, mysql, postgres)
+ * @param string $dbType A possible DB type (sqlite, mysql, postgres,...)
* @param array $p Parameter map with keys:
* - host : The hostname of the DB server
* - user : The name of the database user the client operates under
* - cliMode: Whether to consider the execution context that of a CLI script.
* - agent: Optional name used to identify the end-user in query profiling/logging.
* - srvCache: Optional BagOStuff instance to an APC-style cache.
+ * - nonNativeInsertSelectBatchSize: Optional batch size for non-native INSERT SELECT emulation.
* @return Database|null If the database driver or extension cannot be found
* @throws InvalidArgumentException If the database driver or extension cannot be found
* @since 1.18
*/
final public static function factory( $dbType, $p = [] ) {
+ $class = self::getClass( $dbType, isset( $p['driver'] ) ? $p['driver'] : null );
+
+ if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
+ // 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['variables'] = isset( $p['variables'] ) ? $p['variables'] : [];
+ $p['tablePrefix'] = isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : '';
+ $p['schema'] = isset( $p['schema'] ) ? $p['schema'] : '';
+ $p['cliMode'] = isset( $p['cliMode'] )
+ ? $p['cliMode']
+ : ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' );
+ $p['agent'] = isset( $p['agent'] ) ? $p['agent'] : '';
+ if ( !isset( $p['connLogger'] ) ) {
+ $p['connLogger'] = new NullLogger();
+ }
+ if ( !isset( $p['queryLogger'] ) ) {
+ $p['queryLogger'] = new NullLogger();
+ }
+ $p['profiler'] = isset( $p['profiler'] ) ? $p['profiler'] : null;
+ if ( !isset( $p['trxProfiler'] ) ) {
+ $p['trxProfiler'] = new TransactionProfiler();
+ }
+ if ( !isset( $p['errorLogger'] ) ) {
+ $p['errorLogger'] = function ( Exception $e ) {
+ trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
+ };
+ }
+
+ $conn = new $class( $p );
+ } else {
+ $conn = null;
+ }
+
+ return $conn;
+ }
+
+ /**
+ * @param string $dbType A possible DB type (sqlite, mysql, postgres,...)
+ * @param string|null $driver Optional name of a specific DB client driver
+ * @return array Map of (Database::ATTRIBUTE_* constant => value) for all such constants
+ * @throws InvalidArgumentException
+ * @since 1.31
+ */
+ final public static function attributesFromType( $dbType, $driver = null ) {
+ static $defaults = [ self::ATTR_DB_LEVEL_LOCKING => false ];
+
+ $class = self::getClass( $dbType, $driver );
+
+ return call_user_func( [ $class, 'getAttributes' ] ) + $defaults;
+ }
+
+ /**
+ * @param string $dbType A possible DB type (sqlite, mysql, postgres,...)
+ * @param string|null $driver Optional name of a specific DB client driver
+ * @return string Database subclass name to use
+ * @throws InvalidArgumentException
+ */
+ private static function getClass( $dbType, $driver = null ) {
// For database types with built-in support, the below maps type to IDatabase
// implementations. For types with multipe driver implementations (PHP extensions),
// an array can be used, keyed by extension name. In case of an array, the
$dbType = strtolower( $dbType );
$class = false;
+
if ( isset( $builtinTypes[$dbType] ) ) {
$possibleDrivers = $builtinTypes[$dbType];
if ( is_string( $possibleDrivers ) ) {
$class = $possibleDrivers;
} else {
- if ( !empty( $p['driver'] ) ) {
- if ( !isset( $possibleDrivers[$p['driver']] ) ) {
+ if ( (string)$driver !== '' ) {
+ if ( !isset( $possibleDrivers[$driver] ) ) {
throw new InvalidArgumentException( __METHOD__ .
- " type '$dbType' does not support driver '{$p['driver']}'" );
+ " type '$dbType' does not support driver '{$driver}'" );
} else {
- $class = $possibleDrivers[$p['driver']];
+ $class = $possibleDrivers[$driver];
}
} else {
foreach ( $possibleDrivers as $posDriver => $possibleClass ) {
" no viable database extension found for type '$dbType'" );
}
- if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
- // 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['variables'] = isset( $p['variables'] ) ? $p['variables'] : [];
- $p['tablePrefix'] = isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : '';
- $p['schema'] = isset( $p['schema'] ) ? $p['schema'] : '';
- $p['cliMode'] = isset( $p['cliMode'] )
- ? $p['cliMode']
- : ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' );
- $p['agent'] = isset( $p['agent'] ) ? $p['agent'] : '';
- if ( !isset( $p['connLogger'] ) ) {
- $p['connLogger'] = new \Psr\Log\NullLogger();
- }
- if ( !isset( $p['queryLogger'] ) ) {
- $p['queryLogger'] = new \Psr\Log\NullLogger();
- }
- $p['profiler'] = isset( $p['profiler'] ) ? $p['profiler'] : null;
- if ( !isset( $p['trxProfiler'] ) ) {
- $p['trxProfiler'] = new TransactionProfiler();
- }
- if ( !isset( $p['errorLogger'] ) ) {
- $p['errorLogger'] = function ( Exception $e ) {
- trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
- };
- }
-
- $conn = new $class( $p );
- } else {
- $conn = null;
- }
+ return $class;
+ }
- return $conn;
+ /**
+ * @return array Map of (Database::ATTRIBUTE_* constant => value
+ * @since 1.31
+ */
+ protected static function getAttributes() {
+ return [];
}
/**
$this->tableNamesWithIndexClauseOrJOIN(
$table, $useIndexes, $ignoreIndexes, $join_conds );
} elseif ( $table != '' ) {
- if ( $table[0] == ' ' ) {
- $from = ' FROM ' . $table;
- } else {
- $from = ' FROM ' .
- $this->tableNamesWithIndexClauseOrJOIN(
- [ $table ], $useIndexes, $ignoreIndexes, [] );
- }
+ $from = ' FROM ' .
+ $this->tableNamesWithIndexClauseOrJOIN(
+ [ $table ], $useIndexes, $ignoreIndexes, [] );
} else {
$from = '';
}
$rows = [ $rows ];
}
- $affectedRowCount = 0;
- foreach ( $rows as $row ) {
- // Delete rows which collide with this one
- $indexWhereClauses = [];
- foreach ( $uniqueIndexes as $index ) {
- $indexColumns = (array)$index;
- $indexRowValues = array_intersect_key( $row, array_flip( $indexColumns ) );
- if ( count( $indexRowValues ) != count( $indexColumns ) ) {
- throw new DBUnexpectedError(
- $this,
- 'New record does not provide all values for unique key (' .
+ $useTrx = !$this->trxLevel;
+ if ( $useTrx ) {
+ $this->begin( $fname, self::TRANSACTION_INTERNAL );
+ }
+ try {
+ $affectedRowCount = 0;
+ foreach ( $rows as $row ) {
+ // Delete rows which collide with this one
+ $indexWhereClauses = [];
+ foreach ( $uniqueIndexes as $index ) {
+ $indexColumns = (array)$index;
+ $indexRowValues = array_intersect_key( $row, array_flip( $indexColumns ) );
+ if ( count( $indexRowValues ) != count( $indexColumns ) ) {
+ throw new DBUnexpectedError(
+ $this,
+ 'New record does not provide all values for unique key (' .
implode( ', ', $indexColumns ) . ')'
- );
- } elseif ( in_array( null, $indexRowValues, true ) ) {
- throw new DBUnexpectedError(
- $this,
- 'New record has a null value for unique key (' .
+ );
+ } elseif ( in_array( null, $indexRowValues, true ) ) {
+ throw new DBUnexpectedError(
+ $this,
+ 'New record has a null value for unique key (' .
implode( ', ', $indexColumns ) . ')'
- );
+ );
+ }
+ $indexWhereClauses[] = $this->makeList( $indexRowValues, LIST_AND );
}
- $indexWhereClauses[] = $this->makeList( $indexRowValues, LIST_AND );
- }
- if ( $indexWhereClauses ) {
- $this->delete( $table, $this->makeList( $indexWhereClauses, LIST_OR ), $fname );
+ if ( $indexWhereClauses ) {
+ $this->delete( $table, $this->makeList( $indexWhereClauses, LIST_OR ), $fname );
+ $affectedRowCount += $this->affectedRows();
+ }
+
+ // Now insert the row
+ $this->insert( $table, $row, $fname );
$affectedRowCount += $this->affectedRows();
}
-
- // Now insert the row
- $this->insert( $table, $row, $fname );
- $affectedRowCount += $this->affectedRows();
+ } catch ( Exception $e ) {
+ if ( $useTrx ) {
+ $this->rollback( $fname, self::FLUSHING_INTERNAL );
+ }
+ throw $e;
+ }
+ if ( $useTrx ) {
+ $this->commit( $fname, self::FLUSHING_INTERNAL );
}
$this->affectedRowCount = $affectedRowCount;
$fname = __METHOD__,
$insertOptions = [], $selectOptions = [], $selectJoinConds = []
) {
+ $insertOptions = array_diff( (array)$insertOptions, [ 'NO_AUTO_COLUMNS' ] );
+
// For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
// on only the master (without needing row-based-replication). It also makes it easy to
// know how big the INSERT is going to be.
return false;
}
- $rows = [];
- foreach ( $res as $row ) {
- $rows[] = (array)$row;
+ try {
+ $affectedRowCount = 0;
+ $this->startAtomic( $fname );
+ $rows = [];
+ $ok = true;
+ foreach ( $res as $row ) {
+ $rows[] = (array)$row;
+
+ // Avoid inserts that are too huge
+ if ( count( $rows ) >= $this->nonNativeInsertSelectBatchSize ) {
+ $ok = $this->insert( $destTable, $rows, $fname, $insertOptions );
+ if ( !$ok ) {
+ break;
+ }
+ $affectedRowCount += $this->affectedRows();
+ $rows = [];
+ }
+ }
+ if ( $rows && $ok ) {
+ $ok = $this->insert( $destTable, $rows, $fname, $insertOptions );
+ if ( $ok ) {
+ $affectedRowCount += $this->affectedRows();
+ }
+ }
+ if ( $ok ) {
+ $this->endAtomic( $fname );
+ $this->affectedRowCount = $affectedRowCount;
+ } else {
+ $this->rollback( $fname, self::FLUSHING_INTERNAL );
+ $this->affectedRowCount = 0;
+ }
+ return $ok;
+ } catch ( Exception $e ) {
+ $this->rollback( $fname, self::FLUSHING_INTERNAL );
+ $this->affectedRowCount = 0;
+ throw $e;
}
-
- return $this->insert( $destTable, $rows, $fname, $insertOptions );
}
/**
if ( !is_array( $insertOptions ) ) {
$insertOptions = [ $insertOptions ];
}
+ $insertOptions = array_diff( (array)$insertOptions, [ 'NO_AUTO_COLUMNS' ] );
$insertOptions = $this->makeInsertOptions( $insertOptions );
}
public function lockIsFree( $lockName, $method ) {
- return true;
+ // RDBMs methods for checking named locks may or may not count this thread itself.
+ // In MySQL, IS_FREE_LOCK() returns 0 if the thread already has the lock. This is
+ // the behavior choosen by the interface for this method.
+ return !isset( $this->namedLocksHeld[$lockName] );
}
public function lock( $lockName, $method, $timeout = 5 ) {