MediaWiki REL1_37
DatabaseSqlite.php
Go to the documentation of this file.
1<?php
24namespace Wikimedia\Rdbms;
25
27use LockManager;
29use PDO;
30use PDOException;
31use RuntimeException;
32
36class DatabaseSqlite extends Database {
38 protected $dbDir;
40 protected $dbPath;
42 protected $trxMode;
43
46
48 protected $conn;
49
51 protected $lockMgr;
52
54 private $version;
55
57 private $sessionAttachedDbs = [];
58
60 private static $VALID_TRX_MODES = [ '', 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ];
61
63 private static $VALID_PRAGMAS = [
64 // Optimizations or requirements regarding fsync() usage
65 'synchronous' => [ 'EXTRA', 'FULL', 'NORMAL', 'OFF' ],
66 // Optimizations for TEMPORARY tables
67 'temp_store' => [ 'FILE', 'MEMORY' ]
68 ];
69
77 public function __construct( array $params ) {
78 if ( isset( $params['dbFilePath'] ) ) {
79 $this->dbPath = $params['dbFilePath'];
80 if ( !isset( $params['dbname'] ) || $params['dbname'] === '' ) {
81 $params['dbname'] = self::generateDatabaseName( $this->dbPath );
82 }
83 } elseif ( isset( $params['dbDirectory'] ) ) {
84 $this->dbDir = $params['dbDirectory'];
85 }
86
87 parent::__construct( $params );
88
89 $this->trxMode = strtoupper( $params['trxMode'] ?? '' );
90
91 $lockDirectory = $this->getLockFileDirectory();
92 if ( $lockDirectory !== null ) {
93 $this->lockMgr = new FSLockManager( [
94 'domain' => $this->getDomainID(),
95 'lockDirectory' => $lockDirectory
96 ] );
97 } else {
98 $this->lockMgr = new NullLockManager( [ 'domain' => $this->getDomainID() ] );
99 }
100 }
101
102 protected static function getAttributes() {
103 return [
104 self::ATTR_DB_IS_FILE => true,
105 self::ATTR_DB_LEVEL_LOCKING => true
106 ];
107 }
108
118 public static function newStandaloneInstance( $filename, array $p = [] ) {
119 $p['dbFilePath'] = $filename;
120 $p['schema'] = null;
121 $p['tablePrefix'] = '';
123 $db = Database::factory( 'sqlite', $p );
124
125 return $db;
126 }
127
131 public function getType() {
132 return 'sqlite';
133 }
134
135 protected function open( $server, $user, $password, $db, $schema, $tablePrefix ) {
136 $this->close( __METHOD__ );
137
138 // Note that for SQLite, $server, $user, and $pass are ignored
139
140 if ( $schema !== null ) {
141 throw $this->newExceptionAfterConnectError( "Got schema '$schema'; not supported." );
142 }
143
144 if ( $this->dbPath !== null ) {
146 } elseif ( $this->dbDir !== null ) {
147 $path = self::generateFileName( $this->dbDir, $db );
148 } else {
149 throw $this->newExceptionAfterConnectError( "DB path or directory required" );
150 }
151
152 // Check if the database file already exists but is non-readable
153 if ( !self::isProcessMemoryPath( $path ) && is_file( $path ) && !is_readable( $path ) ) {
154 throw $this->newExceptionAfterConnectError( 'SQLite database file is not readable' );
155 } elseif ( !in_array( $this->trxMode, self::$VALID_TRX_MODES, true ) ) {
156 throw $this->newExceptionAfterConnectError( "Got mode '{$this->trxMode}' for BEGIN" );
157 }
158
159 $attributes = [];
160 if ( $this->getFlag( self::DBO_PERSISTENT ) ) {
161 // Persistent connections can avoid some schema index reading overhead.
162 // On the other hand, they can cause horrible contention with DBO_TRX.
163 if ( $this->getFlag( self::DBO_TRX ) || $this->getFlag( self::DBO_DEFAULT ) ) {
164 $this->connLogger->warning(
165 __METHOD__ . ": ignoring DBO_PERSISTENT due to DBO_TRX or DBO_DEFAULT",
166 $this->getLogContext()
167 );
168 } else {
169 $attributes[PDO::ATTR_PERSISTENT] = true;
170 }
171 }
172
173 try {
174 // Open the database file, creating it if it does not yet exist
175 $this->conn = new PDO( "sqlite:$path", null, null, $attributes );
176 } catch ( PDOException $e ) {
177 throw $this->newExceptionAfterConnectError( $e->getMessage() );
178 }
179
180 $this->currentDomain = new DatabaseDomain( $db, null, $tablePrefix );
181
182 try {
183 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_NO_RETRY;
184 // Enforce LIKE to be case sensitive, just like MySQL
185 $this->query( 'PRAGMA case_sensitive_like = 1', __METHOD__, $flags );
186 // Set any connection-level custom PRAGMA options
187 $pragmas = array_intersect_key( $this->connectionVariables, self::$VALID_PRAGMAS );
188 $pragmas += $this->getDefaultPragmas();
189 foreach ( $pragmas as $name => $value ) {
190 $allowed = self::$VALID_PRAGMAS[$name];
191 if ( in_array( $value, $allowed, true ) ) {
192 $this->query( "PRAGMA $name = $value", __METHOD__, $flags );
193 }
194 }
196 } catch ( RuntimeException $e ) {
197 throw $this->newExceptionAfterConnectError( $e->getMessage() );
198 }
199 }
200
204 private function getDefaultPragmas() {
205 $variables = [];
206
207 if ( !$this->cliMode ) {
208 $variables['temp_store'] = 'MEMORY';
209 }
210
211 return $variables;
212 }
213
219 public function getDbFilePath() {
220 return $this->dbPath ?? self::generateFileName( $this->dbDir, $this->getDBname() );
221 }
222
226 public function getLockFileDirectory() {
227 if ( $this->dbPath !== null && !self::isProcessMemoryPath( $this->dbPath ) ) {
228 return dirname( $this->dbPath ) . '/locks';
229 } elseif ( $this->dbDir !== null && !self::isProcessMemoryPath( $this->dbDir ) ) {
230 return $this->dbDir . '/locks';
231 }
232
233 return null;
234 }
235
240 protected function closeConnection() {
241 $this->conn = null;
242
243 return true;
244 }
245
253 public static function generateFileName( $dir, $dbName ) {
254 if ( $dir == '' ) {
255 throw new DBUnexpectedError( null, __CLASS__ . ": no DB directory specified" );
256 } elseif ( self::isProcessMemoryPath( $dir ) ) {
257 throw new DBUnexpectedError(
258 null,
259 __CLASS__ . ": cannot use process memory directory '$dir'"
260 );
261 } elseif ( !strlen( $dbName ) ) {
262 throw new DBUnexpectedError( null, __CLASS__ . ": no DB name specified" );
263 }
264
265 return "$dir/$dbName.sqlite";
266 }
267
272 private static function generateDatabaseName( $path ) {
273 if ( preg_match( '/^(:memory:$|file::memory:)/', $path ) ) {
274 // E.g. "file::memory:?cache=shared" => ":memory":
275 return ':memory:';
276 } elseif ( preg_match( '/^file::([^?]+)\?mode=memory(&|$)/', $path, $m ) ) {
277 // E.g. "file:memdb1?mode=memory" => ":memdb1:"
278 return ":{$m[1]}:";
279 } else {
280 // E.g. "/home/.../some_db.sqlite3" => "some_db"
281 return preg_replace( '/\.sqlite\d?$/', '', basename( $path ) );
282 }
283 }
284
289 private static function isProcessMemoryPath( $path ) {
290 return preg_match( '/^(:memory:$|file:(:memory:|[^?]+\?mode=memory(&|$)))/', $path );
291 }
292
297 public static function getFulltextSearchModule() {
298 static $cachedResult = null;
299 if ( $cachedResult !== null ) {
300 return $cachedResult;
301 }
302 $cachedResult = false;
303 $table = 'dummy_search_test';
304
305 $db = self::newStandaloneInstance( ':memory:' );
306 if ( $db->query(
307 "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)",
308 __METHOD__,
309 IDatabase::QUERY_SILENCE_ERRORS
310 ) ) {
311 $cachedResult = 'FTS3';
312 }
313 $db->close( __METHOD__ );
314
315 return $cachedResult;
316 }
317
330 public function attachDatabase( $name, $file = false, $fname = __METHOD__ ) {
331 $file = is_string( $file ) ? $file : self::generateFileName( $this->dbDir, $name );
332 $encFile = $this->addQuotes( $file );
333
334 return $this->query(
335 "ATTACH DATABASE $encFile AS $name",
336 $fname,
337 self::QUERY_IGNORE_DBO_TRX
338 );
339 }
340
341 protected function isWriteQuery( $sql, $flags ) {
342 return parent::isWriteQuery( $sql, $flags ) && !preg_match( '/^(ATTACH|PRAGMA)\b/i', $sql );
343 }
344
345 protected function isTransactableQuery( $sql ) {
346 return parent::isTransactableQuery( $sql ) && !in_array(
347 $this->getQueryVerb( $sql ),
348 [ 'ATTACH', 'PRAGMA' ],
349 true
350 );
351 }
352
357 protected function doQuery( $sql ) {
358 $res = $this->getBindingHandle()->query( $sql );
359 if ( $res === false ) {
360 return false;
361 }
362
363 $this->lastAffectedRowCount = $res->rowCount();
364 return new SqliteResultWrapper( $res );
365 }
366
367 protected function doSelectDomain( DatabaseDomain $domain ) {
368 if ( $domain->getSchema() !== null ) {
369 throw new DBExpectedError(
370 $this,
371 __CLASS__ . ": domain '{$domain->getId()}' has a schema component"
372 );
373 }
374
375 $database = $domain->getDatabase();
376 // A null database means "don't care" so leave it as is and update the table prefix
377 if ( $database === null ) {
378 $this->currentDomain = new DatabaseDomain(
379 $this->currentDomain->getDatabase(),
380 null,
381 $domain->getTablePrefix()
382 );
383
384 return true;
385 }
386
387 if ( $database !== $this->getDBname() ) {
388 throw new DBExpectedError(
389 $this,
390 __CLASS__ . ": cannot change database (got '$database')"
391 );
392 }
393
394 return true;
395 }
396
404 public function tableName( $name, $format = 'quoted' ) {
405 // table names starting with sqlite_ are reserved
406 if ( strpos( $name, 'sqlite_' ) === 0 ) {
407 return $name;
408 }
409
410 return str_replace( '"', '', parent::tableName( $name, $format ) );
411 }
412
418 public function insertId() {
419 // PDO::lastInsertId yields a string :(
420 return intval( $this->getBindingHandle()->lastInsertId() );
421 }
422
426 public function lastError() {
427 if ( !is_object( $this->conn ) ) {
428 return "Cannot return last error, no db connection";
429 }
430 $e = $this->conn->errorInfo();
431
432 return $e[2] ?? '';
433 }
434
438 public function lastErrno() {
439 if ( !is_object( $this->conn ) ) {
440 return "Cannot return last error, no db connection";
441 } else {
442 $info = $this->conn->errorInfo();
443
444 return $info[1];
445 }
446 }
447
451 protected function fetchAffectedRowCount() {
453 }
454
455 public function tableExists( $table, $fname = __METHOD__ ) {
456 $tableRaw = $this->tableName( $table, 'raw' );
457 if ( isset( $this->sessionTempTables[$tableRaw] ) ) {
458 return true; // already known to exist
459 }
460
461 $encTable = $this->addQuotes( $tableRaw );
462 $res = $this->query(
463 "SELECT 1 FROM sqlite_master WHERE type='table' AND name=$encTable",
464 __METHOD__,
465 self::QUERY_IGNORE_DBO_TRX
466 );
467
468 return $res->numRows() ? true : false;
469 }
470
481 public function indexInfo( $table, $index, $fname = __METHOD__ ) {
482 $sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')';
483 $res = $this->query( $sql, $fname, self::QUERY_IGNORE_DBO_TRX );
484 if ( !$res || $res->numRows() == 0 ) {
485 return false;
486 }
487 $info = [];
488 foreach ( $res as $row ) {
489 $info[] = $row->name;
490 }
491
492 return $info;
493 }
494
501 public function indexUnique( $table, $index, $fname = __METHOD__ ) {
502 $row = $this->selectRow( 'sqlite_master', '*',
503 [
504 'type' => 'index',
505 'name' => $this->indexName( $index ),
506 ], $fname );
507 if ( !$row || !isset( $row->sql ) ) {
508 return null;
509 }
510
511 // $row->sql will be of the form CREATE [UNIQUE] INDEX ...
512 $indexPos = strpos( $row->sql, 'INDEX' );
513 if ( $indexPos === false ) {
514 return null;
515 }
516 $firstPart = substr( $row->sql, 0, $indexPos );
517 $options = explode( ' ', $firstPart );
518
519 return in_array( 'UNIQUE', $options );
520 }
521
522 protected function makeSelectOptions( array $options ) {
523 // Remove problematic options that the base implementation converts to SQL
524 foreach ( $options as $k => $v ) {
525 if ( is_numeric( $k ) && ( $v === 'FOR UPDATE' || $v === 'LOCK IN SHARE MODE' ) ) {
526 $options[$k] = '';
527 }
528 }
529
530 return parent::makeSelectOptions( $options );
531 }
532
537 protected function makeUpdateOptionsArray( $options ) {
538 $options = parent::makeUpdateOptionsArray( $options );
539 $options = $this->rewriteIgnoreKeyword( $options );
540
541 return $options;
542 }
543
548 private function rewriteIgnoreKeyword( $options ) {
549 # SQLite uses OR IGNORE not just IGNORE
550 foreach ( $options as $k => $v ) {
551 if ( $v == 'IGNORE' ) {
552 $options[$k] = 'OR IGNORE';
553 }
554 }
555
556 return $options;
557 }
558
560 return [ 'INSERT OR IGNORE INTO', '' ];
561 }
562
563 protected function doReplace( $table, array $identityKey, array $rows, $fname ) {
564 $encTable = $this->tableName( $table );
565 list( $sqlColumns, $sqlTuples ) = $this->makeInsertLists( $rows );
566 // https://sqlite.org/lang_insert.html
567 $this->query( "REPLACE INTO $encTable ($sqlColumns) VALUES $sqlTuples", $fname );
568 }
569
578 public function textFieldSize( $table, $field ) {
579 return -1;
580 }
581
585 public function unionSupportsOrderAndLimit() {
586 return false;
587 }
588
594 public function unionQueries( $sqls, $all ) {
595 $glue = $all ? ' UNION ALL ' : ' UNION ';
596
597 return implode( $glue, $sqls );
598 }
599
603 public function wasDeadlock() {
604 return $this->lastErrno() == 5; // SQLITE_BUSY
605 }
606
610 public function wasReadOnlyError() {
611 return $this->lastErrno() == 8; // SQLITE_READONLY;
612 }
613
614 public function wasConnectionError( $errno ) {
615 return $errno == 17; // SQLITE_SCHEMA;
616 }
617
618 protected function wasKnownStatementRollbackError() {
619 // ON CONFLICT ROLLBACK clauses make it so that SQLITE_CONSTRAINT error is
620 // ambiguous with regard to whether it implies a ROLLBACK or an ABORT happened.
621 // https://sqlite.org/lang_createtable.html#uniqueconst
622 // https://sqlite.org/lang_conflict.html
623 return false;
624 }
625
626 public function getTopologyBasedServerId() {
627 // Sqlite topologies trivially consist of single primary server for the dataset
628 return '0';
629 }
630
631 public function serverIsReadOnly() {
633
634 $path = $this->getDbFilePath();
635
636 return ( !self::isProcessMemoryPath( $path ) && !is_writable( $path ) );
637 }
638
642 public function getSoftwareLink() {
643 return "[{{int:version-db-sqlite-url}} SQLite]";
644 }
645
649 public function getServerVersion() {
650 if ( $this->version === null ) {
651 $this->version = $this->getBindingHandle()->getAttribute( PDO::ATTR_SERVER_VERSION );
652 }
653
654 return $this->version;
655 }
656
665 public function fieldInfo( $table, $field ) {
666 $tableName = $this->tableName( $table );
667 $sql = 'PRAGMA table_info(' . $this->addQuotes( $tableName ) . ')';
668 $res = $this->query( $sql, __METHOD__, self::QUERY_IGNORE_DBO_TRX );
669 foreach ( $res as $row ) {
670 if ( $row->name == $field ) {
671 return new SQLiteField( $row, $tableName );
672 }
673 }
674
675 return false;
676 }
677
678 protected function doBegin( $fname = '' ) {
679 if ( $this->trxMode != '' ) {
680 $this->query( "BEGIN {$this->trxMode}", $fname );
681 } else {
682 $this->query( 'BEGIN', $fname );
683 }
684 }
685
690 public function strencode( $s ) {
691 return substr( $this->addQuotes( $s ), 1, -1 );
692 }
693
698 public function encodeBlob( $b ) {
699 return new Blob( $b );
700 }
701
706 public function decodeBlob( $b ) {
707 if ( $b instanceof Blob ) {
708 $b = $b->fetch();
709 }
710
711 return $b;
712 }
713
718 public function addQuotes( $s ) {
719 if ( $s instanceof Blob ) {
720 return "x'" . bin2hex( $s->fetch() ) . "'";
721 } elseif ( is_bool( $s ) ) {
722 return (string)(int)$s;
723 } elseif ( is_int( $s ) ) {
724 return (string)$s;
725 } elseif ( strpos( (string)$s, "\0" ) !== false ) {
726 // SQLite doesn't support \0 in strings, so use the hex representation as a workaround.
727 // This is a known limitation of SQLite's mprintf function which PDO
728 // should work around, but doesn't. I have reported this to php.net as bug #63419:
729 // https://bugs.php.net/bug.php?id=63419
730 // There was already a similar report for SQLite3::escapeString, bug #62361:
731 // https://bugs.php.net/bug.php?id=62361
732 // There is an additional bug regarding sorting this data after insert
733 // on older versions of sqlite shipped with ubuntu 12.04
734 // https://phabricator.wikimedia.org/T74367
735 $this->queryLogger->debug(
736 __FUNCTION__ .
737 ': Quoting value containing null byte. ' .
738 'For consistency all binary data should have been ' .
739 'first processed with self::encodeBlob()'
740 );
741 return "x'" . bin2hex( (string)$s ) . "'";
742 } else {
743 return $this->getBindingHandle()->quote( (string)$s );
744 }
745 }
746
747 public function buildSubstring( $input, $startPosition, $length = null ) {
748 $this->assertBuildSubstringParams( $startPosition, $length );
749 $params = [ $input, $startPosition ];
750 if ( $length !== null ) {
751 $params[] = $length;
752 }
753 return 'SUBSTR(' . implode( ',', $params ) . ')';
754 }
755
761 public function buildStringCast( $field ) {
762 return 'CAST ( ' . $field . ' AS TEXT )';
763 }
764
771 public function deadlockLoop( ...$args ) {
772 $function = array_shift( $args );
773
774 return $function( ...$args );
775 }
776
781 protected function replaceVars( $s ) {
782 $s = parent::replaceVars( $s );
783 if ( preg_match( '/^\s*(CREATE|ALTER) TABLE/i', $s ) ) {
784 // CREATE TABLE hacks to allow schema file sharing with MySQL
785
786 // binary/varbinary column type -> blob
787 $s = preg_replace( '/\b(var)?binary(\‍(\d+\‍))/i', 'BLOB', $s );
788 // no such thing as unsigned
789 $s = preg_replace( '/\b(un)?signed\b/i', '', $s );
790 // INT -> INTEGER
791 $s = preg_replace( '/\b(tiny|small|medium|big|)int(\s*\‍(\s*\d+\s*\‍)|\b)/i', 'INTEGER', $s );
792 // floating point types -> REAL
793 $s = preg_replace(
794 '/\b(float|double(\s+precision)?)(\s*\‍(\s*\d+\s*(,\s*\d+\s*)?\‍)|\b)/i',
795 'REAL',
796 $s
797 );
798 // varchar -> TEXT
799 $s = preg_replace( '/\b(var)?char\s*\‍(.*?\‍)/i', 'TEXT', $s );
800 // TEXT normalization
801 $s = preg_replace( '/\b(tiny|medium|long)text\b/i', 'TEXT', $s );
802 // BLOB normalization
803 $s = preg_replace( '/\b(tiny|small|medium|long|)blob\b/i', 'BLOB', $s );
804 // BOOL -> INTEGER
805 $s = preg_replace( '/\bbool(ean)?\b/i', 'INTEGER', $s );
806 // DATETIME -> TEXT
807 $s = preg_replace( '/\b(datetime|timestamp)\b/i', 'TEXT', $s );
808 // No ENUM type
809 $s = preg_replace( '/\benum\s*\‍([^)]*\‍)/i', 'TEXT', $s );
810 // binary collation type -> nothing
811 $s = preg_replace( '/\bbinary\b/i', '', $s );
812 // auto_increment -> autoincrement
813 $s = preg_replace( '/\bauto_increment\b/i', 'AUTOINCREMENT', $s );
814 // No explicit options
815 $s = preg_replace( '/\‍)[^);]*(;?)\s*$/', ')\1', $s );
816 // AUTOINCREMENT should immedidately follow PRIMARY KEY
817 $s = preg_replace( '/primary key (.*?) autoincrement/i', 'PRIMARY KEY AUTOINCREMENT $1', $s );
818 } elseif ( preg_match( '/^\s*CREATE (\s*(?:UNIQUE|FULLTEXT)\s+)?INDEX/i', $s ) ) {
819 // No truncated indexes
820 $s = preg_replace( '/\‍(\d+\‍)/', '', $s );
821 // No FULLTEXT
822 $s = preg_replace( '/\bfulltext\b/i', '', $s );
823 } elseif ( preg_match( '/^\s*DROP INDEX/i', $s ) ) {
824 // DROP INDEX is database-wide, not table-specific, so no ON <table> clause.
825 $s = preg_replace( '/\sON\s+[^\s]*/i', '', $s );
826 } elseif ( preg_match( '/^\s*INSERT IGNORE\b/i', $s ) ) {
827 // INSERT IGNORE --> INSERT OR IGNORE
828 $s = preg_replace( '/^\s*INSERT IGNORE\b/i', 'INSERT OR IGNORE', $s );
829 }
830
831 return $s;
832 }
833
834 public function doLockIsFree( string $lockName, string $method ) {
835 // Only locks by this thread will be checked
836 return true;
837 }
838
839 public function doLock( string $lockName, string $method, int $timeout ) {
840 $status = $this->lockMgr->lock( [ $lockName ], LockManager::LOCK_EX, $timeout );
841 if (
842 $this->lockMgr instanceof FSLockManager &&
843 $status->hasMessage( 'lockmanager-fail-openlock' )
844 ) {
845 throw new DBError( $this, "Cannot create directory \"{$this->getLockFileDirectory()}\"" );
846 }
847
848 return $status->isOK() ? microtime( true ) : null;
849 }
850
851 public function doUnlock( string $lockName, string $method ) {
852 return $this->lockMgr->unlock( [ $lockName ], LockManager::LOCK_EX )->isGood();
853 }
854
861 public function buildConcat( $stringList ) {
862 return '(' . implode( ') || (', $stringList ) . ')';
863 }
864
865 public function buildGroupConcatField(
866 $delim, $table, $field, $conds = '', $join_conds = []
867 ) {
868 $fld = "group_concat($field," . $this->addQuotes( $delim ) . ')';
869
870 return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
871 }
872
873 public function buildGreatest( $fields, $values ) {
874 return $this->buildSuperlative( 'MAX', $fields, $values );
875 }
876
877 public function buildLeast( $fields, $values ) {
878 return $this->buildSuperlative( 'MIN', $fields, $values );
879 }
880
889 public function duplicateTableStructure(
890 $oldName, $newName, $temporary = false, $fname = __METHOD__
891 ) {
892 $queryFlags = self::QUERY_PSEUDO_PERMANENT | self::QUERY_IGNORE_DBO_TRX;
893
894 $res = $this->query(
895 "SELECT sql FROM sqlite_master WHERE tbl_name=" .
896 $this->addQuotes( $oldName ) . " AND type='table'",
897 $fname,
898 $queryFlags
899 );
900 $obj = $this->fetchObject( $res );
901 if ( !$obj ) {
902 throw new RuntimeException( "Couldn't retrieve structure for table $oldName" );
903 }
904 $sql = $obj->sql;
905 $sql = preg_replace(
906 '/(?<=\W)"?' .
907 preg_quote( trim( $this->addIdentifierQuotes( $oldName ), '"' ), '/' ) .
908 '"?(?=\W)/',
909 $this->addIdentifierQuotes( $newName ),
910 $sql,
911 1
912 );
913 if ( $temporary ) {
914 if ( preg_match( '/^\\s*CREATE\\s+VIRTUAL\\s+TABLE\b/i', $sql ) ) {
915 $this->queryLogger->debug(
916 "Table $oldName is virtual, can't create a temporary duplicate." );
917 } else {
918 $sql = str_replace( 'CREATE TABLE', 'CREATE TEMPORARY TABLE', $sql );
919 }
920 }
921
922 // @phan-suppress-next-line SecurityCheck-SQLInjection SQL is taken from database
923 $res = $this->query( $sql, $fname, $queryFlags );
924
925 // Take over indexes
926 $indexList = $this->query(
927 'PRAGMA INDEX_LIST(' . $this->addQuotes( $oldName ) . ')',
928 $fname,
929 $queryFlags
930 );
931 foreach ( $indexList as $index ) {
932 if ( strpos( $index->name, 'sqlite_autoindex' ) === 0 ) {
933 continue;
934 }
935
936 if ( $index->unique ) {
937 $sqlIndex = 'CREATE UNIQUE INDEX';
938 } else {
939 $sqlIndex = 'CREATE INDEX';
940 }
941 // Try to come up with a new index name, given indexes have database scope in SQLite
942 $indexName = $newName . '_' . $index->name;
943 $sqlIndex .= ' ' . $this->addIdentifierQuotes( $indexName ) .
944 ' ON ' . $this->addIdentifierQuotes( $newName );
945
946 $indexInfo = $this->query(
947 'PRAGMA INDEX_INFO(' . $this->addQuotes( $index->name ) . ')',
948 $fname,
949 $queryFlags
950 );
951 $fields = [];
952 foreach ( $indexInfo as $indexInfoRow ) {
953 $fields[$indexInfoRow->seqno] = $this->addQuotes( $indexInfoRow->name );
954 }
955
956 $sqlIndex .= '(' . implode( ',', $fields ) . ')';
957
958 $this->query( $sqlIndex, __METHOD__ );
959 }
960
961 return $res;
962 }
963
972 public function listTables( $prefix = null, $fname = __METHOD__ ) {
973 $result = $this->query(
974 "SELECT name FROM sqlite_master WHERE type = 'table'",
975 $fname,
976 self::QUERY_IGNORE_DBO_TRX
977 );
978
979 $endArray = [];
980
981 foreach ( $result as $table ) {
982 $vars = get_object_vars( $table );
983 $table = array_pop( $vars );
984
985 if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
986 if ( strpos( $table, 'sqlite_' ) !== 0 ) {
987 $endArray[] = $table;
988 }
989 }
990 }
991
992 return $endArray;
993 }
994
995 public function dropTable( $table, $fname = __METHOD__ ) {
996 if ( !$this->tableExists( $table, $fname ) ) {
997 return false;
998 }
999
1000 // No CASCADE support; https://www.sqlite.org/lang_droptable.html
1001 $sql = "DROP TABLE " . $this->tableName( $table );
1002 $this->query( $sql, $fname, self::QUERY_IGNORE_DBO_TRX );
1003
1004 return true;
1005 }
1006
1007 protected function doTruncate( array $tables, $fname ) {
1008 $this->startAtomic( $fname );
1009
1010 $encSeqNames = [];
1011 foreach ( $tables as $table ) {
1012 // Use "truncate" optimization; https://www.sqlite.org/lang_delete.html
1013 $sql = "DELETE FROM " . $this->tableName( $table );
1014 $this->query( $sql, $fname, self::QUERY_CHANGE_SCHEMA );
1015
1016 $encSeqNames[] = $this->addQuotes( $this->tableName( $table, 'raw' ) );
1017 }
1018
1019 $encMasterTable = $this->addIdentifierQuotes( 'sqlite_sequence' );
1020 $this->query(
1021 "DELETE FROM $encMasterTable WHERE name IN(" . implode( ',', $encSeqNames ) . ")",
1022 $fname,
1023 self::QUERY_CHANGE_SCHEMA
1024 );
1025
1026 $this->endAtomic( $fname );
1027 }
1028
1029 public function setTableAliases( array $aliases ) {
1030 parent::setTableAliases( $aliases );
1031 if ( $this->isOpen() ) {
1033 }
1034 }
1035
1040 foreach ( $this->tableAliases as $params ) {
1041 if (
1042 $params['dbname'] !== $this->getDBname() &&
1043 !isset( $this->sessionAttachedDbs[$params['dbname']] )
1044 ) {
1045 $this->attachDatabase( $params['dbname'], false, __METHOD__ );
1046 $this->sessionAttachedDbs[$params['dbname']] = true;
1047 }
1048 }
1049 }
1050
1051 public function databasesAreIndependent() {
1052 return true;
1053 }
1054
1055 protected function doHandleSessionLossPreconnect() {
1056 $this->sessionAttachedDbs = [];
1057 }
1058
1062 protected function getBindingHandle() {
1063 return parent::getBindingHandle();
1064 }
1065}
1066
1070class_alias( DatabaseSqlite::class, 'DatabaseSqlite' );
Simple version of LockManager based on using FS lock files.
Class for handling resource locking.
Simple version of LockManager that only does lock reference counting.
Database error base class @newable.
Definition DBError.php:32
Base class for the more common types of database errors.
Class to handle database/schema/prefix specifications for IDatabase.
attachDatabasesFromTableAliases()
Issue ATTATCH statements for all unattached foreign DBs in table aliases.
fieldInfo( $table, $field)
Get information about a given field Returns false if the field does not exist.
serverIsReadOnly()
bool Whether the DB is marked as read-only server-side query} 1.28to override
databasesAreIndependent()
Returns true if DBs are assumed to be on potentially different servers.In systems like mysql/mariadb,...
indexUnique( $table, $index, $fname=__METHOD__)
isWriteQuery( $sql, $flags)
Determine whether a query writes to the DB.
doLock(string $lockName, string $method, int $timeout)
buildGreatest( $fields, $values)
Build a GREATEST function statement comparing columns/values.Integer and float values in $values will...
__construct(array $params)
Additional params include:
array $sessionAttachedDbs
List of shared database already attached to this connection.
buildLeast( $fields, $values)
Build a LEAST function statement comparing columns/values.Integer and float values in $values will no...
string null $dbPath
Explicit path for the SQLite database file.
textFieldSize( $table, $field)
Returns the size of a text field, or -1 for "unlimited" In SQLite this is SQLITE_MAX_LENGTH,...
string $trxMode
Transaction mode.
attachDatabase( $name, $file=false, $fname=__METHOD__)
Attaches external database to the connection handle.
makeSelectOptions(array $options)
Returns an optional USE INDEX clause to go after the table, and a string to go at the end of the quer...
tableName( $name, $format='quoted')
Use MySQL's naming (accounts for prefix etc) but remove surrounding backticks.
wasConnectionError( $errno)
Do not use this method outside of Database/DBError classes.
tableExists( $table, $fname=__METHOD__)
Query whether a given table exists.
doSelectDomain(DatabaseDomain $domain)
buildConcat( $stringList)
Build a concatenation list to feed into a SQL query.
dropTable( $table, $fname=__METHOD__)
Delete a table.
isTransactableQuery( $sql)
Determine whether a SQL statement is sensitive to isolation level.
doLockIsFree(string $lockName, string $method)
doHandleSessionLossPreconnect()
Reset any additional subclass trx* and session* fields.
static getFulltextSearchModule()
Returns version of currently supported SQLite fulltext search module or false if none present.
closeConnection()
Does not actually close the connection, just destroys the reference for GC to do its work.
indexInfo( $table, $index, $fname=__METHOD__)
Returns information about an index Returns false if the index does not exist.
int $lastAffectedRowCount
The number of rows affected as an integer.
doBegin( $fname='')
Issues the BEGIN command to the database server.
setTableAliases(array $aliases)
Make certain table names use their own database, schema, and table prefix when passed into SQL querie...
FSLockManager $lockMgr
(hopefully on the same server as the DB)
static string[] $VALID_TRX_MODES
See https://www.sqlite.org/lang_transaction.html.
static newStandaloneInstance( $filename, array $p=[])
string null $dbDir
Directory for SQLite database files listed under their DB name.
doTruncate(array $tables, $fname)
open( $server, $user, $password, $db, $schema, $tablePrefix)
Open a new connection to the database (closing any existing one)
listTables( $prefix=null, $fname=__METHOD__)
List all tables on the database.
getTopologyBasedServerId()
Get a non-recycled ID that uniquely identifies this server within the replication topology.
buildGroupConcatField( $delim, $table, $field, $conds='', $join_conds=[])
Build a GROUP_CONCAT or equivalent statement for a query.This is useful for combining a field for sev...
deadlockLoop(... $args)
No-op version of deadlockLoop.
doUnlock(string $lockName, string $method)
static generateFileName( $dir, $dbName)
Generates a database file name.
doReplace( $table, array $identityKey, array $rows, $fname)
duplicateTableStructure( $oldName, $newName, $temporary=false, $fname=__METHOD__)
insertId()
This must be called after nextSequenceVal.
buildSubstring( $input, $startPosition, $length=null)
to override
Relational database abstraction object.
Definition Database.php:52
string null $password
Password used to establish the current connection.
Definition Database.php:87
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
Wrapper to IDatabase::select() that only fetches one row (via LIMIT)
newExceptionAfterConnectError( $error)
endAtomic( $fname=__METHOD__)
Ends an atomic section of SQL statements.
indexName( $index)
Allows for index remapping in queries where this is not consistent across DBMS.
getDomainID()
Return the currently selected domain ID.
Definition Database.php:868
buildSuperlative( $sqlfunc, $fields, $values)
Build a superlative function statement comparing columns/values.
int $flags
Current bit field of class DBO_* constants.
Definition Database.php:106
assertHasConnectionHandle()
Make sure there is an open connection handle (alive or not) as a sanity check.
query( $sql, $fname=__METHOD__, $flags=self::QUERY_NORMAL)
Run an SQL query and return the result.
close( $fname=__METHOD__, $owner=null)
Close the database connection.
Definition Database.php:990
startAtomic( $fname=__METHOD__, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Begin an atomic section of SQL statements.
addIdentifierQuotes( $s)
Escape a SQL identifier (e.g.table, column, database) for use in a SQL queryDepending on the database...
getLogContext(array $extras=[])
Create a log context to pass to PSR-3 logger functions.
Definition Database.php:979
assertBuildSubstringParams( $startPosition, $length)
Check type and bounds for parameters to self::buildSubstring()
string null $server
Server that this instance is currently connected to.
Definition Database.php:83
selectSQLText( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Take the same arguments as IDatabase::select() and return the SQL it would use.This can be useful for...
static factory( $type, $params=[], $connect=self::NEW_CONNECTED)
Construct a Database subclass instance given a database type and parameters.
Definition Database.php:436
getFlag( $flag)
Returns a boolean whether the flag $flag is set for this connection.
Definition Database.php:864
string null $user
User that this instance is currently connected under the name of.
Definition Database.php:85
makeInsertLists(array $rows)
Make SQL lists of columns, row tuples for INSERT/VALUES expressions.
getDBname()
Get the current database name; null if there isn't one.
fetchObject(IResultWrapper $res)
Fetch the next row from the given result object, in object form.
Definition Database.php:872
if( $line===false) $args
Definition mcc.php:124
foreach( $mmfl['setupFiles'] as $fileName) if($queue) if(empty( $mmfl['quiet'])) $s
return true
Definition router.php:92
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42