Localisation updates from https://translatewiki.net.
[mediawiki.git] / includes / libs / rdbms / database / DatabaseSqlite.php
blob4320c731e371f595d39df34201f5e6e9fc6334df
1 <?php
2 /**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
18 * @file
20 namespace Wikimedia\Rdbms;
22 use FSLockManager;
23 use LockManager;
24 use NullLockManager;
25 use PDO;
26 use PDOException;
27 use PDOStatement;
28 use RuntimeException;
29 use Wikimedia\Rdbms\Platform\SqlitePlatform;
30 use Wikimedia\Rdbms\Platform\SQLPlatform;
31 use Wikimedia\Rdbms\Replication\ReplicationReporter;
33 /**
34 * This is the SQLite database abstraction layer.
36 * See docs/sqlite.txt for development notes about MediaWiki's sqlite schema.
38 * @ingroup Database
40 class DatabaseSqlite extends Database {
41 /** @var string|null Directory for SQLite database files listed under their DB name */
42 protected $dbDir;
43 /** @var string|null Explicit path for the SQLite database file */
44 protected $dbPath;
45 /** @var string Transaction mode */
46 protected $trxMode;
48 /** @var PDO|null */
49 protected $conn;
51 /** @var LockManager|null (hopefully on the same server as the DB) */
52 protected $lockMgr;
54 /** @var string|null */
55 private $version;
57 /** @var array List of shared database already attached to this connection */
58 private $sessionAttachedDbs = [];
60 /** @var string[] See https://www.sqlite.org/lang_transaction.html */
61 private const VALID_TRX_MODES = [ '', 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ];
63 /** @var string[][] */
64 private const VALID_PRAGMAS = [
65 // Optimizations or requirements regarding fsync() usage
66 'synchronous' => [ 'EXTRA', 'FULL', 'NORMAL', 'OFF' ],
67 // Optimizations for TEMPORARY tables
68 'temp_store' => [ 'FILE', 'MEMORY' ],
69 // Optimizations for disk use and page cache
70 'mmap_size' => 'integer',
71 // How many DB pages to keep in memory
72 'cache_size' => 'integer',
75 /** @var SQLPlatform */
76 protected $platform;
78 /**
79 * Additional params include:
80 * - dbDirectory : directory containing the DB and the lock file directory
81 * - dbFilePath : use this to force the path of the DB file
82 * - trxMode : one of (deferred, immediate, exclusive)
83 * @param array $params
85 public function __construct( array $params ) {
86 if ( isset( $params['dbFilePath'] ) ) {
87 $this->dbPath = $params['dbFilePath'];
88 if ( !isset( $params['dbname'] ) || $params['dbname'] === '' ) {
89 $params['dbname'] = self::generateDatabaseName( $this->dbPath );
91 } elseif ( isset( $params['dbDirectory'] ) ) {
92 $this->dbDir = $params['dbDirectory'];
95 parent::__construct( $params );
97 $this->trxMode = strtoupper( $params['trxMode'] ?? '' );
99 $this->lockMgr = $this->makeLockManager();
100 $this->platform = new SqlitePlatform(
101 $this,
102 $this->logger,
103 $this->currentDomain,
104 $this->errorLogger
106 $this->replicationReporter = new ReplicationReporter(
107 $params['topologyRole'],
108 $this->logger,
109 $params['srvCache']
113 public static function getAttributes() {
114 return [
115 self::ATTR_DB_IS_FILE => true,
116 self::ATTR_DB_LEVEL_LOCKING => true
121 * @param string $filename
122 * @param array $p Options map; supports:
123 * - flags : (same as __construct counterpart)
124 * - trxMode : (same as __construct counterpart)
125 * - dbDirectory : (same as __construct counterpart)
126 * @return DatabaseSqlite
127 * @since 1.25
129 public static function newStandaloneInstance( $filename, array $p = [] ) {
130 $p['dbFilePath'] = $filename;
131 $p['schema'] = null;
132 $p['tablePrefix'] = '';
133 /** @var DatabaseSqlite $db */
134 $db = ( new DatabaseFactory() )->create( 'sqlite', $p );
135 '@phan-var DatabaseSqlite $db';
137 return $db;
141 * @return string
143 public function getType() {
144 return 'sqlite';
147 protected function open( $server, $user, $password, $db, $schema, $tablePrefix ) {
148 $this->close( __METHOD__ );
150 // Note that for SQLite, $server, $user, and $pass are ignored
152 if ( $schema !== null ) {
153 throw $this->newExceptionAfterConnectError( "Got schema '$schema'; not supported." );
156 if ( $this->dbPath !== null ) {
157 $path = $this->dbPath;
158 } elseif ( $this->dbDir !== null ) {
159 $path = self::generateFileName( $this->dbDir, $db );
160 } else {
161 throw $this->newExceptionAfterConnectError( "DB path or directory required" );
164 // Check if the database file already exists but is non-readable
165 if ( !self::isProcessMemoryPath( $path ) && is_file( $path ) && !is_readable( $path ) ) {
166 throw $this->newExceptionAfterConnectError( 'SQLite database file is not readable' );
167 } elseif ( !in_array( $this->trxMode, self::VALID_TRX_MODES, true ) ) {
168 throw $this->newExceptionAfterConnectError( "Got mode '{$this->trxMode}' for BEGIN" );
171 $attributes = [
172 PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT,
173 // Starting with PHP 8.1, The SQLite PDO returns proper types instead
174 // of strings or null for everything. We cast every non-null value to
175 // string to restore the old behavior.
176 PDO::ATTR_STRINGIFY_FETCHES => true
178 if ( $this->getFlag( self::DBO_PERSISTENT ) ) {
179 // Persistent connections can avoid some schema index reading overhead.
180 // On the other hand, they can cause horrible contention with DBO_TRX.
181 if ( $this->getFlag( self::DBO_TRX ) || $this->getFlag( self::DBO_DEFAULT ) ) {
182 $this->logger->warning(
183 __METHOD__ . ": ignoring DBO_PERSISTENT due to DBO_TRX or DBO_DEFAULT",
184 $this->getLogContext()
186 } else {
187 $attributes[PDO::ATTR_PERSISTENT] = true;
191 try {
192 // Open the database file, creating it if it does not yet exist
193 $this->conn = new PDO( "sqlite:$path", null, null, $attributes );
194 } catch ( PDOException $e ) {
195 throw $this->newExceptionAfterConnectError( $e->getMessage() );
198 $this->currentDomain = new DatabaseDomain( $db, null, $tablePrefix );
199 $this->platform->setCurrentDomain( $this->currentDomain );
201 try {
202 // Enforce LIKE to be case sensitive, just like MySQL
203 $query = new Query(
204 'PRAGMA case_sensitive_like = 1',
205 self::QUERY_CHANGE_TRX | self::QUERY_NO_RETRY,
206 'PRAGMA'
208 $this->query( $query, __METHOD__ );
209 // Set any connection-level custom PRAGMA options
210 $pragmas = array_intersect_key( $this->connectionVariables, self::VALID_PRAGMAS );
211 $pragmas += $this->getDefaultPragmas();
212 foreach ( $pragmas as $name => $value ) {
213 $allowed = self::VALID_PRAGMAS[$name];
214 if (
215 ( is_array( $allowed ) && in_array( $value, $allowed, true ) ) ||
216 ( is_string( $allowed ) && gettype( $value ) === $allowed )
218 $query = new Query(
219 "PRAGMA $name = $value",
220 self::QUERY_CHANGE_TRX | self::QUERY_NO_RETRY,
221 'PRAGMA',
222 null,
223 "PRAGMA $name = '?'"
225 $this->query( $query, __METHOD__ );
228 $this->attachDatabasesFromTableAliases();
229 } catch ( RuntimeException $e ) {
230 throw $this->newExceptionAfterConnectError( $e->getMessage() );
235 * @return array Map of (name => value) for default values to set via PRAGMA
237 private function getDefaultPragmas() {
238 $variables = [];
240 if ( !$this->cliMode ) {
241 $variables['temp_store'] = 'MEMORY';
244 return $variables;
248 * @return string|null SQLite DB file path
249 * @throws DBUnexpectedError
250 * @since 1.25
252 public function getDbFilePath() {
253 return $this->dbPath ?? self::generateFileName( $this->dbDir, $this->getDBname() );
257 * @return string|null Lock file directory
259 public function getLockFileDirectory() {
260 if ( $this->dbPath !== null && !self::isProcessMemoryPath( $this->dbPath ) ) {
261 return dirname( $this->dbPath ) . '/locks';
262 } elseif ( $this->dbDir !== null && !self::isProcessMemoryPath( $this->dbDir ) ) {
263 return $this->dbDir . '/locks';
266 return null;
270 * Initialize/reset the LockManager instance
272 * @return LockManager
274 private function makeLockManager(): LockManager {
275 $lockDirectory = $this->getLockFileDirectory();
276 if ( $lockDirectory !== null ) {
277 return new FSLockManager( [
278 'domain' => $this->getDomainID(),
279 'lockDirectory' => $lockDirectory,
280 ] );
281 } else {
282 return new NullLockManager( [ 'domain' => $this->getDomainID() ] );
287 * Does not actually close the connection, just destroys the reference for GC to do its work
288 * @return bool
290 protected function closeConnection() {
291 $this->conn = null;
292 // Release all locks, via FSLockManager::__destruct, as the base class expects
293 $this->lockMgr = null;
295 return true;
299 * Generates a database file name. Explicitly public for installer.
300 * @param string $dir Directory where database resides
301 * @param string|null $dbName Database name (or null from Database::factory, validated here)
302 * @return string
303 * @throws DBUnexpectedError
305 public static function generateFileName( $dir, $dbName ) {
306 if ( $dir == '' ) {
307 throw new DBUnexpectedError( null, __CLASS__ . ": no DB directory specified" );
308 } elseif ( self::isProcessMemoryPath( $dir ) ) {
309 throw new DBUnexpectedError(
310 null,
311 __CLASS__ . ": cannot use process memory directory '$dir'"
313 } elseif ( !strlen( $dbName ) ) {
314 throw new DBUnexpectedError( null, __CLASS__ . ": no DB name specified" );
317 return "$dir/$dbName.sqlite";
321 * @param string $path
322 * @return string
324 private static function generateDatabaseName( $path ) {
325 if ( preg_match( '/^(:memory:$|file::memory:)/', $path ) ) {
326 // E.g. "file::memory:?cache=shared" => ":memory":
327 return ':memory:';
328 } elseif ( preg_match( '/^file::([^?]+)\?mode=memory(&|$)/', $path, $m ) ) {
329 // E.g. "file:memdb1?mode=memory" => ":memdb1:"
330 return ":{$m[1]}:";
331 } else {
332 // E.g. "/home/.../some_db.sqlite3" => "some_db"
333 return preg_replace( '/\.sqlite\d?$/', '', basename( $path ) );
338 * @param string $path
339 * @return bool
341 private static function isProcessMemoryPath( $path ) {
342 return preg_match( '/^(:memory:$|file:(:memory:|[^?]+\?mode=memory(&|$)))/', $path );
346 * Returns version of currently supported SQLite fulltext search module or false if none present.
347 * @return string|false
349 public static function getFulltextSearchModule() {
350 static $cachedResult = null;
351 if ( $cachedResult !== null ) {
352 return $cachedResult;
354 $cachedResult = false;
355 $table = 'dummy_search_test';
357 $db = self::newStandaloneInstance( ':memory:' );
358 if ( $db->query(
359 "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)",
360 __METHOD__,
361 IDatabase::QUERY_SILENCE_ERRORS
362 ) ) {
363 $cachedResult = 'FTS3';
365 $db->close( __METHOD__ );
367 return $cachedResult;
371 * Attaches external database to the connection handle
373 * @see https://sqlite.org/lang_attach.html
375 * @param string $name Database name to be used in queries like
376 * SELECT foo FROM dbname.table
377 * @param bool|string $file Database file name. If omitted, will be generated
378 * using $name and configured data directory
379 * @param string $fname Calling function name @phan-mandatory-param
380 * @return IResultWrapper
382 public function attachDatabase( $name, $file = false, $fname = __METHOD__ ) {
383 $file = is_string( $file ) ? $file : self::generateFileName( $this->dbDir, $name );
384 $encFile = $this->addQuotes( $file );
385 $query = new Query(
386 "ATTACH DATABASE $encFile AS $name",
387 self::QUERY_CHANGE_TRX,
388 'ATTACH'
390 return $this->query( $query, $fname );
393 protected function doSingleStatementQuery( string $sql ): QueryStatus {
394 $res = $this->getBindingHandle()->query( $sql );
395 // Note that rowCount() returns 0 for SELECT for SQLite
396 return new QueryStatus(
397 $res instanceof PDOStatement ? new SqliteResultWrapper( $res ) : $res,
398 $res ? $res->rowCount() : 0,
399 $this->lastError(),
400 $this->lastErrno()
404 protected function doSelectDomain( DatabaseDomain $domain ) {
405 if ( $domain->getSchema() !== null ) {
406 throw new DBExpectedError(
407 $this,
408 __CLASS__ . ": domain '{$domain->getId()}' has a schema component"
412 $database = $domain->getDatabase();
413 // A null database means "don't care" so leave it as is and update the table prefix
414 if ( $database === null ) {
415 $this->currentDomain = new DatabaseDomain(
416 $this->currentDomain->getDatabase(),
417 null,
418 $domain->getTablePrefix()
420 $this->platform->setCurrentDomain( $this->currentDomain );
422 return true;
425 if ( $database !== $this->getDBname() ) {
426 throw new DBExpectedError(
427 $this,
428 __CLASS__ . ": cannot change database (got '$database')"
432 // Update that domain fields on success (no exception thrown)
433 $this->currentDomain = $domain;
434 $this->platform->setCurrentDomain( $domain );
436 return true;
439 protected function lastInsertId() {
440 // PDO::lastInsertId yields a string :(
441 return (int)$this->getBindingHandle()->lastInsertId();
445 * @return string
447 public function lastError() {
448 if ( is_object( $this->conn ) ) {
449 $e = $this->conn->errorInfo();
451 return $e[2] ?? $this->lastConnectError;
454 return 'No database connection';
458 * @return int
460 public function lastErrno() {
461 if ( is_object( $this->conn ) ) {
462 $info = $this->conn->errorInfo();
464 if ( isset( $info[1] ) ) {
465 return $info[1];
469 return 0;
472 public function tableExists( $table, $fname = __METHOD__ ) {
473 [ $db, $pt ] = $this->platform->getDatabaseAndTableIdentifier( $table );
474 if ( isset( $this->sessionTempTables[$db][$pt] ) ) {
475 return true; // already known to exist
478 $encTable = $this->addQuotes( $pt );
479 $query = new Query(
480 "SELECT 1 FROM sqlite_master WHERE type='table' AND name=$encTable",
481 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE,
482 'SELECT'
484 $res = $this->query( $query, __METHOD__ );
486 return (bool)$res->numRows();
489 public function indexInfo( $table, $index, $fname = __METHOD__ ) {
490 $indexName = $this->platform->indexName( $index );
491 $components = $this->platform->qualifiedTableComponents( $table );
492 $tableRaw = end( $components );
493 $query = new Query(
494 'PRAGMA index_list(' . $this->addQuotes( $tableRaw ) . ')',
495 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE,
496 'PRAGMA'
498 $res = $this->query( $query, $fname );
500 foreach ( $res as $row ) {
501 if ( $row->name === $indexName ) {
502 return [ 'unique' => (bool)$row->unique ];
506 return false;
509 public function replace( $table, $uniqueKeys, $rows, $fname = __METHOD__ ) {
510 $this->platform->normalizeUpsertParams( $uniqueKeys, $rows );
511 if ( !$rows ) {
512 return;
514 $encTable = $this->tableName( $table );
515 [ $sqlColumns, $sqlTuples ] = $this->platform->makeInsertLists( $rows );
516 // https://sqlite.org/lang_insert.html
517 // Note that any auto-increment columns on conflicting rows will be reassigned
518 // due to combined DELETE+INSERT semantics. This will be reflected in insertId().
519 $query = new Query(
520 "REPLACE INTO $encTable ($sqlColumns) VALUES $sqlTuples",
521 self::QUERY_CHANGE_ROWS,
522 'REPLACE',
523 $table
525 $this->query( $query, $fname );
528 protected function isConnectionError( $errno ) {
529 return $errno == 17; // SQLITE_SCHEMA;
532 protected function isKnownStatementRollbackError( $errno ) {
533 // ON CONFLICT ROLLBACK clauses make it so that SQLITE_CONSTRAINT error is
534 // ambiguous with regard to whether it implies a ROLLBACK or an ABORT happened.
535 // https://sqlite.org/lang_createtable.html#uniqueconst
536 // https://sqlite.org/lang_conflict.html
537 return false;
540 public function serverIsReadOnly() {
541 $this->assertHasConnectionHandle();
543 $path = $this->getDbFilePath();
545 return ( !self::isProcessMemoryPath( $path ) && !is_writable( $path ) );
549 * @return string Wikitext of a link to the server software's web site
551 public function getSoftwareLink() {
552 return "[{{int:version-db-sqlite-url}} SQLite]";
556 * @return string Version information from the database
558 public function getServerVersion() {
559 if ( $this->version === null ) {
560 $this->version = $this->getBindingHandle()->getAttribute( PDO::ATTR_SERVER_VERSION );
563 return $this->version;
567 * Get information about a given field
568 * Returns false if the field does not exist.
570 * @param string $table
571 * @param string $field
572 * @return SQLiteField|false False on failure
574 public function fieldInfo( $table, $field ) {
575 $components = $this->platform->qualifiedTableComponents( $table );
576 $tableRaw = end( $components );
577 $query = new Query(
578 'PRAGMA table_info(' . $this->addQuotes( $tableRaw ) . ')',
579 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE,
580 'PRAGMA'
582 $res = $this->query( $query, __METHOD__ );
583 foreach ( $res as $row ) {
584 if ( $row->name == $field ) {
585 return new SQLiteField( $row, $tableRaw );
589 return false;
592 protected function doBegin( $fname = '' ) {
593 if ( $this->trxMode != '' ) {
594 $sql = "BEGIN {$this->trxMode}";
595 } else {
596 $sql = 'BEGIN';
598 $query = new Query( $sql, self::QUERY_CHANGE_TRX, 'BEGIN' );
599 $this->query( $query, $fname );
603 * @param string $s
604 * @return string
606 public function strencode( $s ) {
607 return substr( $this->addQuotes( $s ), 1, -1 );
611 * @param string $b
612 * @return Blob
614 public function encodeBlob( $b ) {
615 return new Blob( $b );
619 * @param Blob|string $b
620 * @return string
622 public function decodeBlob( $b ) {
623 if ( $b instanceof Blob ) {
624 $b = $b->fetch();
626 if ( $b === null ) {
627 // An empty blob is decoded as null in PHP before PHP 8.1.
628 // It was probably fixed as a side-effect of caa710037e663fd78f67533b29611183090068b2
629 $b = '';
632 return $b;
635 public function addQuotes( $s ) {
636 if ( $s instanceof RawSQLValue ) {
637 return $s->toSql();
639 if ( $s instanceof Blob ) {
640 return "x'" . bin2hex( $s->fetch() ) . "'";
641 } elseif ( is_bool( $s ) ) {
642 return (string)(int)$s;
643 } elseif ( is_int( $s ) ) {
644 return (string)$s;
645 } elseif ( strpos( (string)$s, "\0" ) !== false ) {
646 // SQLite doesn't support \0 in strings, so use the hex representation as a workaround.
647 // This is a known limitation of SQLite's mprintf function which PDO
648 // should work around, but doesn't. I have reported this to php.net as bug #63419:
649 // https://bugs.php.net/bug.php?id=63419
650 // There was already a similar report for SQLite3::escapeString, bug #62361:
651 // https://bugs.php.net/bug.php?id=62361
652 // There is an additional bug regarding sorting this data after insert
653 // on older versions of sqlite shipped with ubuntu 12.04
654 // https://phabricator.wikimedia.org/T74367
655 $this->logger->debug(
656 __FUNCTION__ .
657 ': Quoting value containing null byte. ' .
658 'For consistency all binary data should have been ' .
659 'first processed with self::encodeBlob()'
661 return "x'" . bin2hex( (string)$s ) . "'";
662 } else {
663 return $this->getBindingHandle()->quote( (string)$s );
667 public function doLockIsFree( string $lockName, string $method ) {
668 // Only locks by this thread will be checked
669 return true;
672 public function doLock( string $lockName, string $method, int $timeout ) {
673 $status = $this->lockMgr->lock( [ $lockName ], LockManager::LOCK_EX, $timeout );
674 if (
675 $this->lockMgr instanceof FSLockManager &&
676 $status->hasMessage( 'lockmanager-fail-openlock' )
678 throw new DBError( $this, "Cannot create directory \"{$this->getLockFileDirectory()}\"" );
681 return $status->isOK() ? microtime( true ) : null;
684 public function doUnlock( string $lockName, string $method ) {
685 return $this->lockMgr->unlock( [ $lockName ], LockManager::LOCK_EX )->isGood();
689 * @param string $oldName
690 * @param string $newName
691 * @param bool $temporary
692 * @param string $fname
693 * @return bool|IResultWrapper
694 * @throws RuntimeException
696 public function duplicateTableStructure(
697 $oldName, $newName, $temporary = false, $fname = __METHOD__
699 $query = new Query(
700 "SELECT sql FROM sqlite_master WHERE tbl_name=" .
701 $this->addQuotes( $oldName ) . " AND type='table'",
702 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE,
703 'SELECT'
705 $res = $this->query( $query, $fname );
706 $obj = $res->fetchObject();
707 if ( !$obj ) {
708 throw new RuntimeException( "Couldn't retrieve structure for table $oldName" );
710 $sqlCreateTable = $obj->sql;
711 $sqlCreateTable = preg_replace(
712 '/(?<=\W)"?' .
713 preg_quote( trim( $this->platform->addIdentifierQuotes( $oldName ), '"' ), '/' ) .
714 '"?(?=\W)/',
715 $this->platform->addIdentifierQuotes( $newName ),
716 $sqlCreateTable,
719 $flags = self::QUERY_CHANGE_SCHEMA | self::QUERY_PSEUDO_PERMANENT;
720 if ( $temporary ) {
721 if ( preg_match( '/^\\s*CREATE\\s+VIRTUAL\\s+TABLE\b/i', $sqlCreateTable ) ) {
722 $this->logger->debug(
723 "Table $oldName is virtual, can't create a temporary duplicate." );
724 } else {
725 $sqlCreateTable = str_replace(
726 'CREATE TABLE',
727 'CREATE TEMPORARY TABLE',
728 $sqlCreateTable
733 $query = new Query(
734 $sqlCreateTable,
735 $flags,
736 $temporary ? 'CREATE TEMPORARY' : 'CREATE',
737 // Use a dot to avoid double-prefixing in Database::getTempTableWrites()
738 '.' . $newName
740 $res = $this->query( $query, $fname );
742 $query = new Query(
743 'PRAGMA INDEX_LIST(' . $this->addQuotes( $oldName ) . ')',
744 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE,
745 'PRAGMA'
747 // Take over indexes
748 $indexList = $this->query( $query, $fname );
749 foreach ( $indexList as $index ) {
750 if ( strpos( $index->name, 'sqlite_autoindex' ) === 0 ) {
751 continue;
754 if ( $index->unique ) {
755 $sqlIndex = 'CREATE UNIQUE INDEX';
756 } else {
757 $sqlIndex = 'CREATE INDEX';
759 // Try to come up with a new index name, given indexes have database scope in SQLite
760 $indexName = $newName . '_' . $index->name;
761 $sqlIndex .= ' ' . $this->platform->addIdentifierQuotes( $indexName ) .
762 ' ON ' . $this->platform->addIdentifierQuotes( $newName );
764 $query = new Query(
765 'PRAGMA INDEX_INFO(' . $this->addQuotes( $index->name ) . ')',
766 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE,
767 'PRAGMA'
769 $indexInfo = $this->query( $query, $fname );
770 $fields = [];
771 foreach ( $indexInfo as $indexInfoRow ) {
772 $fields[$indexInfoRow->seqno] = $this->addQuotes( $indexInfoRow->name );
775 $sqlIndex .= '(' . implode( ',', $fields ) . ')';
777 $query = new Query(
778 $sqlIndex,
779 self::QUERY_CHANGE_SCHEMA | self::QUERY_PSEUDO_PERMANENT,
780 'CREATE',
781 $newName
783 $this->query( $query, __METHOD__ );
786 return $res;
790 * List all tables on the database
792 * @param string|null $prefix Only show tables with this prefix, e.g. mw_
793 * @param string $fname Calling function name
795 * @return array
797 public function listTables( $prefix = null, $fname = __METHOD__ ) {
798 $query = new Query(
799 "SELECT name FROM sqlite_master WHERE type = 'table'",
800 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE,
801 'SELECT'
803 $result = $this->query( $query, $fname );
805 $endArray = [];
807 foreach ( $result as $table ) {
808 $vars = get_object_vars( $table );
809 $table = array_pop( $vars );
811 if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
812 if ( strpos( $table, 'sqlite_' ) !== 0 ) {
813 $endArray[] = $table;
818 return $endArray;
821 public function truncateTable( $table, $fname = __METHOD__ ) {
822 $this->startAtomic( $fname );
823 // Use "truncate" optimization; https://www.sqlite.org/lang_delete.html
824 $query = new Query(
825 "DELETE FROM " . $this->tableName( $table ),
826 self::QUERY_CHANGE_SCHEMA,
827 'DELETE',
828 $table
830 $this->query( $query, $fname );
832 $encMasterTable = $this->platform->addIdentifierQuotes( 'sqlite_sequence' );
833 $encSequenceName = $this->addQuotes( $this->tableName( $table, 'raw' ) );
834 $query = new Query(
835 "DELETE FROM $encMasterTable WHERE name = $encSequenceName",
836 self::QUERY_CHANGE_SCHEMA,
837 'DELETE',
838 'sqlite_sequence'
840 $this->query( $query, $fname );
842 $this->endAtomic( $fname );
845 public function setTableAliases( array $aliases ) {
846 parent::setTableAliases( $aliases );
847 if ( $this->isOpen() ) {
848 $this->attachDatabasesFromTableAliases();
853 * Issue ATTATCH statements for all unattached foreign DBs in table aliases
855 private function attachDatabasesFromTableAliases() {
856 foreach ( $this->platform->getTableAliases() as $params ) {
857 if (
858 $params['dbname'] !== $this->getDBname() &&
859 !isset( $this->sessionAttachedDbs[$params['dbname']] )
861 $this->attachDatabase( $params['dbname'], false, __METHOD__ );
862 $this->sessionAttachedDbs[$params['dbname']] = true;
867 public function databasesAreIndependent() {
868 return true;
871 protected function doHandleSessionLossPreconnect() {
872 $this->sessionAttachedDbs = [];
873 // Release all locks, via FSLockManager::__destruct, as the base class expects;
874 $this->lockMgr = null;
875 // Create a new lock manager instance
876 $this->lockMgr = $this->makeLockManager();
879 protected function doFlushSession( $fname ) {
880 // Release all locks, via FSLockManager::__destruct, as the base class expects
881 $this->lockMgr = null;
882 // Create a new lock manager instance
883 $this->lockMgr = $this->makeLockManager();
887 * @return PDO
889 protected function getBindingHandle() {
890 return parent::getBindingHandle();
893 protected function getInsertIdColumnForUpsert( $table ) {
894 $components = $this->platform->qualifiedTableComponents( $table );
895 $tableRaw = end( $components );
896 $query = new Query(
897 'PRAGMA table_info(' . $this->addQuotes( $tableRaw ) . ')',
898 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE,
899 'PRAGMA'
901 $res = $this->query( $query, __METHOD__ );
902 foreach ( $res as $row ) {
903 if ( $row->pk && strtolower( $row->type ) === 'integer' ) {
904 return $row->name;
908 return null;