const SLOW_WRITE_SEC = 0.500;
const SMALL_WRITE_ROWS = 100;
- /** @var string Whether lock granularity is on the level of the entire database */
+ /** @var string Lock granularity is on the level of the entire database */
const ATTR_DB_LEVEL_LOCKING = 'db-level-locking';
+ /** @var string The SCHEMA keyword refers to a grouping of tables in a database */
+ const ATTR_SCHEMAS_AS_TABLE_GROUPS = 'supports-schemas';
/** @var int New Database instance will not be connected yet when returned */
const NEW_UNCONNECTED = 0;
/** @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()
/**
* @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
+ * @return array Map of (Database::ATTR_* 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 ];
+ static $defaults = [
+ self::ATTR_DB_LEVEL_LOCKING => false,
+ self::ATTR_SCHEMAS_AS_TABLE_GROUPS => false
+ ];
$class = self::getClass( $dbType, $driver );
$possibleDrivers = $builtinTypes[$dbType];
if ( is_string( $possibleDrivers ) ) {
$class = $possibleDrivers;
+ } elseif ( (string)$driver !== '' ) {
+ if ( !isset( $possibleDrivers[$driver] ) ) {
+ throw new InvalidArgumentException( __METHOD__ .
+ " type '$dbType' does not support driver '{$driver}'" );
+ }
+
+ $class = $possibleDrivers[$driver];
} else {
- if ( (string)$driver !== '' ) {
- if ( !isset( $possibleDrivers[$driver] ) ) {
- throw new InvalidArgumentException( __METHOD__ .
- " type '$dbType' does not support driver '{$driver}'" );
- } else {
- $class = $possibleDrivers[$driver];
- }
- } else {
- foreach ( $possibleDrivers as $posDriver => $possibleClass ) {
- if ( extension_loaded( $posDriver ) ) {
- $class = $possibleClass;
- break;
- }
+ foreach ( $possibleDrivers as $posDriver => $possibleClass ) {
+ if ( extension_loaded( $posDriver ) ) {
+ $class = $possibleClass;
+ break;
}
}
}
}
/**
- * @return array Map of (Database::ATTRIBUTE_* constant => value
+ * @return array Map of (Database::ATTR_* constant => value
* @since 1.31
*/
protected static function getAttributes() {
}
public function dbSchema( $schema = null ) {
+ if ( strlen( $schema ) && $this->getDBname() === null ) {
+ throw new DBUnexpectedError( $this, "Cannot set schema to '$schema'; no database set." );
+ }
+
$old = $this->currentDomain->getSchema();
if ( $schema !== null ) {
$this->currentDomain = new DatabaseDomain(
public function getLBInfo( $name = null ) {
if ( is_null( $name ) ) {
return $this->lbInfo;
- } else {
- if ( array_key_exists( $name, $this->lbInfo ) ) {
- return $this->lbInfo[$name];
- } else {
- return null;
- }
}
+
+ if ( array_key_exists( $name, $this->lbInfo ) ) {
+ return $this->lbInfo[$name];
+ }
+
+ return null;
}
public function setLBInfo( $name, $value = null ) {
*/
protected function assertIsWritableMaster() {
if ( $this->getLBInfo( 'replica' ) === true ) {
- throw new DBUnexpectedError(
+ throw new DBReadOnlyRoleError(
$this,
'Write operations are not allowed on replica database connections.'
);
/**
* @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 ) {
+ protected function registerTempTableWrite( $sql, $pseudoPermanent ) {
+ static $qt = '[`"\']?(\w+)[`"\']?'; // quoted table
+
if ( preg_match(
- '/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
+ '/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?' . $qt . '/i',
$sql,
$matches
) ) {
- $this->sessionTempTables[$matches[1]] = 1;
+ $type = $pseudoPermanent ? self::TEMP_PSEUDO_PERMANENT : self::TEMP_NORMAL;
+ $this->sessionTempTables[$matches[1]] = $type;
- return true;
+ return $type;
} elseif ( preg_match(
- '/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
+ '/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?' . $qt . '/i',
$sql,
$matches
) ) {
- $isTemp = isset( $this->sessionTempTables[$matches[1]] );
+ $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 );
+
$priorTransaction = $this->trxLevel;
$priorWritesPending = $this->writesOrCallbacksPending();
$this->lastQuery = $sql;
# 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.
+ $pseudoPermanent = $this->hasFlags( $flags, self::QUERY_PSEUDO_PERMANENT );
+ $tableType = $this->registerTempTableWrite( $sql, $pseudoPermanent );
+ $isEffectiveWrite = ( $tableType !== self::TEMP_NORMAL );
+ # DBConnRef uses QUERY_REPLICA_ROLE to enforce the replica role for raw SQL queries
+ if ( $isEffectiveWrite && $this->hasFlags( $flags, self::QUERY_REPLICA_ROLE ) ) {
+ throw new DBReadOnlyRoleError( $this, "Cannot write; target role is DB_REPLICA" );
+ }
} else {
$isEffectiveWrite = false;
}
$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 );
/**
* 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 );
throw new InvalidArgumentException( "Table must be a string or Subquery." );
}
- if ( !strlen( $alias ) || $alias === $table ) {
+ if ( $alias === false || $alias === $table ) {
if ( $table instanceof Subquery ) {
throw new InvalidArgumentException( "Subquery table missing alias." );
}
list( $phpCallback ) = $callback;
$this->clearFlag( self::DBO_TRX ); // make each query its own transaction
try {
+ // @phan-suppress-next-line PhanParamTooManyCallable
call_user_func( $phpCallback, $trigger, $this );
} catch ( Exception $ex ) {
call_user_func( $this->errorLogger, $ex );
"$fname: Flushing an explicit transaction, getting out of sync."
);
}
- } else {
- if ( !$this->trxLevel ) {
- $this->queryLogger->error(
- "$fname: No transaction to commit, something got out of sync." );
- return; // nothing to do
- } elseif ( $this->trxAutomatic ) {
- throw new DBUnexpectedError(
- $this,
- "$fname: Expected mass commit of all peer transactions (DBO_TRX set)."
- );
- }
+ } elseif ( !$this->trxLevel ) {
+ $this->queryLogger->error(
+ "$fname: No transaction to commit, something got out of sync." );
+ return; // nothing to do
+ } elseif ( $this->trxAutomatic ) {
+ throw new DBUnexpectedError(
+ $this,
+ "$fname: Expected mass commit of all peer transactions (DBO_TRX set)."
+ );
}
$this->assertHasConnectionHandle();
final public function rollback( $fname = __METHOD__, $flush = '' ) {
$trxActive = $this->trxLevel;
- if ( $flush !== self::FLUSHING_INTERNAL && $flush !== self::FLUSHING_ALL_PEERS ) {
- if ( $this->getFlag( self::DBO_TRX ) ) {
- throw new DBUnexpectedError(
- $this,
- "$fname: Expected mass rollback of all peer transactions (DBO_TRX set)."
- );
- }
+ if ( $flush !== self::FLUSHING_INTERNAL
+ && $flush !== self::FLUSHING_ALL_PEERS
+ && $this->getFlag( self::DBO_TRX )
+ ) {
+ throw new DBUnexpectedError(
+ $this,
+ "$fname: Expected mass rollback of all peer transactions (DBO_TRX set)."
+ );
}
if ( $trxActive ) {
$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
*