MediaWiki REL1_34
DatabaseSqlite.php
Go to the documentation of this file.
1<?php
24namespace Wikimedia\Rdbms;
25
27use PDO;
28use PDOException;
29use Exception;
30use LockManager;
32use RuntimeException;
33use stdClass;
34
38class DatabaseSqlite extends Database {
40 protected $dbDir;
42 protected $dbPath;
44 protected $trxMode;
45
50
52 protected $conn;
53
55 protected $lockMgr;
56
58 private $sessionAttachedDbs = [];
59
61 private static $VALID_TRX_MODES = [ '', 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ];
62
70 public function __construct( array $p ) {
71 if ( isset( $p['dbFilePath'] ) ) {
72 $this->dbPath = $p['dbFilePath'];
73 if ( !strlen( $p['dbname'] ) ) {
74 $p['dbname'] = self::generateDatabaseName( $this->dbPath );
75 }
76 } elseif ( isset( $p['dbDirectory'] ) ) {
77 $this->dbDir = $p['dbDirectory'];
78 }
79
80 parent::__construct( $p );
81
82 $this->trxMode = strtoupper( $p['trxMode'] ?? '' );
83
84 $lockDirectory = $this->getLockFileDirectory();
85 if ( $lockDirectory !== null ) {
86 $this->lockMgr = new FSLockManager( [
87 'domain' => $this->getDomainID(),
88 'lockDirectory' => $lockDirectory
89 ] );
90 } else {
91 $this->lockMgr = new NullLockManager( [ 'domain' => $this->getDomainID() ] );
92 }
93 }
94
95 protected static function getAttributes() {
96 return [ self::ATTR_DB_LEVEL_LOCKING => true ];
97 }
98
108 public static function newStandaloneInstance( $filename, array $p = [] ) {
109 $p['dbFilePath'] = $filename;
110 $p['schema'] = null;
111 $p['tablePrefix'] = '';
113 $db = Database::factory( 'sqlite', $p );
114
115 return $db;
116 }
117
121 public function getType() {
122 return 'sqlite';
123 }
124
125 protected function open( $server, $user, $pass, $dbName, $schema, $tablePrefix ) {
126 $this->close();
127
128 // Note that for SQLite, $server, $user, and $pass are ignored
129
130 if ( $schema !== null ) {
131 throw $this->newExceptionAfterConnectError( "Got schema '$schema'; not supported." );
132 }
133
134 if ( $this->dbPath !== null ) {
136 } elseif ( $this->dbDir !== null ) {
137 $path = self::generateFileName( $this->dbDir, $dbName );
138 } else {
139 throw $this->newExceptionAfterConnectError( "DB path or directory required" );
140 }
141
142 // Check if the database file already exists but is non-readable
143 if (
144 !self::isProcessMemoryPath( $path ) &&
145 file_exists( $path ) &&
146 !is_readable( $path )
147 ) {
148 throw $this->newExceptionAfterConnectError( 'SQLite database file is not readable' );
149 } elseif ( !in_array( $this->trxMode, self::$VALID_TRX_MODES, true ) ) {
150 throw $this->newExceptionAfterConnectError( "Got mode '{$this->trxMode}' for BEGIN" );
151 }
152
153 $attributes = [];
154 if ( $this->getFlag( self::DBO_PERSISTENT ) ) {
155 // Persistent connections can avoid some schema index reading overhead.
156 // On the other hand, they can cause horrible contention with DBO_TRX.
157 if ( $this->getFlag( self::DBO_TRX ) || $this->getFlag( self::DBO_DEFAULT ) ) {
158 $this->connLogger->warning(
159 __METHOD__ . ": ignoring DBO_PERSISTENT due to DBO_TRX or DBO_DEFAULT",
160 $this->getLogContext()
161 );
162 } else {
163 $attributes[PDO::ATTR_PERSISTENT] = true;
164 }
165 }
166
167 try {
168 // Open the database file, creating it if it does not yet exist
169 $this->conn = new PDO( "sqlite:$path", null, null, $attributes );
170 } catch ( PDOException $e ) {
171 throw $this->newExceptionAfterConnectError( $e->getMessage() );
172 }
173
174 $this->currentDomain = new DatabaseDomain( $dbName, null, $tablePrefix );
175
176 try {
177 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_NO_RETRY;
178 // Enforce LIKE to be case sensitive, just like MySQL
179 $this->query( 'PRAGMA case_sensitive_like = 1', __METHOD__, $flags );
180 // Apply optimizations or requirements regarding fsync() usage
181 $sync = $this->connectionVariables['synchronous'] ?? null;
182 if ( in_array( $sync, [ 'EXTRA', 'FULL', 'NORMAL', 'OFF' ], true ) ) {
183 $this->query( "PRAGMA synchronous = $sync", __METHOD__, $flags );
184 }
186 } catch ( Exception $e ) {
187 throw $this->newExceptionAfterConnectError( $e->getMessage() );
188 }
189 }
190
196 public function getDbFilePath() {
197 return $this->dbPath ?? self::generateFileName( $this->dbDir, $this->getDBname() );
198 }
199
203 public function getLockFileDirectory() {
204 if ( $this->dbPath !== null && !self::isProcessMemoryPath( $this->dbPath ) ) {
205 return dirname( $this->dbPath ) . '/locks';
206 } elseif ( $this->dbDir !== null && !self::isProcessMemoryPath( $this->dbDir ) ) {
207 return $this->dbDir . '/locks';
208 }
209
210 return null;
211 }
212
217 protected function closeConnection() {
218 $this->conn = null;
219
220 return true;
221 }
222
230 public static function generateFileName( $dir, $dbName ) {
231 if ( $dir == '' ) {
232 throw new DBUnexpectedError( null, __CLASS__ . ": no DB directory specified" );
233 } elseif ( self::isProcessMemoryPath( $dir ) ) {
234 throw new DBUnexpectedError(
235 null,
236 __CLASS__ . ": cannot use process memory directory '$dir'"
237 );
238 } elseif ( !strlen( $dbName ) ) {
239 throw new DBUnexpectedError( null, __CLASS__ . ": no DB name specified" );
240 }
241
242 return "$dir/$dbName.sqlite";
243 }
244
249 private static function generateDatabaseName( $path ) {
250 if ( preg_match( '/^(:memory:$|file::memory:)/', $path ) ) {
251 // E.g. "file::memory:?cache=shared" => ":memory":
252 return ':memory:';
253 } elseif ( preg_match( '/^file::([^?]+)\?mode=memory(&|$)/', $path, $m ) ) {
254 // E.g. "file:memdb1?mode=memory" => ":memdb1:"
255 return ":{$m[1]}:";
256 } else {
257 // E.g. "/home/.../some_db.sqlite3" => "some_db"
258 return preg_replace( '/\.sqlite\d?$/', '', basename( $path ) );
259 }
260 }
261
266 private static function isProcessMemoryPath( $path ) {
267 return preg_match( '/^(:memory:$|file:(:memory:|[^?]+\?mode=memory(&|$)))/', $path );
268 }
269
274 static function getFulltextSearchModule() {
275 static $cachedResult = null;
276 if ( $cachedResult !== null ) {
277 return $cachedResult;
278 }
279 $cachedResult = false;
280 $table = 'dummy_search_test';
281
282 $db = self::newStandaloneInstance( ':memory:' );
283 if ( $db->query( "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)", __METHOD__, true ) ) {
284 $cachedResult = 'FTS3';
285 }
286 $db->close();
287
288 return $cachedResult;
289 }
290
303 public function attachDatabase( $name, $file = false, $fname = __METHOD__ ) {
304 $file = is_string( $file ) ? $file : self::generateFileName( $this->dbDir, $name );
305 $encFile = $this->addQuotes( $file );
306
307 return $this->query(
308 "ATTACH DATABASE $encFile AS $name",
309 $fname,
310 self::QUERY_IGNORE_DBO_TRX
311 );
312 }
313
314 protected function isWriteQuery( $sql ) {
315 return parent::isWriteQuery( $sql ) && !preg_match( '/^(ATTACH|PRAGMA)\b/i', $sql );
316 }
317
318 protected function isTransactableQuery( $sql ) {
319 return parent::isTransactableQuery( $sql ) && !in_array(
320 $this->getQueryVerb( $sql ),
321 [ 'ATTACH', 'PRAGMA' ],
322 true
323 );
324 }
325
332 protected function doQuery( $sql ) {
333 $res = $this->getBindingHandle()->query( $sql );
334 if ( $res === false ) {
335 return false;
336 }
337
338 $resource = ResultWrapper::unwrap( $res );
339 $this->lastAffectedRowCount = $resource->rowCount();
340 $res = new ResultWrapper( $this, $resource->fetchAll() );
341
342 return $res;
343 }
344
348 function freeResult( $res ) {
349 if ( $res instanceof ResultWrapper ) {
350 $res->free();
351 }
352 }
353
358 function fetchObject( $res ) {
359 $resource =& ResultWrapper::unwrap( $res );
360
361 $cur = current( $resource );
362 if ( is_array( $cur ) ) {
363 next( $resource );
364 $obj = new stdClass;
365 foreach ( $cur as $k => $v ) {
366 if ( !is_numeric( $k ) ) {
367 $obj->$k = $v;
368 }
369 }
370
371 return $obj;
372 }
373
374 return false;
375 }
376
381 function fetchRow( $res ) {
382 $resource =& ResultWrapper::unwrap( $res );
383 $cur = current( $resource );
384 if ( is_array( $cur ) ) {
385 next( $resource );
386
387 return $cur;
388 }
389
390 return false;
391 }
392
399 function numRows( $res ) {
400 // false does not implement Countable
401 $resource = ResultWrapper::unwrap( $res );
402
403 return is_array( $resource ) ? count( $resource ) : 0;
404 }
405
410 function numFields( $res ) {
411 $resource = ResultWrapper::unwrap( $res );
412 if ( is_array( $resource ) && count( $resource ) > 0 ) {
413 // The size of the result array is twice the number of fields. (T67578)
414 return count( $resource[0] ) / 2;
415 } else {
416 // If the result is empty return 0
417 return 0;
418 }
419 }
420
426 function fieldName( $res, $n ) {
427 $resource = ResultWrapper::unwrap( $res );
428 if ( is_array( $resource ) ) {
429 $keys = array_keys( $resource[0] );
430
431 return $keys[$n];
432 }
433
434 return false;
435 }
436
437 protected function doSelectDomain( DatabaseDomain $domain ) {
438 if ( $domain->getSchema() !== null ) {
439 throw new DBExpectedError(
440 $this,
441 __CLASS__ . ": domain '{$domain->getId()}' has a schema component"
442 );
443 }
444
445 $database = $domain->getDatabase();
446 // A null database means "don't care" so leave it as is and update the table prefix
447 if ( $database === null ) {
448 $this->currentDomain = new DatabaseDomain(
449 $this->currentDomain->getDatabase(),
450 null,
451 $domain->getTablePrefix()
452 );
453
454 return true;
455 }
456
457 if ( $database !== $this->getDBname() ) {
458 throw new DBExpectedError(
459 $this,
460 __CLASS__ . ": cannot change database (got '$database')"
461 );
462 }
463
464 return true;
465 }
466
474 function tableName( $name, $format = 'quoted' ) {
475 // table names starting with sqlite_ are reserved
476 if ( strpos( $name, 'sqlite_' ) === 0 ) {
477 return $name;
478 }
479
480 return str_replace( '"', '', parent::tableName( $name, $format ) );
481 }
482
488 function insertId() {
489 // PDO::lastInsertId yields a string :(
490 return intval( $this->getBindingHandle()->lastInsertId() );
491 }
492
497 function dataSeek( $res, $row ) {
498 $resource =& ResultWrapper::unwrap( $res );
499 reset( $resource );
500 if ( $row > 0 ) {
501 for ( $i = 0; $i < $row; $i++ ) {
502 next( $resource );
503 }
504 }
505 }
506
510 function lastError() {
511 if ( !is_object( $this->conn ) ) {
512 return "Cannot return last error, no db connection";
513 }
514 $e = $this->conn->errorInfo();
515
516 return $e[2] ?? '';
517 }
518
522 function lastErrno() {
523 if ( !is_object( $this->conn ) ) {
524 return "Cannot return last error, no db connection";
525 } else {
526 $info = $this->conn->errorInfo();
527
528 return $info[1];
529 }
530 }
531
535 protected function fetchAffectedRowCount() {
537 }
538
539 function tableExists( $table, $fname = __METHOD__ ) {
540 $tableRaw = $this->tableName( $table, 'raw' );
541 if ( isset( $this->sessionTempTables[$tableRaw] ) ) {
542 return true; // already known to exist
543 }
544
545 $encTable = $this->addQuotes( $tableRaw );
546 $res = $this->query(
547 "SELECT 1 FROM sqlite_master WHERE type='table' AND name=$encTable",
548 __METHOD__,
549 self::QUERY_IGNORE_DBO_TRX
550 );
551
552 return $res->numRows() ? true : false;
553 }
554
565 function indexInfo( $table, $index, $fname = __METHOD__ ) {
566 $sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')';
567 $res = $this->query( $sql, $fname, self::QUERY_IGNORE_DBO_TRX );
568 if ( !$res || $res->numRows() == 0 ) {
569 return false;
570 }
571 $info = [];
572 foreach ( $res as $row ) {
573 $info[] = $row->name;
574 }
575
576 return $info;
577 }
578
585 function indexUnique( $table, $index, $fname = __METHOD__ ) {
586 $row = $this->selectRow( 'sqlite_master', '*',
587 [
588 'type' => 'index',
589 'name' => $this->indexName( $index ),
590 ], $fname );
591 if ( !$row || !isset( $row->sql ) ) {
592 return null;
593 }
594
595 // $row->sql will be of the form CREATE [UNIQUE] INDEX ...
596 $indexPos = strpos( $row->sql, 'INDEX' );
597 if ( $indexPos === false ) {
598 return null;
599 }
600 $firstPart = substr( $row->sql, 0, $indexPos );
601 $options = explode( ' ', $firstPart );
602
603 return in_array( 'UNIQUE', $options );
604 }
605
606 protected function makeSelectOptions( array $options ) {
607 // Remove problematic options that the base implementation converts to SQL
608 foreach ( $options as $k => $v ) {
609 if ( is_numeric( $k ) && ( $v === 'FOR UPDATE' || $v === 'LOCK IN SHARE MODE' ) ) {
610 $options[$k] = '';
611 }
612 }
613
614 return parent::makeSelectOptions( $options );
615 }
616
621 protected function makeUpdateOptionsArray( $options ) {
622 $options = parent::makeUpdateOptionsArray( $options );
623 $options = self::fixIgnore( $options );
624
625 return $options;
626 }
627
632 static function fixIgnore( $options ) {
633 # SQLite uses OR IGNORE not just IGNORE
634 foreach ( $options as $k => $v ) {
635 if ( $v == 'IGNORE' ) {
636 $options[$k] = 'OR IGNORE';
637 }
638 }
639
640 return $options;
641 }
642
647 function makeInsertOptions( $options ) {
648 $options = self::fixIgnore( $options );
649
650 return parent::makeInsertOptions( $options );
651 }
652
661 function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
662 if ( !count( $a ) ) {
663 return true;
664 }
665
666 # SQLite can't handle multi-row inserts, so divide up into multiple single-row inserts
667 if ( isset( $a[0] ) && is_array( $a[0] ) ) {
669 try {
670 $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
671 foreach ( $a as $v ) {
672 parent::insert( $table, $v, "$fname/multi-row", $options );
674 }
675 $this->endAtomic( $fname );
676 } catch ( Exception $e ) {
677 $this->cancelAtomic( $fname );
678 throw $e;
679 }
680 $this->affectedRowCount = $affectedRowCount;
681 } else {
682 parent::insert( $table, $a, "$fname/single-row", $options );
683 }
684
685 return true;
686 }
687
694 function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
695 if ( !count( $rows ) ) {
696 return;
697 }
698
699 # SQLite can't handle multi-row replaces, so divide up into multiple single-row queries
700 if ( isset( $rows[0] ) && is_array( $rows[0] ) ) {
702 try {
703 $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
704 foreach ( $rows as $v ) {
705 $this->nativeReplace( $table, $v, "$fname/multi-row" );
707 }
708 $this->endAtomic( $fname );
709 } catch ( Exception $e ) {
710 $this->cancelAtomic( $fname );
711 throw $e;
712 }
713 $this->affectedRowCount = $affectedRowCount;
714 } else {
715 $this->nativeReplace( $table, $rows, "$fname/single-row" );
716 }
717 }
718
727 function textFieldSize( $table, $field ) {
728 return -1;
729 }
730
735 return false;
736 }
737
743 function unionQueries( $sqls, $all ) {
744 $glue = $all ? ' UNION ALL ' : ' UNION ';
745
746 return implode( $glue, $sqls );
747 }
748
752 function wasDeadlock() {
753 return $this->lastErrno() == 5; // SQLITE_BUSY
754 }
755
759 function wasReadOnlyError() {
760 return $this->lastErrno() == 8; // SQLITE_READONLY;
761 }
762
763 public function wasConnectionError( $errno ) {
764 return $errno == 17; // SQLITE_SCHEMA;
765 }
766
767 protected function wasKnownStatementRollbackError() {
768 // ON CONFLICT ROLLBACK clauses make it so that SQLITE_CONSTRAINT error is
769 // ambiguous with regard to whether it implies a ROLLBACK or an ABORT happened.
770 // https://sqlite.org/lang_createtable.html#uniqueconst
771 // https://sqlite.org/lang_conflict.html
772 return false;
773 }
774
775 public function serverIsReadOnly() {
777
778 $path = $this->getDbFilePath();
779
780 return ( !self::isProcessMemoryPath( $path ) && !is_writable( $path ) );
781 }
782
786 public function getSoftwareLink() {
787 return "[{{int:version-db-sqlite-url}} SQLite]";
788 }
789
793 function getServerVersion() {
794 $ver = $this->getBindingHandle()->getAttribute( PDO::ATTR_SERVER_VERSION );
795
796 return $ver;
797 }
798
807 function fieldInfo( $table, $field ) {
808 $tableName = $this->tableName( $table );
809 $sql = 'PRAGMA table_info(' . $this->addQuotes( $tableName ) . ')';
810 $res = $this->query( $sql, __METHOD__, self::QUERY_IGNORE_DBO_TRX );
811 foreach ( $res as $row ) {
812 if ( $row->name == $field ) {
813 return new SQLiteField( $row, $tableName );
814 }
815 }
816
817 return false;
818 }
819
820 protected function doBegin( $fname = '' ) {
821 if ( $this->trxMode != '' ) {
822 $this->query( "BEGIN {$this->trxMode}", $fname );
823 } else {
824 $this->query( 'BEGIN', $fname );
825 }
826 }
827
832 function strencode( $s ) {
833 return substr( $this->addQuotes( $s ), 1, -1 );
834 }
835
840 function encodeBlob( $b ) {
841 return new Blob( $b );
842 }
843
848 function decodeBlob( $b ) {
849 if ( $b instanceof Blob ) {
850 $b = $b->fetch();
851 }
852
853 return $b;
854 }
855
860 function addQuotes( $s ) {
861 if ( $s instanceof Blob ) {
862 return "x'" . bin2hex( $s->fetch() ) . "'";
863 } elseif ( is_bool( $s ) ) {
864 return (int)$s;
865 } elseif ( strpos( (string)$s, "\0" ) !== false ) {
866 // SQLite doesn't support \0 in strings, so use the hex representation as a workaround.
867 // This is a known limitation of SQLite's mprintf function which PDO
868 // should work around, but doesn't. I have reported this to php.net as bug #63419:
869 // https://bugs.php.net/bug.php?id=63419
870 // There was already a similar report for SQLite3::escapeString, bug #62361:
871 // https://bugs.php.net/bug.php?id=62361
872 // There is an additional bug regarding sorting this data after insert
873 // on older versions of sqlite shipped with ubuntu 12.04
874 // https://phabricator.wikimedia.org/T74367
875 $this->queryLogger->debug(
876 __FUNCTION__ .
877 ': Quoting value containing null byte. ' .
878 'For consistency all binary data should have been ' .
879 'first processed with self::encodeBlob()'
880 );
881 return "x'" . bin2hex( (string)$s ) . "'";
882 } else {
883 return $this->getBindingHandle()->quote( (string)$s );
884 }
885 }
886
887 public function buildSubstring( $input, $startPosition, $length = null ) {
888 $this->assertBuildSubstringParams( $startPosition, $length );
889 $params = [ $input, $startPosition ];
890 if ( $length !== null ) {
891 $params[] = $length;
892 }
893 return 'SUBSTR(' . implode( ',', $params ) . ')';
894 }
895
901 public function buildStringCast( $field ) {
902 return 'CAST ( ' . $field . ' AS TEXT )';
903 }
904
910 public function deadlockLoop( /*...*/ ) {
911 $args = func_get_args();
912 $function = array_shift( $args );
913
914 return $function( ...$args );
915 }
916
921 protected function replaceVars( $s ) {
922 $s = parent::replaceVars( $s );
923 if ( preg_match( '/^\s*(CREATE|ALTER) TABLE/i', $s ) ) {
924 // CREATE TABLE hacks to allow schema file sharing with MySQL
925
926 // binary/varbinary column type -> blob
927 $s = preg_replace( '/\b(var)?binary(\‍(\d+\‍))/i', 'BLOB', $s );
928 // no such thing as unsigned
929 $s = preg_replace( '/\b(un)?signed\b/i', '', $s );
930 // INT -> INTEGER
931 $s = preg_replace( '/\b(tiny|small|medium|big|)int(\s*\‍(\s*\d+\s*\‍)|\b)/i', 'INTEGER', $s );
932 // floating point types -> REAL
933 $s = preg_replace(
934 '/\b(float|double(\s+precision)?)(\s*\‍(\s*\d+\s*(,\s*\d+\s*)?\‍)|\b)/i',
935 'REAL',
936 $s
937 );
938 // varchar -> TEXT
939 $s = preg_replace( '/\b(var)?char\s*\‍(.*?\‍)/i', 'TEXT', $s );
940 // TEXT normalization
941 $s = preg_replace( '/\b(tiny|medium|long)text\b/i', 'TEXT', $s );
942 // BLOB normalization
943 $s = preg_replace( '/\b(tiny|small|medium|long|)blob\b/i', 'BLOB', $s );
944 // BOOL -> INTEGER
945 $s = preg_replace( '/\bbool(ean)?\b/i', 'INTEGER', $s );
946 // DATETIME -> TEXT
947 $s = preg_replace( '/\b(datetime|timestamp)\b/i', 'TEXT', $s );
948 // No ENUM type
949 $s = preg_replace( '/\benum\s*\‍([^)]*\‍)/i', 'TEXT', $s );
950 // binary collation type -> nothing
951 $s = preg_replace( '/\bbinary\b/i', '', $s );
952 // auto_increment -> autoincrement
953 $s = preg_replace( '/\bauto_increment\b/i', 'AUTOINCREMENT', $s );
954 // No explicit options
955 $s = preg_replace( '/\‍)[^);]*(;?)\s*$/', ')\1', $s );
956 // AUTOINCREMENT should immedidately follow PRIMARY KEY
957 $s = preg_replace( '/primary key (.*?) autoincrement/i', 'PRIMARY KEY AUTOINCREMENT $1', $s );
958 } elseif ( preg_match( '/^\s*CREATE (\s*(?:UNIQUE|FULLTEXT)\s+)?INDEX/i', $s ) ) {
959 // No truncated indexes
960 $s = preg_replace( '/\‍(\d+\‍)/', '', $s );
961 // No FULLTEXT
962 $s = preg_replace( '/\bfulltext\b/i', '', $s );
963 } elseif ( preg_match( '/^\s*DROP INDEX/i', $s ) ) {
964 // DROP INDEX is database-wide, not table-specific, so no ON <table> clause.
965 $s = preg_replace( '/\sON\s+[^\s]*/i', '', $s );
966 } elseif ( preg_match( '/^\s*INSERT IGNORE\b/i', $s ) ) {
967 // INSERT IGNORE --> INSERT OR IGNORE
968 $s = preg_replace( '/^\s*INSERT IGNORE\b/i', 'INSERT OR IGNORE', $s );
969 }
970
971 return $s;
972 }
973
974 public function lock( $lockName, $method, $timeout = 5 ) {
975 $status = $this->lockMgr->lock( [ $lockName ], LockManager::LOCK_EX, $timeout );
976 if (
977 $this->lockMgr instanceof FSLockManager &&
978 $status->hasMessage( 'lockmanager-fail-openlock' )
979 ) {
980 throw new DBError( $this, "Cannot create directory \"{$this->getLockFileDirectory()}\"" );
981 }
982
983 return $status->isOK();
984 }
985
986 public function unlock( $lockName, $method ) {
987 return $this->lockMgr->unlock( [ $lockName ], LockManager::LOCK_EX )->isGood();
988 }
989
996 function buildConcat( $stringList ) {
997 return '(' . implode( ') || (', $stringList ) . ')';
998 }
999
1000 public function buildGroupConcatField(
1001 $delim, $table, $field, $conds = '', $join_conds = []
1002 ) {
1003 $fld = "group_concat($field," . $this->addQuotes( $delim ) . ')';
1004
1005 return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
1006 }
1007
1016 function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
1017 $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name=" .
1018 $this->addQuotes( $oldName ) . " AND type='table'", $fname );
1019 $obj = $this->fetchObject( $res );
1020 if ( !$obj ) {
1021 throw new RuntimeException( "Couldn't retrieve structure for table $oldName" );
1022 }
1023 $sql = $obj->sql;
1024 $sql = preg_replace(
1025 '/(?<=\W)"?' .
1026 preg_quote( trim( $this->addIdentifierQuotes( $oldName ), '"' ), '/' ) .
1027 '"?(?=\W)/',
1028 $this->addIdentifierQuotes( $newName ),
1029 $sql,
1030 1
1031 );
1032 if ( $temporary ) {
1033 if ( preg_match( '/^\\s*CREATE\\s+VIRTUAL\\s+TABLE\b/i', $sql ) ) {
1034 $this->queryLogger->debug(
1035 "Table $oldName is virtual, can't create a temporary duplicate.\n" );
1036 } else {
1037 $sql = str_replace( 'CREATE TABLE', 'CREATE TEMPORARY TABLE', $sql );
1038 }
1039 }
1040
1041 $res = $this->query( $sql, $fname, self::QUERY_PSEUDO_PERMANENT );
1042
1043 // Take over indexes
1044 $indexList = $this->query( 'PRAGMA INDEX_LIST(' . $this->addQuotes( $oldName ) . ')' );
1045 foreach ( $indexList as $index ) {
1046 if ( strpos( $index->name, 'sqlite_autoindex' ) === 0 ) {
1047 continue;
1048 }
1049
1050 if ( $index->unique ) {
1051 $sql = 'CREATE UNIQUE INDEX';
1052 } else {
1053 $sql = 'CREATE INDEX';
1054 }
1055 // Try to come up with a new index name, given indexes have database scope in SQLite
1056 $indexName = $newName . '_' . $index->name;
1057 $sql .= ' ' . $indexName . ' ON ' . $newName;
1058
1059 $indexInfo = $this->query( 'PRAGMA INDEX_INFO(' . $this->addQuotes( $index->name ) . ')' );
1060 $fields = [];
1061 foreach ( $indexInfo as $indexInfoRow ) {
1062 $fields[$indexInfoRow->seqno] = $indexInfoRow->name;
1063 }
1064
1065 $sql .= '(' . implode( ',', $fields ) . ')';
1066
1067 $this->query( $sql );
1068 }
1069
1070 return $res;
1071 }
1072
1081 function listTables( $prefix = null, $fname = __METHOD__ ) {
1082 $result = $this->select(
1083 'sqlite_master',
1084 'name',
1085 "type='table'"
1086 );
1087
1088 $endArray = [];
1089
1090 foreach ( $result as $table ) {
1091 $vars = get_object_vars( $table );
1092 $table = array_pop( $vars );
1093
1094 if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
1095 if ( strpos( $table, 'sqlite_' ) !== 0 ) {
1096 $endArray[] = $table;
1097 }
1098 }
1099 }
1100
1101 return $endArray;
1102 }
1103
1112 public function dropTable( $tableName, $fName = __METHOD__ ) {
1113 if ( !$this->tableExists( $tableName, $fName ) ) {
1114 return false;
1115 }
1116 $sql = "DROP TABLE " . $this->tableName( $tableName );
1117
1118 return $this->query( $sql, $fName, self::QUERY_IGNORE_DBO_TRX );
1119 }
1120
1121 public function setTableAliases( array $aliases ) {
1122 parent::setTableAliases( $aliases );
1123 if ( $this->isOpen() ) {
1125 }
1126 }
1127
1132 foreach ( $this->tableAliases as $params ) {
1133 if (
1134 $params['dbname'] !== $this->getDBname() &&
1135 !isset( $this->sessionAttachedDbs[$params['dbname']] )
1136 ) {
1137 $this->attachDatabase( $params['dbname'] );
1138 $this->sessionAttachedDbs[$params['dbname']] = true;
1139 }
1140 }
1141 }
1142
1143 public function resetSequenceForTable( $table, $fname = __METHOD__ ) {
1144 $encTable = $this->addIdentifierQuotes( 'sqlite_sequence' );
1145 $encName = $this->addQuotes( $this->tableName( $table, 'raw' ) );
1146 $this->query(
1147 "DELETE FROM $encTable WHERE name = $encName",
1148 $fname,
1149 self::QUERY_IGNORE_DBO_TRX
1150 );
1151 }
1152
1153 public function databasesAreIndependent() {
1154 return true;
1155 }
1156
1157 protected function doHandleSessionLossPreconnect() {
1158 $this->sessionAttachedDbs = [];
1159 }
1160
1164 protected function getBindingHandle() {
1165 return parent::getBindingHandle();
1166 }
1167}
1168
1172class_alias( DatabaseSqlite::class, 'DatabaseSqlite' );
if( $line===false) $args
Definition cdb.php:64
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.
Definition DBError.php:30
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.
databasesAreIndependent()
Returns true if DBs are assumed to be on potentially different servers.
indexUnique( $table, $index, $fname=__METHOD__)
lock( $lockName, $method, $timeout=5)
Acquire a named lock.
array $sessionAttachedDbs
List of shared database already attached to this connection.
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.
deadlockLoop()
No-op version of deadlockLoop.
unlock( $lockName, $method)
Release a lock.
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)
buildConcat( $stringList)
Build a concatenation list to feed into a SQL query.
isTransactableQuery( $sql)
Determine whether a SQL statement is sensitive to isolation level.
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.
isWriteQuery( $sql)
Determine whether a query writes to the DB.
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...
dropTable( $tableName, $fName=__METHOD__)
Override due to no CASCADE support.
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.
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.
replace( $table, $uniqueIndexes, $rows, $fname=__METHOD__)
buildGroupConcatField( $delim, $table, $field, $conds='', $join_conds=[])
Build a GROUP_CONCAT or equivalent statement for a query.
static generateFileName( $dir, $dbName)
Generates a database file name.
duplicateTableStructure( $oldName, $newName, $temporary=false, $fname=__METHOD__)
insertId()
This must be called after nextSequenceVal.
insert( $table, $a, $fname=__METHOD__, $options=[])
Based on generic method (parent) with some prior SQLite-sepcific adjustments.
resetSequenceForTable( $table, $fname=__METHOD__)
__construct(array $p)
Additional params include:
buildSubstring( $input, $startPosition, $length=null)
Relational database abstraction object.
Definition Database.php:49
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
Wrapper to IDatabase::select() that only fetches one row (via LIMIT)
integer null $affectedRowCount
Rows affected by the last query to query() or its CRUD wrappers.
Definition Database.php:164
newExceptionAfterConnectError( $error)
endAtomic( $fname=__METHOD__)
Ends an atomic section of SQL statements.
cancelAtomic( $fname=__METHOD__, AtomicSectionIdentifier $sectionId=null)
Cancel 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:790
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
string $user
User that this instance is currently connected under the name of.
Definition Database.php:77
int $flags
Current bit field of class DBO_* constants.
Definition Database.php:92
assertHasConnectionHandle()
Make sure there is an open connection handle (alive or not) as a sanity check.
Definition Database.php:954
affectedRows()
Get the number of rows affected by the last write query.
close( $fname=__METHOD__, $owner=null)
Close the database connection.
Definition Database.php:876
startAtomic( $fname=__METHOD__, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Begin an atomic section of SQL statements.
addIdentifierQuotes( $s)
Escape a SQL identifier (e.g.
getLogContext(array $extras=[])
Create a log context to pass to PSR-3 logger functions.
Definition Database.php:865
nativeReplace( $table, $rows, $fname)
REPLACE query wrapper for MySQL and SQLite, which have a native REPLACE statement.
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.
string $server
Server that this instance is currently connected to.
Definition Database.php:75
static factory( $type, $params=[], $connect=self::NEW_CONNECTED)
Construct a Database subclass instance given a database type and parameters.
Definition Database.php:370
getFlag( $flag)
Returns a boolean whether the flag $flag is set for this connection.
Definition Database.php:786
query( $sql, $fname=__METHOD__, $flags=0)
Run an SQL query and return the result.
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.
return true
Definition router.php:94
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42