protected $lockMgr;
/** @var array List of shared database already attached to this connection */
- private $alreadyAttached = [];
+ private $sessionAttachedDbs = [];
- /** @var bool Whether full text is enabled */
- private static $fulltextEnabled = null;
+ /** @var string[] See https://www.sqlite.org/lang_transaction.html */
+ private static $VALID_TRX_MODES = [ '', 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ];
/**
* Additional params include:
$this->dbDir = $p['dbDirectory'];
}
- // Set a dummy user to make initConnection() trigger open()
- parent::__construct( [ 'user' => '@' ] + $p );
+ parent::__construct( $p );
$this->trxMode = strtoupper( $p['trxMode'] ?? '' );
return 'sqlite';
}
- /**
- * @todo Check if it should be true like parent class
- *
- * @return bool
- */
- public function implicitGroupby() {
- return false;
- }
-
protected function open( $server, $user, $pass, $dbName, $schema, $tablePrefix ) {
$this->close();
// Note that for SQLite, $server, $user, and $pass are ignored
if ( $schema !== null ) {
- throw new DBExpectedError( $this, __CLASS__ . ": cannot use schemas ('$schema')" );
+ throw $this->newExceptionAfterConnectError( "Got schema '$schema'; not supported." );
}
if ( $this->dbPath !== null ) {
} elseif ( $this->dbDir !== null ) {
$path = self::generateFileName( $this->dbDir, $dbName );
} else {
- throw new DBExpectedError( $this, __CLASS__ . ": DB path or directory required" );
+ throw $this->newExceptionAfterConnectError( "DB path or directory required" );
}
- if ( !in_array( $this->trxMode, [ '', 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ], true ) ) {
- throw new DBExpectedError(
- $this,
- __CLASS__ . ": invalid transaction mode '{$this->trxMode}'"
- );
+ // Check if the database file already exists but is non-readable
+ if (
+ !self::isProcessMemoryPath( $path ) &&
+ file_exists( $path ) &&
+ !is_readable( $path )
+ ) {
+ throw $this->newExceptionAfterConnectError( 'SQLite database file is not readable' );
+ } elseif ( !in_array( $this->trxMode, self::$VALID_TRX_MODES, true ) ) {
+ throw $this->newExceptionAfterConnectError( "Got mode '{$this->trxMode}' for BEGIN" );
}
- if ( !self::isProcessMemoryPath( $path ) && !is_readable( $path ) ) {
- $error = "SQLite database file not readable";
- $this->connLogger->error(
- "Error connecting to {db_server}: {error}",
- $this->getLogContext( [ 'method' => __METHOD__, 'error' => $error ] )
- );
- throw new DBConnectionError( $this, $error );
+ $attributes = [];
+ if ( $this->getFlag( self::DBO_PERSISTENT ) ) {
+ // Persistent connections can avoid some schema index reading overhead.
+ // On the other hand, they can cause horrible contention with DBO_TRX.
+ if ( $this->getFlag( self::DBO_TRX ) || $this->getFlag( self::DBO_DEFAULT ) ) {
+ $this->connLogger->warning(
+ __METHOD__ . ": ignoring DBO_PERSISTENT due to DBO_TRX or DBO_DEFAULT",
+ $this->getLogContext()
+ );
+ } else {
+ $attributes[PDO::ATTR_PERSISTENT] = true;
+ }
}
try {
- $conn = new PDO(
- "sqlite:$path",
- '',
- '',
- [ PDO::ATTR_PERSISTENT => (bool)( $this->flags & self::DBO_PERSISTENT ) ]
- );
- // Set error codes only, don't raise exceptions
- $conn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
+ // Open the database file, creating it if it does not yet exist
+ $this->conn = new PDO( "sqlite:$path", null, null, $attributes );
} catch ( PDOException $e ) {
- $error = $e->getMessage();
- $this->connLogger->error(
- "Error connecting to {db_server}: {error}",
- $this->getLogContext( [ 'method' => __METHOD__, 'error' => $error ] )
- );
- throw new DBConnectionError( $this, $error );
+ throw $this->newExceptionAfterConnectError( $e->getMessage() );
}
- $this->conn = $conn;
$this->currentDomain = new DatabaseDomain( $dbName, null, $tablePrefix );
try {
$flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_NO_RETRY;
// Enforce LIKE to be case sensitive, just like MySQL
$this->query( 'PRAGMA case_sensitive_like = 1', __METHOD__, $flags );
- // Apply an optimizations or requirements regarding fsync() usage
+ // Apply optimizations or requirements regarding fsync() usage
$sync = $this->connectionVariables['synchronous'] ?? null;
if ( in_array( $sync, [ 'EXTRA', 'FULL', 'NORMAL', 'OFF' ], true ) ) {
$this->query( "PRAGMA synchronous = $sync", __METHOD__, $flags );
}
+ $this->attachDatabasesFromTableAliases();
} catch ( Exception $e ) {
- // Connection was not fully initialized and is not safe for use
- $this->conn = false;
- throw $e;
+ throw $this->newExceptionAfterConnectError( $e->getMessage() );
}
}
return preg_match( '/^(:memory:$|file:(:memory:|[^?]+\?mode=memory(&|$)))/', $path );
}
- /**
- * Check if the searchindext table is FTS enabled.
- * @return bool False if not enabled.
- */
- public function checkForEnabledSearch() {
- if ( self::$fulltextEnabled === null ) {
- self::$fulltextEnabled = false;
- $table = $this->tableName( 'searchindex' );
- $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name = '$table'", __METHOD__ );
- if ( $res ) {
- $row = $res->fetchRow();
- self::$fulltextEnabled = stristr( $row['sql'], 'fts' ) !== false;
- }
- }
-
- return self::$fulltextEnabled;
- }
-
/**
* Returns version of currently supported SQLite fulltext search module or false if none present.
* @return string
}
/**
- * Attaches external database to our connection, see https://sqlite.org/lang_attach.html
- * for details.
+ * Attaches external database to the connection handle
+ *
+ * @see https://sqlite.org/lang_attach.html
*
* @param string $name Database name to be used in queries like
* SELECT foo FROM dbname.table
$file = is_string( $file ) ? $file : self::generateFileName( $this->dbDir, $name );
$encFile = $this->addQuotes( $file );
- return $this->query( "ATTACH DATABASE $encFile AS $name", $fname );
+ return $this->query(
+ "ATTACH DATABASE $encFile AS $name",
+ $fname,
+ self::QUERY_IGNORE_DBO_TRX
+ );
}
protected function isWriteQuery( $sql ) {
return false;
}
+ protected function doSelectDomain( DatabaseDomain $domain ) {
+ if ( $domain->getSchema() !== null ) {
+ throw new DBExpectedError(
+ $this,
+ __CLASS__ . ": domain '{$domain->getId()}' has a schema component"
+ );
+ }
+
+ $database = $domain->getDatabase();
+ // A null database means "don't care" so leave it as is and update the table prefix
+ if ( $database === null ) {
+ $this->currentDomain = new DatabaseDomain(
+ $this->currentDomain->getDatabase(),
+ null,
+ $domain->getTablePrefix()
+ );
+
+ return true;
+ }
+
+ if ( $database !== $this->getDBname() ) {
+ throw new DBExpectedError(
+ $this,
+ __CLASS__ . ": cannot change database (got '$database')"
+ );
+ }
+
+ return true;
+ }
+
/**
* Use MySQL's naming (accounts for prefix etc) but remove surrounding backticks
*
$encTable = $this->addQuotes( $tableRaw );
$res = $this->query(
- "SELECT 1 FROM sqlite_master WHERE type='table' AND name=$encTable" );
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name=$encTable",
+ __METHOD__,
+ self::QUERY_IGNORE_DBO_TRX
+ );
return $res->numRows() ? true : false;
}
*/
function indexInfo( $table, $index, $fname = __METHOD__ ) {
$sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')';
- $res = $this->query( $sql, $fname );
+ $res = $this->query( $sql, $fname, self::QUERY_IGNORE_DBO_TRX );
if ( !$res || $res->numRows() == 0 ) {
return false;
}
return in_array( 'UNIQUE', $options );
}
- /**
- * Filter the options used in SELECT statements
- *
- * @param array $options
- * @return array
- */
- function makeSelectOptions( $options ) {
+ protected function makeSelectOptions( array $options ) {
+ // Remove problematic options that the base implementation converts to SQL
foreach ( $options as $k => $v ) {
- if ( is_numeric( $k ) && ( $v == 'FOR UPDATE' || $v == 'LOCK IN SHARE MODE' ) ) {
+ if ( is_numeric( $k ) && ( $v === 'FOR UPDATE' || $v === 'LOCK IN SHARE MODE' ) ) {
$options[$k] = '';
}
}
}
public function serverIsReadOnly() {
+ $this->assertHasConnectionHandle();
+
$path = $this->getDbFilePath();
return ( !self::isProcessMemoryPath( $path ) && !is_writable( $path ) );
function fieldInfo( $table, $field ) {
$tableName = $this->tableName( $table );
$sql = 'PRAGMA table_info(' . $this->addQuotes( $tableName ) . ')';
- $res = $this->query( $sql, __METHOD__ );
+ $res = $this->query( $sql, __METHOD__, self::QUERY_IGNORE_DBO_TRX );
foreach ( $res as $row ) {
if ( $row->name == $field ) {
return new SQLiteField( $row, $tableName );
}
$sql = "DROP TABLE " . $this->tableName( $tableName );
- return $this->query( $sql, $fName );
+ return $this->query( $sql, $fName, self::QUERY_IGNORE_DBO_TRX );
}
public function setTableAliases( array $aliases ) {
parent::setTableAliases( $aliases );
+ if ( $this->isOpen() ) {
+ $this->attachDatabasesFromTableAliases();
+ }
+ }
+
+ /**
+ * Issue ATTATCH statements for all unattached foreign DBs in table aliases
+ */
+ private function attachDatabasesFromTableAliases() {
foreach ( $this->tableAliases as $params ) {
- if ( isset( $this->alreadyAttached[$params['dbname']] ) ) {
- continue;
+ if (
+ $params['dbname'] !== $this->getDBname() &&
+ !isset( $this->sessionAttachedDbs[$params['dbname']] )
+ ) {
+ $this->attachDatabase( $params['dbname'] );
+ $this->sessionAttachedDbs[$params['dbname']] = true;
}
- $this->attachDatabase( $params['dbname'] );
- $this->alreadyAttached[$params['dbname']] = true;
}
}
public function resetSequenceForTable( $table, $fname = __METHOD__ ) {
$encTable = $this->addIdentifierQuotes( 'sqlite_sequence' );
$encName = $this->addQuotes( $this->tableName( $table, 'raw' ) );
- $this->query( "DELETE FROM $encTable WHERE name = $encName", $fname );
+ $this->query(
+ "DELETE FROM $encTable WHERE name = $encName",
+ $fname,
+ self::QUERY_IGNORE_DBO_TRX
+ );
}
public function databasesAreIndependent() {
return true;
}
+ protected function doHandleSessionLossPreconnect() {
+ $this->sessionAttachedDbs = [];
+ }
+
/**
* @return PDO
*/