/** @var array Map of (name => 1) for locks obtained via lock() */
private $mNamedLocksHeld = [];
+ /** @var array Map of (table name => 1) for TEMPORARY tables */
+ protected $mSessionTempTables = [];
/** @var IDatabase|null Lazy handle to the master DB this server replicates from */
private $lazyMasterHandle;
- /**
- * @since 1.21
- * @var resource File handle for upgrade
- */
- protected $fileHandle = null;
-
/**
* @since 1.22
* @var string[] Process cache of VIEWs names in the database
$this->trxProfiler = $params['trxProfiler'];
$this->connLogger = $params['connLogger'];
$this->queryLogger = $params['queryLogger'];
+ $this->errorLogger = $params['errorLogger'];
// Set initial dummy domain until open() sets the final DB/prefix
$this->currentDomain = DatabaseDomain::newUnspecified();
return $old;
}
- /**
- * Set the filehandle to copy write statements to.
- *
- * @param resource $fh File handle
- */
- public function setFileHandle( $fh ) {
- $this->fileHandle = $fh;
- }
-
public function getLBInfo( $name = null ) {
if ( is_null( $name ) ) {
return $this->mLBInfo;
protected function installErrorHandler() {
$this->mPHPError = false;
$this->htmlErrors = ini_set( 'html_errors', '0' );
- set_error_handler( [ $this, 'connectionerrorLogger' ] );
+ set_error_handler( [ $this, 'connectionErrorLogger' ] );
}
/**
}
/**
+ * This method should not be used outside of Database classes
+ *
* @param int $errno
* @param string $errstr
*/
- public function connectionerrorLogger( $errno, $errstr ) {
+ public function connectionErrorLogger( $errno, $errstr ) {
$this->mPHPError = $errstr;
}
*/
abstract protected function closeConnection();
- function reportConnectionError( $error = 'Unknown error' ) {
+ public function reportConnectionError( $error = 'Unknown error' ) {
$myError = $this->lastError();
if ( $myError ) {
$error = $myError;
return !in_array( $verb, [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET' ], true );
}
+ /**
+ * @param string $sql A SQL query
+ * @return bool Whether $sql is SQL for creating/dropping a new TEMPORARY table
+ */
+ protected function registerTempTableOperation( $sql ) {
+ if ( preg_match(
+ '/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
+ $sql,
+ $matches
+ ) ) {
+ $this->mSessionTempTables[$matches[1]] = 1;
+
+ return true;
+ } elseif ( preg_match(
+ '/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
+ $sql,
+ $matches
+ ) ) {
+ unset( $this->mSessionTempTables[$matches[1]] );
+
+ return true;
+ } elseif ( preg_match(
+ '/^(?:INSERT\s+(?:\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+[`"\']?(\w+)[`"\']?/i',
+ $sql,
+ $matches
+ ) ) {
+ return isset( $this->mSessionTempTables[$matches[1]] );
+ }
+
+ return false;
+ }
+
public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
$priorWritesPending = $this->writesOrCallbacksPending();
$this->mLastQuery = $sql;
- $isWrite = $this->isWriteQuery( $sql );
+ $isWrite = $this->isWriteQuery( $sql ) && !$this->registerTempTableOperation( $sql );
if ( $isWrite ) {
$reason = $this->getReadOnlyReason();
if ( $reason !== false ) {
$lastError = $this->lastError();
$lastErrno = $this->lastErrno();
# Update state tracking to reflect transaction loss due to disconnection
- $this->handleTransactionLoss();
+ $this->handleSessionLoss();
if ( $this->reconnect() ) {
$msg = __METHOD__ . ": lost connection to {$this->getServer()}; reconnected";
$this->connLogger->warning( $msg );
$tempIgnore = false; // not recoverable
}
# Update state tracking to reflect transaction loss
- $this->handleTransactionLoss();
+ $this->handleSessionLoss();
}
$this->reportQueryError(
return true;
}
- private function handleTransactionLoss() {
+ private function handleSessionLoss() {
$this->mTrxLevel = 0;
$this->mTrxIdleCallbacks = []; // bug 65263
$this->mTrxPreCommitCallbacks = []; // bug 65263
+ $this->mSessionTempTables = [];
+ $this->mNamedLocksHeld = [];
try {
// Handle callbacks in mTrxEndCallbacks
$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
* @return array
* @see DatabaseBase::select()
*/
- public function makeSelectOptions( $options ) {
+ protected function makeSelectOptions( $options ) {
$preLimitTail = $postLimitTail = '';
$startOpts = '';
* @see DatabaseBase::select()
* @since 1.21
*/
- public function makeGroupByWithHaving( $options ) {
+ protected function makeGroupByWithHaving( $options ) {
$sql = '';
if ( isset( $options['GROUP BY'] ) ) {
$gb = is_array( $options['GROUP BY'] )
* @see DatabaseBase::select()
* @since 1.21
*/
- public function makeOrderBy( $options ) {
+ protected function makeOrderBy( $options ) {
if ( isset( $options['ORDER BY'] ) ) {
$ob = is_array( $options['ORDER BY'] )
? implode( ',', $options['ORDER BY'] )
return '';
}
- // See IDatabase::select for the docs for this function
public function select( $table, $vars, $conds = '', $fname = __METHOD__,
$options = [], $join_conds = [] ) {
$sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
$useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
? $options['USE INDEX']
: [];
- $ignoreIndexes = ( isset( $options['IGNORE INDEX'] ) && is_array( $options['IGNORE INDEX'] ) )
+ $ignoreIndexes = (
+ isset( $options['IGNORE INDEX'] ) &&
+ is_array( $options['IGNORE INDEX'] )
+ )
? $options['IGNORE INDEX']
: [];
if ( is_array( $table ) ) {
$from = ' FROM ' .
- $this->tableNamesWithIndexClauseOrJOIN( $table, $useIndexes, $ignoreIndexes, $join_conds );
+ $this->tableNamesWithIndexClauseOrJOIN(
+ $table, $useIndexes, $ignoreIndexes, $join_conds );
} elseif ( $table != '' ) {
if ( $table[0] == ' ' ) {
$from = ' FROM ' . $table;
} else {
$from = ' FROM ' .
- $this->tableNamesWithIndexClauseOrJOIN( [ $table ], $useIndexes, $ignoreIndexes, [] );
+ $this->tableNamesWithIndexClauseOrJOIN(
+ [ $table ], $useIndexes, $ignoreIndexes, [] );
}
} else {
$from = '';
if ( is_array( $conds ) ) {
$conds = $this->makeList( $conds, self::LIST_AND );
}
- $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex WHERE $conds $preLimitTail";
+ $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex " .
+ "WHERE $conds $preLimitTail";
} else {
$sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex $preLimitTail";
}
}
public function tableExists( $table, $fname = __METHOD__ ) {
+ $tableRaw = $this->tableName( $table, 'raw' );
+ if ( isset( $this->mSessionTempTables[$tableRaw] ) ) {
+ return true; // already known to exist
+ }
+
$table = $this->tableName( $table );
$old = $this->ignoreErrors( true );
$res = $this->query( "SELECT 1 FROM $table LIMIT 1", $fname );
return implode( ' ', $opts );
}
- function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
+ public function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
$table = $this->tableName( $table );
$opts = $this->makeUpdateOptions( $options );
$sql = "UPDATE $opts $table SET " . $this->makeList( $values, self::LIST_SET );
}
}
- /**
- * Return aggregated value alias
- *
- * @param array $valuedata
- * @param string $valuename
- *
- * @return string
- */
public function aggregateValue( $valuedata, $valuename = 'value' ) {
return $valuename;
}
return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
}
- /**
- * @param string $field Field or column to cast
- * @return string
- * @since 1.28
- */
public function buildStringCast( $field ) {
return $field;
}
* @param string|bool $alias Alias (optional)
* @return string SQL name for aliased table. Will not alias a table to its own name
*/
- public function tableNameWithAlias( $name, $alias = false ) {
+ protected function tableNameWithAlias( $name, $alias = false ) {
if ( !$alias || $alias == $name ) {
return $this->tableName( $name );
} else {
* @param array $tables [ [alias] => table ]
* @return string[] See tableNameWithAlias()
*/
- public function tableNamesWithAlias( $tables ) {
+ protected function tableNamesWithAlias( $tables ) {
$retval = [];
foreach ( $tables as $alias => $table ) {
if ( is_numeric( $alias ) ) {
* @param string|bool $alias Alias (optional)
* @return string SQL name for aliased field. Will not alias a field to its own name
*/
- public function fieldNameWithAlias( $name, $alias = false ) {
+ protected function fieldNameWithAlias( $name, $alias = false ) {
if ( !$alias || (string)$alias === (string)$name ) {
return $name;
} else {
* @param array $fields [ [alias] => field ]
* @return string[] See fieldNameWithAlias()
*/
- public function fieldNamesWithAlias( $fields ) {
+ protected function fieldNamesWithAlias( $fields ) {
$retval = [];
foreach ( $fields as $alias => $field ) {
if ( is_numeric( $alias ) ) {
}
}
if ( isset( $ignore_index[$alias] ) ) { // has IGNORE INDEX?
- $ignore = $this->ignoreIndexClause( implode( ',', (array)$ignore_index[$alias] ) );
+ $ignore = $this->ignoreIndexClause(
+ implode( ',', (array)$ignore_index[$alias] ) );
if ( $ignore != '' ) {
$tableClause .= ' ' . $ignore;
}
* @return string
*/
protected function indexName( $index ) {
- // Backwards-compatibility hack
- $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;
- }
+ return $index;
}
public function addQuotes( $s ) {
}
if ( $s === null ) {
return 'NULL';
+ } elseif ( is_bool( $s ) ) {
+ return (int)$s;
} else {
# This will also quote numeric values. This should be harmless,
# and protects against weird problems that occur when they really
return $this->insert( $destTable, $rows, $fname, $insertOptions );
}
- public function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
+ protected function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
$fname = __METHOD__,
$insertOptions = [], $selectOptions = []
) {
$srcTable = $this->tableName( $srcTable );
}
- $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
+ $sql = "INSERT $insertOptions" .
+ " INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
" SELECT $startOpts " . implode( ',', $varMap ) .
" FROM $srcTable $useIndex $ignoreIndex ";
*/
public function limitResult( $sql, $limit, $offset = false ) {
if ( !is_numeric( $limit ) ) {
- throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
+ throw new DBUnexpectedError( $this,
+ "Invalid non-numeric limit passed to limitResult()\n" );
}
return "$sql LIMIT "
}
/**
- * Determines if the given query error was a connection drop
- * STUB
+ * Do not use this method outside of Database/DBError classes
*
* @param integer|string $errno
- * @return bool
+ * @return bool Whether the given query error was a connection drop
*/
public function wasConnectionError( $errno ) {
return false;
}
} else {
if ( !$this->mTrxLevel ) {
- $this->queryLogger->error( "$fname: No transaction to commit, something got out of sync." );
+ $this->queryLogger->error(
+ "$fname: No transaction to commit, something got out of sync." );
return; // nothing to do
} elseif ( $this->mTrxAutomatic ) {
// @TODO: make this an exception at some point
throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
}
- function listTables( $prefix = null, $fname = __METHOD__ ) {
+ public function listTables( $prefix = null, $fname = __METHOD__ ) {
throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
}
}
public function timestamp( $ts = 0 ) {
- $t = new ConvertableTimestamp( $ts );
+ $t = new ConvertibleTimestamp( $ts );
// Let errors bubble up to avoid putting garbage in the DB
return $t->getTimestamp( TS_MW );
}
* @return array|null ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN)
* @since 1.27
*/
- public function getTransactionLagStatus() {
+ protected function getTransactionLagStatus() {
return $this->mTrxLevel
? [ 'lag' => $this->mTrxReplicaLag, 'since' => $this->trxTimestamp() ]
: null;
* @return array ('lag': seconds or false on error, 'since': UNIX timestamp of estimate)
* @since 1.27
*/
- public function getApproximateLagStatus() {
+ protected function getApproximateLagStatus() {
return [
'lag' => $this->getLBInfo( 'replica' ) ? $this->getLag() : 0,
'since' => microtime( true )
return 0;
}
- function maxListLen() {
+ public function maxListLen() {
return 0;
}
* @throws Exception
*/
public function sourceFile(
- $filename, $lineCallback = false, $resultCallback = false, $fname = false, $inputCallback = false
+ $filename,
+ $lineCallback = false,
+ $resultCallback = false,
+ $fname = false,
+ $inputCallback = false
) {
MediaWiki\suppressWarnings();
$fp = fopen( $filename, 'r' );
}
try {
- $error = $this->sourceStream( $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
+ $error = $this->sourceStream(
+ $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
} catch ( Exception $e ) {
fclose( $fp );
throw $e;
* @param bool|callable $inputCallback Optional function called for each complete query sent
* @return bool|string
*/
- public function sourceStream( $fp, $lineCallback = false, $resultCallback = false,
- $fname = __METHOD__, $inputCallback = false
+ public function sourceStream(
+ $fp,
+ $lineCallback = false,
+ $resultCallback = false,
+ $fname = __METHOD__,
+ $inputCallback = false
) {
$cmd = '';
if ( $done || feof( $fp ) ) {
$cmd = $this->replaceVars( $cmd );
- if ( ( $inputCallback && call_user_func( $inputCallback, $cmd ) ) || !$inputCallback ) {
+ if ( !$inputCallback || call_user_func( $inputCallback, $cmd ) ) {
$res = $this->query( $cmd, $fname );
if ( $resultCallback ) {
/**
* Called by sourceStream() to check if we've reached a statement end
*
- * @param string $sql SQL assembled so far
- * @param string $newLine New line about to be added to $sql
+ * @param string &$sql SQL assembled so far
+ * @param string &$newLine New line about to be added to $sql
* @return bool Whether $newLine contains end of the statement
*/
public function streamStatementEnd( &$sql, &$newLine ) {
if ( $this->delimiter ) {
$prev = $newLine;
- $newLine = preg_replace( '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
+ $newLine = preg_replace(
+ '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
if ( $newLine != $prev ) {
return true;
}
return 'infinity';
}
- try {
- $t = new ConvertableTimestamp( $expiry );
-
- return $t->getTimestamp( $format );
- } catch ( TimestampException $e ) {
- return false;
- }
+ return ConvertibleTimestamp::convert( $format, $expiry );
}
public function setBigSelects( $value = true ) {
return (string)$this->mConn;
}
+ /**
+ * Make sure that copies do not share the same client binding handle
+ * @throws DBConnectionError
+ */
+ public function __clone() {
+ $this->connLogger->warning(
+ "Cloning " . get_class( $this ) . " is not recomended; forking connection:\n" .
+ ( new RuntimeException() )->getTraceAsString()
+ );
+
+ if ( $this->isOpen() ) {
+ // Open a new connection resource without messing with the old one
+ $this->mOpened = false;
+ $this->mConn = false;
+ $this->mTrxEndCallbacks = []; // don't copy
+ $this->handleSessionLoss(); // no trx or locks anymore
+ $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
+ $this->lastPing = microtime( true );
+ }
+ }
+
/**
* Called by serialize. Throw an exception when DB connection is serialized.
* This causes problems on some database engines because the connection is
}
/**
- * Run a few simple sanity checks
+ * Run a few simple sanity checks and close dangling connections
*/
public function __destruct() {
if ( $this->mTrxLevel && $this->mTrxDoneWrites ) {
$fnames = implode( ', ', $danglingWriters );
trigger_error( "DB transaction writes or callbacks still pending ($fnames)." );
}
+
+ if ( $this->mConn ) {
+ // Avoid connection leaks for sanity
+ $this->closeConnection();
+ $this->mConn = false;
+ $this->mOpened = false;
+ }
}
}