MediaWiki REL1_35
DatabaseSqlite.php
Go to the documentation of this file.
1<?php
24namespace Wikimedia\Rdbms;
25
27use LockManager;
29use PDO;
30use PDOException;
31use RuntimeException;
32use stdClass;
33
37class DatabaseSqlite extends Database {
39 protected $dbDir;
41 protected $dbPath;
43 protected $trxMode;
44
47
49 protected $conn;
50
52 protected $lockMgr;
53
55 private $version;
56
58 private $sessionAttachedDbs = [];
59
61 private static $VALID_TRX_MODES = [ '', 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ];
62
64 private static $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 ];
70
78 public function __construct( array $params ) {
79 if ( isset( $params['dbFilePath'] ) ) {
80 $this->dbPath = $params['dbFilePath'];
81 if ( !isset( $params['dbname'] ) || $params['dbname'] === '' ) {
82 $params['dbname'] = self::generateDatabaseName( $this->dbPath );
83 }
84 } elseif ( isset( $params['dbDirectory'] ) ) {
85 $this->dbDir = $params['dbDirectory'];
86 }
87
88 parent::__construct( $params );
89
90 $this->trxMode = strtoupper( $params['trxMode'] ?? '' );
91
92 $lockDirectory = $this->getLockFileDirectory();
93 if ( $lockDirectory !== null ) {
94 $this->lockMgr = new FSLockManager( [
95 'domain' => $this->getDomainID(),
96 'lockDirectory' => $lockDirectory
97 ] );
98 } else {
99 $this->lockMgr = new NullLockManager( [ 'domain' => $this->getDomainID() ] );
100 }
101 }
102
103 protected static function getAttributes() {
104 return [
105 self::ATTR_DB_IS_FILE => true,
106 self::ATTR_DB_LEVEL_LOCKING => true
107 ];
108 }
109
119 public static function newStandaloneInstance( $filename, array $p = [] ) {
120 $p['dbFilePath'] = $filename;
121 $p['schema'] = null;
122 $p['tablePrefix'] = '';
124 $db = Database::factory( 'sqlite', $p );
125
126 return $db;
127 }
128
132 public function getType() {
133 return 'sqlite';
134 }
135
136 protected function open( $server, $user, $pass, $dbName, $schema, $tablePrefix ) {
137 $this->close( __METHOD__ );
138
139 // Note that for SQLite, $server, $user, and $pass are ignored
140
141 if ( $schema !== null ) {
142 throw $this->newExceptionAfterConnectError( "Got schema '$schema'; not supported." );
143 }
144
145 if ( $this->dbPath !== null ) {
147 } elseif ( $this->dbDir !== null ) {
148 $path = self::generateFileName( $this->dbDir, $dbName );
149 } else {
150 throw $this->newExceptionAfterConnectError( "DB path or directory required" );
151 }
152
153 // Check if the database file already exists but is non-readable
154 if (
155 !self::isProcessMemoryPath( $path ) &&
156 file_exists( $path ) &&
157 !is_readable( $path )
158 ) {
159 throw $this->newExceptionAfterConnectError( 'SQLite database file is not readable' );
160 } elseif ( !in_array( $this->trxMode, self::$VALID_TRX_MODES, true ) ) {
161 throw $this->newExceptionAfterConnectError( "Got mode '{$this->trxMode}' for BEGIN" );
162 }
163
164 $this->server = 'localhost';
165
166 $attributes = [];
167 if ( $this->getFlag( self::DBO_PERSISTENT ) ) {
168 // Persistent connections can avoid some schema index reading overhead.
169 // On the other hand, they can cause horrible contention with DBO_TRX.
170 if ( $this->getFlag( self::DBO_TRX ) || $this->getFlag( self::DBO_DEFAULT ) ) {
171 $this->connLogger->warning(
172 __METHOD__ . ": ignoring DBO_PERSISTENT due to DBO_TRX or DBO_DEFAULT",
173 $this->getLogContext()
174 );
175 } else {
176 $attributes[PDO::ATTR_PERSISTENT] = true;
177 }
178 }
179
180 try {
181 // Open the database file, creating it if it does not yet exist
182 $this->conn = new PDO( "sqlite:$path", null, null, $attributes );
183 } catch ( PDOException $e ) {
184 throw $this->newExceptionAfterConnectError( $e->getMessage() );
185 }
186
187 $this->currentDomain = new DatabaseDomain( $dbName, null, $tablePrefix );
188
189 try {
190 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_NO_RETRY;
191 // Enforce LIKE to be case sensitive, just like MySQL
192 $this->query( 'PRAGMA case_sensitive_like = 1', __METHOD__, $flags );
193 // Set any connection-level custom PRAGMA options
194 $pragmas = array_intersect_key( $this->connectionVariables, self::$VALID_PRAGMAS );
195 $pragmas += $this->getDefaultPragmas();
196 foreach ( $pragmas as $name => $value ) {
197 $allowed = self::$VALID_PRAGMAS[$name];
198 if ( in_array( $value, $allowed, true ) ) {
199 $this->query( "PRAGMA $name = $value", __METHOD__, $flags );
200 }
201 }
203 } catch ( RuntimeException $e ) {
204 throw $this->newExceptionAfterConnectError( $e->getMessage() );
205 }
206 }
207
211 private function getDefaultPragmas() {
212 $variables = [];
213
214 if ( !$this->cliMode ) {
215 $variables['temp_store'] = 'MEMORY';
216 }
217
218 return $variables;
219 }
220
226 public function getDbFilePath() {
227 return $this->dbPath ?? self::generateFileName( $this->dbDir, $this->getDBname() );
228 }
229
233 public function getLockFileDirectory() {
234 if ( $this->dbPath !== null && !self::isProcessMemoryPath( $this->dbPath ) ) {
235 return dirname( $this->dbPath ) . '/locks';
236 } elseif ( $this->dbDir !== null && !self::isProcessMemoryPath( $this->dbDir ) ) {
237 return $this->dbDir . '/locks';
238 }
239
240 return null;
241 }
242
247 protected function closeConnection() {
248 $this->conn = null;
249
250 return true;
251 }
252
260 public static function generateFileName( $dir, $dbName ) {
261 if ( $dir == '' ) {
262 throw new DBUnexpectedError( null, __CLASS__ . ": no DB directory specified" );
263 } elseif ( self::isProcessMemoryPath( $dir ) ) {
264 throw new DBUnexpectedError(
265 null,
266 __CLASS__ . ": cannot use process memory directory '$dir'"
267 );
268 } elseif ( !strlen( $dbName ) ) {
269 throw new DBUnexpectedError( null, __CLASS__ . ": no DB name specified" );
270 }
271
272 return "$dir/$dbName.sqlite";
273 }
274
279 private static function generateDatabaseName( $path ) {
280 if ( preg_match( '/^(:memory:$|file::memory:)/', $path ) ) {
281 // E.g. "file::memory:?cache=shared" => ":memory":
282 return ':memory:';
283 } elseif ( preg_match( '/^file::([^?]+)\?mode=memory(&|$)/', $path, $m ) ) {
284 // E.g. "file:memdb1?mode=memory" => ":memdb1:"
285 return ":{$m[1]}:";
286 } else {
287 // E.g. "/home/.../some_db.sqlite3" => "some_db"
288 return preg_replace( '/\.sqlite\d?$/', '', basename( $path ) );
289 }
290 }
291
296 private static function isProcessMemoryPath( $path ) {
297 return preg_match( '/^(:memory:$|file:(:memory:|[^?]+\?mode=memory(&|$)))/', $path );
298 }
299
304 public static function getFulltextSearchModule() {
305 static $cachedResult = null;
306 if ( $cachedResult !== null ) {
307 return $cachedResult;
308 }
309 $cachedResult = false;
310 $table = 'dummy_search_test';
311
312 $db = self::newStandaloneInstance( ':memory:' );
313 if ( $db->query(
314 "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)",
315 __METHOD__,
316 IDatabase::QUERY_SILENCE_ERRORS
317 ) ) {
318 $cachedResult = 'FTS3';
319 }
320 $db->close( __METHOD__ );
321
322 return $cachedResult;
323 }
324
337 public function attachDatabase( $name, $file = false, $fname = __METHOD__ ) {
338 $file = is_string( $file ) ? $file : self::generateFileName( $this->dbDir, $name );
339 $encFile = $this->addQuotes( $file );
340
341 return $this->query(
342 "ATTACH DATABASE $encFile AS $name",
343 $fname,
344 self::QUERY_IGNORE_DBO_TRX
345 );
346 }
347
348 protected function isWriteQuery( $sql, $flags ) {
349 return parent::isWriteQuery( $sql, $flags ) && !preg_match( '/^(ATTACH|PRAGMA)\b/i', $sql );
350 }
351
352 protected function isTransactableQuery( $sql ) {
353 return parent::isTransactableQuery( $sql ) && !in_array(
354 $this->getQueryVerb( $sql ),
355 [ 'ATTACH', 'PRAGMA' ],
356 true
357 );
358 }
359
366 protected function doQuery( $sql ) {
367 $res = $this->getBindingHandle()->query( $sql );
368 if ( $res === false ) {
369 return false;
370 }
371
372 $resource = ResultWrapper::unwrap( $res );
373 $this->lastAffectedRowCount = $resource->rowCount();
374 $res = new ResultWrapper( $this, $resource->fetchAll() );
375
376 return $res;
377 }
378
382 public function freeResult( $res ) {
383 if ( $res instanceof ResultWrapper ) {
384 $res->free();
385 }
386 }
387
392 public function fetchObject( $res ) {
393 $resource =& ResultWrapper::unwrap( $res );
394
395 $cur = current( $resource );
396 if ( is_array( $cur ) ) {
397 next( $resource );
398 $obj = (object)[];
399 foreach ( $cur as $k => $v ) {
400 if ( !is_numeric( $k ) ) {
401 $obj->$k = $v;
402 }
403 }
404
405 return $obj;
406 }
407
408 return false;
409 }
410
415 public function fetchRow( $res ) {
416 $resource =& ResultWrapper::unwrap( $res );
417 $cur = current( $resource );
418 if ( is_array( $cur ) ) {
419 next( $resource );
420
421 return $cur;
422 }
423
424 return false;
425 }
426
433 public function numRows( $res ) {
434 // false does not implement Countable
435 $resource = ResultWrapper::unwrap( $res );
436
437 return is_array( $resource ) ? count( $resource ) : 0;
438 }
439
444 public function numFields( $res ) {
445 $resource = ResultWrapper::unwrap( $res );
446 if ( is_array( $resource ) && count( $resource ) > 0 ) {
447 // The size of the result array is twice the number of fields. (T67578)
448 return count( $resource[0] ) / 2;
449 } else {
450 // If the result is empty return 0
451 return 0;
452 }
453 }
454
460 public function fieldName( $res, $n ) {
461 $resource = ResultWrapper::unwrap( $res );
462 if ( is_array( $resource ) ) {
463 $keys = array_keys( $resource[0] );
464
465 return $keys[$n];
466 }
467
468 return false;
469 }
470
471 protected function doSelectDomain( DatabaseDomain $domain ) {
472 if ( $domain->getSchema() !== null ) {
473 throw new DBExpectedError(
474 $this,
475 __CLASS__ . ": domain '{$domain->getId()}' has a schema component"
476 );
477 }
478
479 $database = $domain->getDatabase();
480 // A null database means "don't care" so leave it as is and update the table prefix
481 if ( $database === null ) {
482 $this->currentDomain = new DatabaseDomain(
483 $this->currentDomain->getDatabase(),
484 null,
485 $domain->getTablePrefix()
486 );
487
488 return true;
489 }
490
491 if ( $database !== $this->getDBname() ) {
492 throw new DBExpectedError(
493 $this,
494 __CLASS__ . ": cannot change database (got '$database')"
495 );
496 }
497
498 return true;
499 }
500
508 public function tableName( $name, $format = 'quoted' ) {
509 // table names starting with sqlite_ are reserved
510 if ( strpos( $name, 'sqlite_' ) === 0 ) {
511 return $name;
512 }
513
514 return str_replace( '"', '', parent::tableName( $name, $format ) );
515 }
516
522 public function insertId() {
523 // PDO::lastInsertId yields a string :(
524 return intval( $this->getBindingHandle()->lastInsertId() );
525 }
526
531 public function dataSeek( $res, $row ) {
532 $resource =& ResultWrapper::unwrap( $res );
533 reset( $resource );
534 if ( $row > 0 ) {
535 for ( $i = 0; $i < $row; $i++ ) {
536 next( $resource );
537 }
538 }
539 }
540
544 public function lastError() {
545 if ( !is_object( $this->conn ) ) {
546 return "Cannot return last error, no db connection";
547 }
548 $e = $this->conn->errorInfo();
549
550 return $e[2] ?? '';
551 }
552
556 public function lastErrno() {
557 if ( !is_object( $this->conn ) ) {
558 return "Cannot return last error, no db connection";
559 } else {
560 $info = $this->conn->errorInfo();
561
562 return $info[1];
563 }
564 }
565
569 protected function fetchAffectedRowCount() {
571 }
572
573 public function tableExists( $table, $fname = __METHOD__ ) {
574 $tableRaw = $this->tableName( $table, 'raw' );
575 if ( isset( $this->sessionTempTables[$tableRaw] ) ) {
576 return true; // already known to exist
577 }
578
579 $encTable = $this->addQuotes( $tableRaw );
580 $res = $this->query(
581 "SELECT 1 FROM sqlite_master WHERE type='table' AND name=$encTable",
582 __METHOD__,
583 self::QUERY_IGNORE_DBO_TRX
584 );
585
586 return $res->numRows() ? true : false;
587 }
588
599 public function indexInfo( $table, $index, $fname = __METHOD__ ) {
600 $sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')';
601 $res = $this->query( $sql, $fname, self::QUERY_IGNORE_DBO_TRX );
602 if ( !$res || $res->numRows() == 0 ) {
603 return false;
604 }
605 $info = [];
606 foreach ( $res as $row ) {
607 $info[] = $row->name;
608 }
609
610 return $info;
611 }
612
619 public function indexUnique( $table, $index, $fname = __METHOD__ ) {
620 $row = $this->selectRow( 'sqlite_master', '*',
621 [
622 'type' => 'index',
623 'name' => $this->indexName( $index ),
624 ], $fname );
625 if ( !$row || !isset( $row->sql ) ) {
626 return null;
627 }
628
629 // $row->sql will be of the form CREATE [UNIQUE] INDEX ...
630 $indexPos = strpos( $row->sql, 'INDEX' );
631 if ( $indexPos === false ) {
632 return null;
633 }
634 $firstPart = substr( $row->sql, 0, $indexPos );
635 $options = explode( ' ', $firstPart );
636
637 return in_array( 'UNIQUE', $options );
638 }
639
640 protected function makeSelectOptions( array $options ) {
641 // Remove problematic options that the base implementation converts to SQL
642 foreach ( $options as $k => $v ) {
643 if ( is_numeric( $k ) && ( $v === 'FOR UPDATE' || $v === 'LOCK IN SHARE MODE' ) ) {
644 $options[$k] = '';
645 }
646 }
647
648 return parent::makeSelectOptions( $options );
649 }
650
655 protected function makeUpdateOptionsArray( $options ) {
656 $options = parent::makeUpdateOptionsArray( $options );
657 $options = $this->rewriteIgnoreKeyword( $options );
658
659 return $options;
660 }
661
666 private function rewriteIgnoreKeyword( $options ) {
667 # SQLite uses OR IGNORE not just IGNORE
668 foreach ( $options as $k => $v ) {
669 if ( $v == 'IGNORE' ) {
670 $options[$k] = 'OR IGNORE';
671 }
672 }
673
674 return $options;
675 }
676
678 return [ 'INSERT OR IGNORE INTO', '' ];
679 }
680
681 protected function doReplace( $table, array $uniqueKeys, array $rows, $fname ) {
682 $encTable = $this->tableName( $table );
683 list( $sqlColumns, $sqlTuples ) = $this->makeInsertLists( $rows );
684 // https://sqlite.org/lang_insert.html
685 $this->query( "REPLACE INTO $encTable ($sqlColumns) VALUES $sqlTuples", $fname );
686 }
687
696 public function textFieldSize( $table, $field ) {
697 return -1;
698 }
699
703 public function unionSupportsOrderAndLimit() {
704 return false;
705 }
706
712 public function unionQueries( $sqls, $all ) {
713 $glue = $all ? ' UNION ALL ' : ' UNION ';
714
715 return implode( $glue, $sqls );
716 }
717
721 public function wasDeadlock() {
722 return $this->lastErrno() == 5; // SQLITE_BUSY
723 }
724
728 public function wasReadOnlyError() {
729 return $this->lastErrno() == 8; // SQLITE_READONLY;
730 }
731
732 public function wasConnectionError( $errno ) {
733 return $errno == 17; // SQLITE_SCHEMA;
734 }
735
736 protected function wasKnownStatementRollbackError() {
737 // ON CONFLICT ROLLBACK clauses make it so that SQLITE_CONSTRAINT error is
738 // ambiguous with regard to whether it implies a ROLLBACK or an ABORT happened.
739 // https://sqlite.org/lang_createtable.html#uniqueconst
740 // https://sqlite.org/lang_conflict.html
741 return false;
742 }
743
744 public function serverIsReadOnly() {
746
747 $path = $this->getDbFilePath();
748
749 return ( !self::isProcessMemoryPath( $path ) && !is_writable( $path ) );
750 }
751
755 public function getSoftwareLink() {
756 return "[{{int:version-db-sqlite-url}} SQLite]";
757 }
758
762 public function getServerVersion() {
763 if ( $this->version === null ) {
764 $this->version = $this->getBindingHandle()->getAttribute( PDO::ATTR_SERVER_VERSION );
765 }
766
767 return $this->version;
768 }
769
778 public function fieldInfo( $table, $field ) {
779 $tableName = $this->tableName( $table );
780 $sql = 'PRAGMA table_info(' . $this->addQuotes( $tableName ) . ')';
781 $res = $this->query( $sql, __METHOD__, self::QUERY_IGNORE_DBO_TRX );
782 foreach ( $res as $row ) {
783 if ( $row->name == $field ) {
784 return new SQLiteField( $row, $tableName );
785 }
786 }
787
788 return false;
789 }
790
791 protected function doBegin( $fname = '' ) {
792 if ( $this->trxMode != '' ) {
793 $this->query( "BEGIN {$this->trxMode}", $fname );
794 } else {
795 $this->query( 'BEGIN', $fname );
796 }
797 }
798
803 public function strencode( $s ) {
804 return substr( $this->addQuotes( $s ), 1, -1 );
805 }
806
811 public function encodeBlob( $b ) {
812 return new Blob( $b );
813 }
814
819 public function decodeBlob( $b ) {
820 if ( $b instanceof Blob ) {
821 $b = $b->fetch();
822 }
823
824 return $b;
825 }
826
831 public function addQuotes( $s ) {
832 if ( $s instanceof Blob ) {
833 return "x'" . bin2hex( $s->fetch() ) . "'";
834 } elseif ( is_bool( $s ) ) {
835 return (string)(int)$s;
836 } elseif ( is_int( $s ) ) {
837 return (string)$s;
838 } elseif ( strpos( (string)$s, "\0" ) !== false ) {
839 // SQLite doesn't support \0 in strings, so use the hex representation as a workaround.
840 // This is a known limitation of SQLite's mprintf function which PDO
841 // should work around, but doesn't. I have reported this to php.net as bug #63419:
842 // https://bugs.php.net/bug.php?id=63419
843 // There was already a similar report for SQLite3::escapeString, bug #62361:
844 // https://bugs.php.net/bug.php?id=62361
845 // There is an additional bug regarding sorting this data after insert
846 // on older versions of sqlite shipped with ubuntu 12.04
847 // https://phabricator.wikimedia.org/T74367
848 $this->queryLogger->debug(
849 __FUNCTION__ .
850 ': Quoting value containing null byte. ' .
851 'For consistency all binary data should have been ' .
852 'first processed with self::encodeBlob()'
853 );
854 return "x'" . bin2hex( (string)$s ) . "'";
855 } else {
856 return $this->getBindingHandle()->quote( (string)$s );
857 }
858 }
859
860 public function buildSubstring( $input, $startPosition, $length = null ) {
861 $this->assertBuildSubstringParams( $startPosition, $length );
862 $params = [ $input, $startPosition ];
863 if ( $length !== null ) {
864 $params[] = $length;
865 }
866 return 'SUBSTR(' . implode( ',', $params ) . ')';
867 }
868
874 public function buildStringCast( $field ) {
875 return 'CAST ( ' . $field . ' AS TEXT )';
876 }
877
884 public function deadlockLoop( ...$args ) {
885 $function = array_shift( $args );
886
887 return $function( ...$args );
888 }
889
894 protected function replaceVars( $s ) {
895 $s = parent::replaceVars( $s );
896 if ( preg_match( '/^\s*(CREATE|ALTER) TABLE/i', $s ) ) {
897 // CREATE TABLE hacks to allow schema file sharing with MySQL
898
899 // binary/varbinary column type -> blob
900 $s = preg_replace( '/\b(var)?binary(\‍(\d+\‍))/i', 'BLOB', $s );
901 // no such thing as unsigned
902 $s = preg_replace( '/\b(un)?signed\b/i', '', $s );
903 // INT -> INTEGER
904 $s = preg_replace( '/\b(tiny|small|medium|big|)int(\s*\‍(\s*\d+\s*\‍)|\b)/i', 'INTEGER', $s );
905 // floating point types -> REAL
906 $s = preg_replace(
907 '/\b(float|double(\s+precision)?)(\s*\‍(\s*\d+\s*(,\s*\d+\s*)?\‍)|\b)/i',
908 'REAL',
909 $s
910 );
911 // varchar -> TEXT
912 $s = preg_replace( '/\b(var)?char\s*\‍(.*?\‍)/i', 'TEXT', $s );
913 // TEXT normalization
914 $s = preg_replace( '/\b(tiny|medium|long)text\b/i', 'TEXT', $s );
915 // BLOB normalization
916 $s = preg_replace( '/\b(tiny|small|medium|long|)blob\b/i', 'BLOB', $s );
917 // BOOL -> INTEGER
918 $s = preg_replace( '/\bbool(ean)?\b/i', 'INTEGER', $s );
919 // DATETIME -> TEXT
920 $s = preg_replace( '/\b(datetime|timestamp)\b/i', 'TEXT', $s );
921 // No ENUM type
922 $s = preg_replace( '/\benum\s*\‍([^)]*\‍)/i', 'TEXT', $s );
923 // binary collation type -> nothing
924 $s = preg_replace( '/\bbinary\b/i', '', $s );
925 // auto_increment -> autoincrement
926 $s = preg_replace( '/\bauto_increment\b/i', 'AUTOINCREMENT', $s );
927 // No explicit options
928 $s = preg_replace( '/\‍)[^);]*(;?)\s*$/', ')\1', $s );
929 // AUTOINCREMENT should immedidately follow PRIMARY KEY
930 $s = preg_replace( '/primary key (.*?) autoincrement/i', 'PRIMARY KEY AUTOINCREMENT $1', $s );
931 } elseif ( preg_match( '/^\s*CREATE (\s*(?:UNIQUE|FULLTEXT)\s+)?INDEX/i', $s ) ) {
932 // No truncated indexes
933 $s = preg_replace( '/\‍(\d+\‍)/', '', $s );
934 // No FULLTEXT
935 $s = preg_replace( '/\bfulltext\b/i', '', $s );
936 } elseif ( preg_match( '/^\s*DROP INDEX/i', $s ) ) {
937 // DROP INDEX is database-wide, not table-specific, so no ON <table> clause.
938 $s = preg_replace( '/\sON\s+[^\s]*/i', '', $s );
939 } elseif ( preg_match( '/^\s*INSERT IGNORE\b/i', $s ) ) {
940 // INSERT IGNORE --> INSERT OR IGNORE
941 $s = preg_replace( '/^\s*INSERT IGNORE\b/i', 'INSERT OR IGNORE', $s );
942 }
943
944 return $s;
945 }
946
947 public function lock( $lockName, $method, $timeout = 5 ) {
948 $status = $this->lockMgr->lock( [ $lockName ], LockManager::LOCK_EX, $timeout );
949 if (
950 $this->lockMgr instanceof FSLockManager &&
951 $status->hasMessage( 'lockmanager-fail-openlock' )
952 ) {
953 throw new DBError( $this, "Cannot create directory \"{$this->getLockFileDirectory()}\"" );
954 }
955
956 return $status->isOK();
957 }
958
959 public function unlock( $lockName, $method ) {
960 return $this->lockMgr->unlock( [ $lockName ], LockManager::LOCK_EX )->isGood();
961 }
962
969 public function buildConcat( $stringList ) {
970 return '(' . implode( ') || (', $stringList ) . ')';
971 }
972
973 public function buildGroupConcatField(
974 $delim, $table, $field, $conds = '', $join_conds = []
975 ) {
976 $fld = "group_concat($field," . $this->addQuotes( $delim ) . ')';
977
978 return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
979 }
980
981 public function buildGreatest( $fields, $values ) {
982 return $this->buildSuperlative( 'MAX', $fields, $values );
983 }
984
985 public function buildLeast( $fields, $values ) {
986 return $this->buildSuperlative( 'MIN', $fields, $values );
987 }
988
997 public function duplicateTableStructure(
998 $oldName, $newName, $temporary = false, $fname = __METHOD__
999 ) {
1000 $queryFlags = self::QUERY_PSEUDO_PERMANENT | self::QUERY_IGNORE_DBO_TRX;
1001
1002 $res = $this->query(
1003 "SELECT sql FROM sqlite_master WHERE tbl_name=" .
1004 $this->addQuotes( $oldName ) . " AND type='table'",
1005 $fname,
1006 $queryFlags
1007 );
1008 $obj = $this->fetchObject( $res );
1009 if ( !$obj ) {
1010 throw new RuntimeException( "Couldn't retrieve structure for table $oldName" );
1011 }
1012 $sql = $obj->sql;
1013 $sql = preg_replace(
1014 '/(?<=\W)"?' .
1015 preg_quote( trim( $this->addIdentifierQuotes( $oldName ), '"' ), '/' ) .
1016 '"?(?=\W)/',
1017 $this->addIdentifierQuotes( $newName ),
1018 $sql,
1019 1
1020 );
1021 if ( $temporary ) {
1022 if ( preg_match( '/^\\s*CREATE\\s+VIRTUAL\\s+TABLE\b/i', $sql ) ) {
1023 $this->queryLogger->debug(
1024 "Table $oldName is virtual, can't create a temporary duplicate." );
1025 } else {
1026 $sql = str_replace( 'CREATE TABLE', 'CREATE TEMPORARY TABLE', $sql );
1027 }
1028 }
1029
1030 $res = $this->query( $sql, $fname, $queryFlags );
1031
1032 // Take over indexes
1033 $indexList = $this->query(
1034 'PRAGMA INDEX_LIST(' . $this->addQuotes( $oldName ) . ')',
1035 $fname,
1036 $queryFlags
1037 );
1038 foreach ( $indexList as $index ) {
1039 if ( strpos( $index->name, 'sqlite_autoindex' ) === 0 ) {
1040 continue;
1041 }
1042
1043 if ( $index->unique ) {
1044 $sql = 'CREATE UNIQUE INDEX';
1045 } else {
1046 $sql = 'CREATE INDEX';
1047 }
1048 // Try to come up with a new index name, given indexes have database scope in SQLite
1049 $indexName = $newName . '_' . $index->name;
1050 $sql .= ' ' . $indexName . ' ON ' . $newName;
1051
1052 $indexInfo = $this->query(
1053 'PRAGMA INDEX_INFO(' . $this->addQuotes( $index->name ) . ')',
1054 $fname,
1055 $queryFlags
1056 );
1057 $fields = [];
1058 foreach ( $indexInfo as $indexInfoRow ) {
1059 $fields[$indexInfoRow->seqno] = $indexInfoRow->name;
1060 }
1061
1062 $sql .= '(' . implode( ',', $fields ) . ')';
1063
1064 $this->query( $sql, __METHOD__ );
1065 }
1066
1067 return $res;
1068 }
1069
1078 public function listTables( $prefix = null, $fname = __METHOD__ ) {
1079 $result = $this->query(
1080 "SELECT name FROM sqlite_master WHERE type = 'table'",
1081 $fname,
1082 self::QUERY_IGNORE_DBO_TRX
1083 );
1084
1085 $endArray = [];
1086
1087 foreach ( $result as $table ) {
1088 $vars = get_object_vars( $table );
1089 $table = array_pop( $vars );
1090
1091 if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
1092 if ( strpos( $table, 'sqlite_' ) !== 0 ) {
1093 $endArray[] = $table;
1094 }
1095 }
1096 }
1097
1098 return $endArray;
1099 }
1100
1101 public function dropTable( $table, $fname = __METHOD__ ) {
1102 if ( !$this->tableExists( $table, $fname ) ) {
1103 return false;
1104 }
1105
1106 // No CASCADE support; https://www.sqlite.org/lang_droptable.html
1107 $sql = "DROP TABLE " . $this->tableName( $table );
1108 $this->query( $sql, $fname, self::QUERY_IGNORE_DBO_TRX );
1109
1110 return true;
1111 }
1112
1113 protected function doTruncate( array $tables, $fname ) {
1114 $this->startAtomic( $fname );
1115
1116 $encSeqNames = [];
1117 foreach ( $tables as $table ) {
1118 // Use "truncate" optimization; https://www.sqlite.org/lang_delete.html
1119 $sql = "DELETE FROM " . $this->tableName( $table );
1120 $this->query( $sql, $fname, self::QUERY_CHANGE_SCHEMA );
1121
1122 $encSeqNames[] = $this->addQuotes( $this->tableName( $table, 'raw' ) );
1123 }
1124
1125 $encMasterTable = $this->addIdentifierQuotes( 'sqlite_sequence' );
1126 $this->query(
1127 "DELETE FROM $encMasterTable WHERE name IN(" . implode( ',', $encSeqNames ) . ")",
1128 $fname,
1129 self::QUERY_CHANGE_SCHEMA
1130 );
1131
1132 $this->endAtomic( $fname );
1133 }
1134
1135 public function setTableAliases( array $aliases ) {
1136 parent::setTableAliases( $aliases );
1137 if ( $this->isOpen() ) {
1139 }
1140 }
1141
1146 foreach ( $this->tableAliases as $params ) {
1147 if (
1148 $params['dbname'] !== $this->getDBname() &&
1149 !isset( $this->sessionAttachedDbs[$params['dbname']] )
1150 ) {
1151 $this->attachDatabase( $params['dbname'], false, __METHOD__ );
1152 $this->sessionAttachedDbs[$params['dbname']] = true;
1153 }
1154 }
1155 }
1156
1157 public function databasesAreIndependent() {
1158 return true;
1159 }
1160
1161 protected function doHandleSessionLossPreconnect() {
1162 $this->sessionAttachedDbs = [];
1163 }
1164
1168 protected function getBindingHandle() {
1169 return parent::getBindingHandle();
1170 }
1171}
1172
1176class_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.
@newable Stable to extend
Definition Blob.php:9
Database error base class @newable Stable to extend.
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 1.28 Stable to override Stable to override
static getAttributes()
Stable to 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.
lock( $lockName, $method, $timeout=5)
Acquire a named lock.Named locks are not related to transactionsbool Success Stable to override Stab...
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.
unlock( $lockName, $method)
Release a lock.Named locks are not related to transactionsbool Success Stable to override Stable to ...
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...
numRows( $res)
The PDO::Statement class implements the array interface so count() will work.
doQuery( $sql)
SQLite doesn't allow buffered results or data seeking etc, so we'll use fetchAll as the result.
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)
Stable to override.
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.
doHandleSessionLossPreconnect()
Reset any additional subclass trx* and session* fields Stable to override.
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=[])
wasKnownStatementRollbackError()
Stable to override.
string null $dbDir
Directory for SQLite database files listed under their DB name.
doTruncate(array $tables, $fname)
makeInsertNonConflictingVerbAndOptions()
Stable to override.
open( $server, $user, $pass, $dbName, $schema, $tablePrefix)
Open a new connection to the database (closing any existing one)
listTables( $prefix=null, $fname=__METHOD__)
List all tables on the database.
doReplace( $table, array $uniqueKeys, array $rows, $fname)
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.
static generateFileName( $dir, $dbName)
Generates a database file name.
duplicateTableStructure( $oldName, $newName, $temporary=false, $fname=__METHOD__)
insertId()
This must be called after nextSequenceVal.
buildSubstring( $input, $startPosition, $length=null)
Stable to override Stable to override
Relational database abstraction object.
Definition Database.php:50
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:823
string $user
User that this instance is currently connected under the name of.
Definition Database.php:81
buildSuperlative( $sqlfunc, $fields, $values)
Build a superlative function statement comparing columns/values.
int $flags
Current bit field of class DBO_* constants.
Definition Database.php:100
assertHasConnectionHandle()
Make sure there is an open connection handle (alive or not) as a sanity check.
Definition Database.php:990
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:912
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:901
assertBuildSubstringParams( $startPosition, $length)
Check type and bounds for parameters to self::buildSubstring()
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...
string $server
Server that this instance is currently connected to.
Definition Database.php:79
static factory( $type, $params=[], $connect=self::NEW_CONNECTED)
Construct a Database subclass instance given a database type and parameters.
Definition Database.php:405
getFlag( $flag)
Returns a boolean whether the flag $flag is set for this connection.
Definition Database.php:819
makeInsertLists(array $rows)
Make SQL lists of columns, row tuples for INSERT/VALUES expressions.
getDBname()
Get the current DB name.
Result wrapper for grabbing data queried from an IDatabase object.
static & unwrap(&$res)
Get the underlying RDBMS driver-specific result resource.
if( $line===false) $args
Definition mcc.php:124
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