Merge "rdbms: treat cloned temporary tables as "effective write" targets"

This commit is contained in:
jenkins-bot 2019-03-26 22:02:10 +00:00 committed by Gerrit Code Review
commit 1a62e51a00
9 changed files with 80 additions and 41 deletions

View file

@ -254,7 +254,7 @@ class DBConnRef implements IDatabase {
throw new DBUnexpectedError( $this->conn, 'Cannot close shared connection.' );
}
public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
public function query( $sql, $fname = __METHOD__, $flags = 0 ) {
return $this->__call( __FUNCTION__, func_get_args() );
}

View file

@ -280,6 +280,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
/** @var int No transaction is active */
const STATUS_TRX_NONE = 3;
/** @var int Writes to this temporary table do not affect lastDoneWrites() */
const TEMP_NORMAL = 1;
/** @var int Writes to this temporary table effect lastDoneWrites() */
const TEMP_PSEUDO_PERMANENT = 2;
/**
* @note exceptions for missing libraries/drivers should be thrown in initConnection()
* @param array $params Parameters passed from Database::factory()
@ -1135,47 +1140,55 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
/**
* @param string $sql A SQL query
* @return bool Whether $sql is SQL for TEMPORARY table operation
* @param bool $pseudoPermanent Treat any table from CREATE TEMPORARY as pseudo-permanent
* @return int|null A self::TEMP_* constant for temp table operations or null otherwise
*/
protected function registerTempTableOperation( $sql ) {
if ( preg_match(
'/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
$sql,
$matches
) ) {
$this->sessionTempTables[$matches[1]] = 1;
protected function registerTempTableWrite( $sql, $pseudoPermanent ) {
static $qt = '[`"\']?(\w+)[`"\']?'; // quoted table
return true;
} elseif ( preg_match(
'/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
if ( preg_match(
'/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?' . $qt . '/i',
$sql,
$matches
) ) {
$isTemp = isset( $this->sessionTempTables[$matches[1]] );
$type = $pseudoPermanent ? self::TEMP_PSEUDO_PERMANENT : self::TEMP_NORMAL;
$this->sessionTempTables[$matches[1]] = $type;
return $type;
} elseif ( preg_match(
'/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?' . $qt . '/i',
$sql,
$matches
) ) {
$type = $this->sessionTempTables[$matches[1]] ?? null;
unset( $this->sessionTempTables[$matches[1]] );
return $isTemp;
return $type;
} elseif ( preg_match(
'/^TRUNCATE\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
'/^TRUNCATE\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?' . $qt . '/i',
$sql,
$matches
) ) {
return isset( $this->sessionTempTables[$matches[1]] );
return $this->sessionTempTables[$matches[1]] ?? null;
} elseif ( preg_match(
'/^(?:INSERT\s+(?:\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+[`"\']?(\w+)[`"\']?/i',
'/^(?:(?:INSERT|REPLACE)\s+(?:\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+' . $qt . '/i',
$sql,
$matches
) ) {
return isset( $this->sessionTempTables[$matches[1]] );
return $this->sessionTempTables[$matches[1]] ?? null;
}
return false;
return null;
}
public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
public function query( $sql, $fname = __METHOD__, $flags = 0 ) {
$this->assertTransactionStatus( $sql, $fname );
$this->assertHasConnectionHandle();
$flags = (int)$flags; // b/c; this field used to be a bool
$ignoreErrors = $this->hasFlags( $flags, self::QUERY_SILENCE_ERRORS );
$pseudoPermanent = $this->hasFlags( $flags, self::QUERY_PSEUDO_PERMANENT );
$priorTransaction = $this->trxLevel;
$priorWritesPending = $this->writesOrCallbacksPending();
$this->lastQuery = $sql;
@ -1184,8 +1197,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
# 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...
$this->assertIsWritableMaster();
# Avoid treating temporary table operations as meaningful "writes"
$isEffectiveWrite = !$this->registerTempTableOperation( $sql );
# Do not treat temporary table writes as "meaningful writes" that need committing.
# Profile them as reads. Integration tests can override this behavior via $flags.
$tableType = $this->registerTempTableWrite( $sql, $pseudoPermanent );
$isEffectiveWrite = ( $tableType !== self::TEMP_NORMAL );
} else {
$isEffectiveWrite = false;
}
@ -1240,12 +1255,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
$this->trxStatus = self::STATUS_TRX_ERROR;
$this->trxStatusCause =
$this->getQueryExceptionAndLog( $lastError, $lastErrno, $sql, $fname );
$tempIgnore = false; // cannot recover
$ignoreErrors = false; // cannot recover
$this->trxStatusIgnoredCause = null;
}
}
$this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $tempIgnore );
$this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $ignoreErrors );
}
return $this->resultObject( $ret );
@ -1514,17 +1529,17 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
/**
* Report a query error. Log the error, and if neither the object ignore
* flag nor the $tempIgnore flag is set, throw a DBQueryError.
* flag nor the $ignoreErrors flag is set, throw a DBQueryError.
*
* @param string $error
* @param int $errno
* @param string $sql
* @param string $fname
* @param bool $tempIgnore
* @param bool $ignoreErrors
* @throws DBQueryError
*/
public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
if ( $tempIgnore ) {
public function reportQueryError( $error, $errno, $sql, $fname, $ignoreErrors = false ) {
if ( $ignoreErrors ) {
$this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
} else {
$exception = $this->getQueryExceptionAndLog( $error, $errno, $sql, $fname );
@ -4684,6 +4699,15 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
$this->indexAliases = $aliases;
}
/**
* @param int $field
* @param int $flags
* @return bool
*/
protected function hasFlags( $field, $flags ) {
return ( ( $field & $flags ) === $flags );
}
/**
* Get the underlying binding connection handle
*

View file

@ -1460,7 +1460,7 @@ abstract class DatabaseMysqlBase extends Database {
$oldName = $this->addIdentifierQuotes( $oldName );
$query = "CREATE $tmp TABLE $newName (LIKE $oldName)";
return $this->query( $query, $fname );
return $this->query( $query, $fname, $this::QUERY_PSEUDO_PERMANENT );
}
/**

View file

@ -819,8 +819,12 @@ __INDEXATTR__;
$temporary = $temporary ? 'TEMPORARY' : '';
$ret = $this->query( "CREATE $temporary TABLE $newNameE " .
"(LIKE $oldNameE INCLUDING DEFAULTS INCLUDING INDEXES)", $fname );
$ret = $this->query(
"CREATE $temporary TABLE $newNameE " .
"(LIKE $oldNameE INCLUDING DEFAULTS INCLUDING INDEXES)",
$fname,
$this::QUERY_PSEUDO_PERMANENT
);
if ( !$ret ) {
return $ret;
}
@ -842,7 +846,10 @@ __INDEXATTR__;
$fieldE = $this->addIdentifierQuotes( $field );
$newSeqE = $this->addIdentifierQuotes( $newSeq );
$newSeqQ = $this->addQuotes( $newSeq );
$this->query( "CREATE $temporary SEQUENCE $newSeqE OWNED BY $newNameE.$fieldE", $fname );
$this->query(
"CREATE $temporary SEQUENCE $newSeqE OWNED BY $newNameE.$fieldE",
$fname
);
$this->query(
"ALTER TABLE $newNameE ALTER COLUMN $fieldE SET DEFAULT nextval({$newSeqQ}::regclass)",
$fname

View file

@ -1018,7 +1018,7 @@ class DatabaseSqlite extends Database {
}
}
$res = $this->query( $sql, $fname );
$res = $this->query( $sql, $fname, self::QUERY_PSEUDO_PERMANENT );
// Take over indexes
$indexList = $this->query( 'PRAGMA INDEX_LIST(' . $this->addQuotes( $oldName ) . ')' );

View file

@ -106,6 +106,14 @@ interface IDatabase {
/** @var int Enable compression in connection protocol */
const DBO_COMPRESS = 512;
/** @var int Ignore query errors and return false when they happen */
const QUERY_SILENCE_ERRORS = 1; // b/c for 1.32 query() argument; note that (int)true = 1
/**
* @var int Treat the TEMPORARY table from the given CREATE query as if it is
* permanent as far as write tracking is concerned. This is useful for testing.
*/
const QUERY_PSEUDO_PERMANENT = 2;
/**
* A string describing the current software version, and possibly
* other details in a user-friendly way. Will be listed on Special:Version, etc.
@ -527,13 +535,13 @@ interface IDatabase {
* @param string $sql SQL query
* @param string $fname Name of the calling function, for profiling/SHOW PROCESSLIST
* comment (you can use __METHOD__ or add some extra info)
* @param bool $tempIgnore Whether to avoid throwing an exception on errors...
* maybe best to catch the exception instead?
* @param int $flags Bitfield of IDatabase::QUERY_* constants. Note that suppression
* of errors is best handled by try/catch rather than using one of these flags.
* @return bool|IResultWrapper True for a successful write query, IResultWrapper object
* for a successful read query, or false on failure if $tempIgnore set
* for a successful read query, or false on failure if QUERY_SILENCE_ERRORS is set.
* @throws DBError
*/
public function query( $sql, $fname = __METHOD__, $tempIgnore = false );
public function query( $sql, $fname = __METHOD__, $flags = 0 );
/**
* Free a result object returned by query() or select(). It's usually not

View file

@ -531,7 +531,7 @@ class DatabaseSqliteMock extends DatabaseSqlite {
return Database::factory( 'SqliteMock', $p );
}
function query( $sql, $fname = '', $tempIgnore = false ) {
function query( $sql, $fname = '', $flags = 0 ) {
return true;
}

View file

@ -133,10 +133,10 @@ class DatabaseTestHelper extends Database {
return $s;
}
public function query( $sql, $fname = '', $tempIgnore = false ) {
public function query( $sql, $fname = '', $flags = 0 ) {
$this->checkFunctionName( $fname );
return parent::query( $sql, $fname, $tempIgnore );
return parent::query( $sql, $fname, $flags );
}
public function tableExists( $table, $fname = __METHOD__ ) {

View file

@ -1343,7 +1343,7 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase {
}
/**
* @covers Wikimedia\Rdbms\Database::registerTempTableOperation
* @covers Wikimedia\Rdbms\Database::registerTempTableWrite
*/
public function testSessionTempTables() {
$temp1 = $this->database->tableName( 'tmp_table_1' );