*/
namespace Wikimedia\Rdbms;
+use NullLockManager;
use PDO;
use PDOException;
use Exception;
use LockManager;
use FSLockManager;
-use InvalidArgumentException;
use RuntimeException;
use stdClass;
* @ingroup Database
*/
class DatabaseSqlite extends Database {
- /** @var bool Whether full text is enabled */
- private static $fulltextEnabled = null;
-
- /** @var string Directory */
+ /** @var string|null Directory for SQLite database files listed under their DB name */
protected $dbDir;
- /** @var string File name for SQLite database file */
+ /** @var string|null Explicit path for the SQLite database file */
protected $dbPath;
/** @var string Transaction mode */
protected $trxMode;
/** @var array List of shared database already attached to this connection */
private $alreadyAttached = [];
+ /** @var bool Whether full text is enabled */
+ private static $fulltextEnabled = null;
+
/**
* Additional params include:
* - dbDirectory : directory containing the DB and the lock file directory
- * [defaults to $wgSQLiteDataDir]
* - dbFilePath : use this to force the path of the DB file
* - trxMode : one of (deferred, immediate, exclusive)
* @param array $p
*/
- function __construct( array $p ) {
+ public function __construct( array $p ) {
if ( isset( $p['dbFilePath'] ) ) {
$this->dbPath = $p['dbFilePath'];
- $lockDomain = md5( $this->dbPath );
- // Use "X" for things like X.sqlite and ":memory:" for RAM-only DBs
- if ( !isset( $p['dbname'] ) || !strlen( $p['dbname'] ) ) {
- $p['dbname'] = preg_replace( '/\.sqlite\d?$/', '', basename( $this->dbPath ) );
+ if ( !strlen( $p['dbname'] ) ) {
+ $p['dbname'] = self::generateDatabaseName( $this->dbPath );
}
} elseif ( isset( $p['dbDirectory'] ) ) {
$this->dbDir = $p['dbDirectory'];
- $lockDomain = $p['dbname'];
- } else {
- throw new InvalidArgumentException( "Need 'dbDirectory' or 'dbFilePath' parameter." );
}
- $this->trxMode = isset( $p['trxMode'] ) ? strtoupper( $p['trxMode'] ) : null;
- if ( $this->trxMode &&
- !in_array( $this->trxMode, [ 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ] )
- ) {
- $this->trxMode = null;
- $this->queryLogger->warning( "Invalid SQLite transaction mode provided." );
- }
+ // Set a dummy user to make initConnection() trigger open()
+ parent::__construct( [ 'user' => '@' ] + $p );
- $this->lockMgr = new FSLockManager( [
- 'domain' => $lockDomain,
- 'lockDirectory' => "{$this->dbDir}/locks"
- ] );
+ $this->trxMode = strtoupper( $p['trxMode'] ?? '' );
- parent::__construct( $p );
+ $lockDirectory = $this->getLockFileDirectory();
+ if ( $lockDirectory !== null ) {
+ $this->lockMgr = new FSLockManager( [
+ 'domain' => $this->getDomainID(),
+ 'lockDirectory' => $lockDirectory
+ ] );
+ } else {
+ $this->lockMgr = new NullLockManager( [ 'domain' => $this->getDomainID() ] );
+ }
}
protected static function getAttributes() {
return $db;
}
- protected function doInitConnection() {
- if ( $this->dbPath !== null ) {
- // Standalone .sqlite file mode.
- $this->openFile(
- $this->dbPath,
- $this->connectionParams['dbname'],
- $this->connectionParams['tablePrefix']
- );
- } elseif ( $this->dbDir !== null ) {
- // Stock wiki mode using standard file names per DB
- if ( strlen( $this->connectionParams['dbname'] ) ) {
- $this->open(
- $this->connectionParams['host'],
- $this->connectionParams['user'],
- $this->connectionParams['password'],
- $this->connectionParams['dbname'],
- $this->connectionParams['schema'],
- $this->connectionParams['tablePrefix']
- );
- } else {
- // Caller will manually call open() later?
- $this->connLogger->debug( __METHOD__ . ': no database opened.' );
- }
- } else {
- throw new InvalidArgumentException( "Need 'dbDirectory' or 'dbFilePath' parameter." );
- }
- }
-
/**
* @return string
*/
- function getType() {
+ public function getType() {
return 'sqlite';
}
*
* @return bool
*/
- function implicitGroupby() {
+ 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__ . ": domain schemas are not supported." );
+ throw new DBExpectedError( $this, __CLASS__ . ": cannot use schemas ('$schema')" );
}
- $fileName = self::generateFileName( $this->dbDir, $dbName );
- if ( !is_readable( $fileName ) ) {
+ if ( $this->dbPath !== null ) {
+ $path = $this->dbPath;
+ } elseif ( $this->dbDir !== null ) {
+ $path = self::generateFileName( $this->dbDir, $dbName );
+ } else {
+ throw new DBExpectedError( $this, __CLASS__ . ": DB path or directory required" );
+ }
+
+ if ( !in_array( $this->trxMode, [ '', 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ], true ) ) {
+ throw new DBExpectedError(
+ $this,
+ __CLASS__ . ": invalid transaction mode '{$this->trxMode}'"
+ );
+ }
+
+ if ( !self::isProcessMemoryPath( $path ) && !is_readable( $path ) ) {
$error = "SQLite database file not readable";
$this->connLogger->error(
"Error connecting to {db_server}: {error}",
throw new DBConnectionError( $this, $error );
}
- // Only $dbName is used, the other parameters are irrelevant for SQLite databases
- $this->openFile( $fileName, $dbName, $tablePrefix );
- }
-
- /**
- * Opens a database file
- *
- * @param string $fileName
- * @param string $dbName
- * @param string $tablePrefix
- * @throws DBConnectionError
- */
- protected function openFile( $fileName, $dbName, $tablePrefix ) {
- $this->dbPath = $fileName;
try {
- $this->conn = new PDO(
- "sqlite:$fileName",
+ $conn = new PDO(
+ "sqlite:$path",
'',
'',
[ PDO::ATTR_PERSISTENT => (bool)( $this->flags & self::DBO_PERSISTENT ) ]
);
- $error = 'unknown error';
+ // Set error codes only, don't raise exceptions
+ $conn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
} catch ( PDOException $e ) {
$error = $e->getMessage();
- }
-
- if ( !$this->conn ) {
$this->connLogger->error(
"Error connecting to {db_server}: {error}",
$this->getLogContext( [ 'method' => __METHOD__, 'error' => $error ] )
throw new DBConnectionError( $this, $error );
}
- try {
- // Set error codes only, don't raise exceptions
- $this->conn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
-
- $this->currentDomain = new DatabaseDomain( $dbName, null, $tablePrefix );
+ $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
$sync = $this->connectionVariables['synchronous'] ?? null;
if ( in_array( $sync, [ 'EXTRA', 'FULL', 'NORMAL', 'OFF' ], true ) ) {
- $this->query( "PRAGMA synchronous = $sync", __METHOD__ );
+ $this->query( "PRAGMA synchronous = $sync", __METHOD__, $flags );
}
} catch ( Exception $e ) {
// Connection was not fully initialized and is not safe for use
}
/**
- * @return string SQLite DB file path
+ * @return string|null SQLite DB file path
+ * @throws DBUnexpectedError
* @since 1.25
*/
public function getDbFilePath() {
- return $this->dbPath;
+ return $this->dbPath ?? self::generateFileName( $this->dbDir, $this->getDBname() );
+ }
+
+ /**
+ * @return string|null Lock file directory
+ */
+ public function getLockFileDirectory() {
+ if ( $this->dbPath !== null && !self::isProcessMemoryPath( $this->dbPath ) ) {
+ return dirname( $this->dbPath ) . '/locks';
+ } elseif ( $this->dbDir !== null && !self::isProcessMemoryPath( $this->dbDir ) ) {
+ return $this->dbDir . '/locks';
+ }
+
+ return null;
}
/**
/**
* Generates a database file name. Explicitly public for installer.
* @param string $dir Directory where database resides
- * @param string $dbName Database name
+ * @param string|bool $dbName Database name (or false from Database::factory, validated here)
* @return string
+ * @throws DBUnexpectedError
*/
public static function generateFileName( $dir, $dbName ) {
+ if ( $dir == '' ) {
+ throw new DBUnexpectedError( null, __CLASS__ . ": no DB directory specified" );
+ } elseif ( self::isProcessMemoryPath( $dir ) ) {
+ throw new DBUnexpectedError(
+ null,
+ __CLASS__ . ": cannot use process memory directory '$dir'"
+ );
+ } elseif ( !strlen( $dbName ) ) {
+ throw new DBUnexpectedError( null, __CLASS__ . ": no DB name specified" );
+ }
+
return "$dir/$dbName.sqlite";
}
+ /**
+ * @param string $path
+ * @return string
+ */
+ private static function generateDatabaseName( $path ) {
+ if ( preg_match( '/^(:memory:$|file::memory:)/', $path ) ) {
+ // E.g. "file::memory:?cache=shared" => ":memory":
+ return ':memory:';
+ } elseif ( preg_match( '/^file::([^?]+)\?mode=memory(&|$)/', $path, $m ) ) {
+ // E.g. "file:memdb1?mode=memory" => ":memdb1:"
+ return ":{$m[1]}:";
+ } else {
+ // E.g. "/home/.../some_db.sqlite3" => "some_db"
+ return preg_replace( '/\.sqlite\d?$/', '', basename( $path ) );
+ }
+ }
+
+ /**
+ * @param string $path
+ * @return bool
+ */
+ private static function isProcessMemoryPath( $path ) {
+ return preg_match( '/^(:memory:$|file:(:memory:|[^?]+\?mode=memory(&|$)))/', $path );
+ }
+
/**
* Check if the searchindext table is FTS enabled.
* @return bool False if not enabled.
if ( self::$fulltextEnabled === null ) {
self::$fulltextEnabled = false;
$table = $this->tableName( 'searchindex' );
- $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name = '$table'", __METHOD__ );
+ $res = $this->query(
+ "SELECT sql FROM sqlite_master WHERE tbl_name = '$table'",
+ __METHOD__,
+ self::QUERY_IGNORE_DBO_TRX
+ );
if ( $res ) {
$row = $res->fetchRow();
self::$fulltextEnabled = stristr( $row['sql'], 'fts' ) !== false;
* @param string $fname Calling function name
* @return IResultWrapper
*/
- function attachDatabase( $name, $file = false, $fname = __METHOD__ ) {
- if ( !$file ) {
- $file = self::generateFileName( $this->dbDir, $name );
- }
- $file = $this->addQuotes( $file );
-
- return $this->query( "ATTACH DATABASE $file AS $name", $fname );
+ public function attachDatabase( $name, $file = false, $fname = __METHOD__ ) {
+ $file = is_string( $file ) ? $file : self::generateFileName( $this->dbDir, $name );
+ $encFile = $this->addQuotes( $file );
+
+ return $this->query(
+ "ATTACH DATABASE $encFile AS $name",
+ $fname,
+ self::QUERY_IGNORE_DBO_TRX
+ );
}
protected function isWriteQuery( $sql ) {
return false;
}
- $r = $res instanceof ResultWrapper ? $res->result : $res;
- $this->lastAffectedRowCount = $r->rowCount();
- $res = new ResultWrapper( $this, $r->fetchAll() );
+ $resource = ResultWrapper::unwrap( $res );
+ $this->lastAffectedRowCount = $resource->rowCount();
+ $res = new ResultWrapper( $this, $resource->fetchAll() );
return $res;
}
*/
function freeResult( $res ) {
if ( $res instanceof ResultWrapper ) {
- $res->result = null;
- } else {
- $res = null;
+ $res->free();
}
}
* @return stdClass|bool
*/
function fetchObject( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $r =& $res->result;
- } else {
- $r =& $res;
- }
+ $resource =& ResultWrapper::unwrap( $res );
- $cur = current( $r );
+ $cur = current( $resource );
if ( is_array( $cur ) ) {
- next( $r );
+ next( $resource );
$obj = new stdClass;
foreach ( $cur as $k => $v ) {
if ( !is_numeric( $k ) ) {
* @return array|bool
*/
function fetchRow( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $r =& $res->result;
- } else {
- $r =& $res;
- }
- $cur = current( $r );
+ $resource =& ResultWrapper::unwrap( $res );
+ $cur = current( $resource );
if ( is_array( $cur ) ) {
- next( $r );
+ next( $resource );
return $cur;
}
*/
function numRows( $res ) {
// false does not implement Countable
- $r = $res instanceof ResultWrapper ? $res->result : $res;
+ $resource = ResultWrapper::unwrap( $res );
- return is_array( $r ) ? count( $r ) : 0;
+ return is_array( $resource ) ? count( $resource ) : 0;
}
/**
* @return int
*/
function numFields( $res ) {
- $r = $res instanceof ResultWrapper ? $res->result : $res;
- if ( is_array( $r ) && count( $r ) > 0 ) {
+ $resource = ResultWrapper::unwrap( $res );
+ if ( is_array( $resource ) && count( $resource ) > 0 ) {
// The size of the result array is twice the number of fields. (T67578)
- return count( $r[0] ) / 2;
+ return count( $resource[0] ) / 2;
} else {
// If the result is empty return 0
return 0;
* @return bool
*/
function fieldName( $res, $n ) {
- $r = $res instanceof ResultWrapper ? $res->result : $res;
- if ( is_array( $r ) ) {
- $keys = array_keys( $r[0] );
+ $resource = ResultWrapper::unwrap( $res );
+ if ( is_array( $resource ) ) {
+ $keys = array_keys( $resource[0] );
return $keys[$n];
}
* @param int $row
*/
function dataSeek( $res, $row ) {
- if ( $res instanceof ResultWrapper ) {
- $r =& $res->result;
- } else {
- $r =& $res;
- }
- reset( $r );
+ $resource =& ResultWrapper::unwrap( $res );
+ reset( $resource );
if ( $row > 0 ) {
for ( $i = 0; $i < $row; $i++ ) {
- next( $r );
+ next( $resource );
}
}
}
$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 false;
}
+ public function serverIsReadOnly() {
+ $path = $this->getDbFilePath();
+
+ return ( !self::isProcessMemoryPath( $path ) && !is_writable( $path ) );
+ }
+
/**
* @return string Wikitext of a link to the server software's web site
*/
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 );
}
protected function doBegin( $fname = '' ) {
- if ( $this->trxMode ) {
+ if ( $this->trxMode != '' ) {
$this->query( "BEGIN {$this->trxMode}", $fname );
} else {
$this->query( 'BEGIN', $fname );
}
public function lock( $lockName, $method, $timeout = 5 ) {
- if ( !is_dir( "{$this->dbDir}/locks" ) ) { // create dir as needed
- if ( !is_writable( $this->dbDir ) || !mkdir( "{$this->dbDir}/locks" ) ) {
- throw new DBError( $this, "Cannot create directory \"{$this->dbDir}/locks\"." );
- }
+ $status = $this->lockMgr->lock( [ $lockName ], LockManager::LOCK_EX, $timeout );
+ if (
+ $this->lockMgr instanceof FSLockManager &&
+ $status->hasMessage( 'lockmanager-fail-openlock' )
+ ) {
+ throw new DBError( $this, "Cannot create directory \"{$this->getLockFileDirectory()}\"" );
}
- return $this->lockMgr->lock( [ $lockName ], LockManager::LOCK_EX, $timeout )->isOK();
+ return $status->isOK();
}
public function unlock( $lockName, $method ) {
- return $this->lockMgr->unlock( [ $lockName ], LockManager::LOCK_EX )->isOK();
+ return $this->lockMgr->unlock( [ $lockName ], LockManager::LOCK_EX )->isGood();
}
/**
}
$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 ) {
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() {