MediaWiki  master
DatabaseSqlite.php
Go to the documentation of this file.
1 <?php
24 namespace Wikimedia\Rdbms;
25 
26 use FSLockManager;
27 use LockManager;
28 use NullLockManager;
29 use PDO;
30 use PDOException;
31 use RuntimeException;
34 
38 class DatabaseSqlite extends Database {
40  protected $dbDir;
42  protected $dbPath;
44  protected $trxMode;
45 
48 
50  protected $conn;
51 
53  protected $lockMgr;
54 
56  private $version;
57 
59  private $sessionAttachedDbs = [];
60 
62  private const VALID_TRX_MODES = [ '', 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ];
63 
65  private const VALID_PRAGMAS = [
66  // Optimizations or requirements regarding fsync() usage
67  'synchronous' => [ 'EXTRA', 'FULL', 'NORMAL', 'OFF' ],
68  // Optimizations for TEMPORARY tables
69  'temp_store' => [ 'FILE', 'MEMORY' ]
70  ];
71 
73  protected $platform;
74 
82  public function __construct( array $params ) {
83  if ( isset( $params['dbFilePath'] ) ) {
84  $this->dbPath = $params['dbFilePath'];
85  if ( !strlen( $params['dbname'] ) ) {
86  $params['dbname'] = self::generateDatabaseName( $this->dbPath );
87  }
88  } elseif ( isset( $params['dbDirectory'] ) ) {
89  $this->dbDir = $params['dbDirectory'];
90  }
91 
92  parent::__construct( $params );
93 
94  $this->trxMode = strtoupper( $params['trxMode'] ?? '' );
95 
96  $this->lockMgr = $this->makeLockManager();
97  $this->platform = new SqlitePlatform( $this );
98  }
99 
100  protected static function getAttributes() {
101  return [
102  self::ATTR_DB_IS_FILE => true,
103  self::ATTR_DB_LEVEL_LOCKING => true
104  ];
105  }
106 
116  public static function newStandaloneInstance( $filename, array $p = [] ) {
117  $p['dbFilePath'] = $filename;
118  $p['schema'] = null;
119  $p['tablePrefix'] = '';
121  $db = Database::factory( 'sqlite', $p );
122  '@phan-var DatabaseSqlite $db';
123 
124  return $db;
125  }
126 
130  public function getType() {
131  return 'sqlite';
132  }
133 
134  protected function open( $server, $user, $password, $db, $schema, $tablePrefix ) {
135  $this->close( __METHOD__ );
136 
137  // Note that for SQLite, $server, $user, and $pass are ignored
138 
139  if ( $schema !== null ) {
140  throw $this->newExceptionAfterConnectError( "Got schema '$schema'; not supported." );
141  }
142 
143  if ( $this->dbPath !== null ) {
145  } elseif ( $this->dbDir !== null ) {
146  $path = self::generateFileName( $this->dbDir, $db );
147  } else {
148  throw $this->newExceptionAfterConnectError( "DB path or directory required" );
149  }
150 
151  // Check if the database file already exists but is non-readable
152  if ( !self::isProcessMemoryPath( $path ) && is_file( $path ) && !is_readable( $path ) ) {
153  throw $this->newExceptionAfterConnectError( 'SQLite database file is not readable' );
154  } elseif ( !in_array( $this->trxMode, self::VALID_TRX_MODES, true ) ) {
155  throw $this->newExceptionAfterConnectError( "Got mode '{$this->trxMode}' for BEGIN" );
156  }
157 
158  $attributes = [ PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT ];
159  if ( $this->getFlag( self::DBO_PERSISTENT ) ) {
160  // Persistent connections can avoid some schema index reading overhead.
161  // On the other hand, they can cause horrible contention with DBO_TRX.
162  if ( $this->getFlag( self::DBO_TRX ) || $this->getFlag( self::DBO_DEFAULT ) ) {
163  $this->connLogger->warning(
164  __METHOD__ . ": ignoring DBO_PERSISTENT due to DBO_TRX or DBO_DEFAULT",
165  $this->getLogContext()
166  );
167  } else {
168  $attributes[PDO::ATTR_PERSISTENT] = true;
169  }
170  }
171 
172  try {
173  // Open the database file, creating it if it does not yet exist
174  $this->conn = new PDO( "sqlite:$path", null, null, $attributes );
175  } catch ( PDOException $e ) {
176  throw $this->newExceptionAfterConnectError( $e->getMessage() );
177  }
178 
179  $this->currentDomain = new DatabaseDomain( $db, null, $tablePrefix );
180 
181  try {
182  $flags = self::QUERY_CHANGE_TRX | self::QUERY_NO_RETRY;
183  // Enforce LIKE to be case sensitive, just like MySQL
184  $this->query( 'PRAGMA case_sensitive_like = 1', __METHOD__, $flags );
185  // Set any connection-level custom PRAGMA options
186  $pragmas = array_intersect_key( $this->connectionVariables, self::VALID_PRAGMAS );
187  $pragmas += $this->getDefaultPragmas();
188  foreach ( $pragmas as $name => $value ) {
189  $allowed = self::VALID_PRAGMAS[$name];
190  if ( in_array( $value, $allowed, true ) ) {
191  $this->query( "PRAGMA $name = $value", __METHOD__, $flags );
192  }
193  }
195  } catch ( RuntimeException $e ) {
196  throw $this->newExceptionAfterConnectError( $e->getMessage() );
197  }
198  }
199 
203  private function getDefaultPragmas() {
204  $variables = [];
205 
206  if ( !$this->cliMode ) {
207  $variables['temp_store'] = 'MEMORY';
208  }
209 
210  return $variables;
211  }
212 
218  public function getDbFilePath() {
219  return $this->dbPath ?? self::generateFileName( $this->dbDir, $this->getDBname() );
220  }
221 
225  public function getLockFileDirectory() {
226  if ( $this->dbPath !== null && !self::isProcessMemoryPath( $this->dbPath ) ) {
227  return dirname( $this->dbPath ) . '/locks';
228  } elseif ( $this->dbDir !== null && !self::isProcessMemoryPath( $this->dbDir ) ) {
229  return $this->dbDir . '/locks';
230  }
231 
232  return null;
233  }
234 
240  private function makeLockManager(): LockManager {
241  $lockDirectory = $this->getLockFileDirectory();
242  if ( $lockDirectory !== null ) {
243  return new FSLockManager( [
244  'domain' => $this->getDomainID(),
245  'lockDirectory' => $lockDirectory,
246  ] );
247  } else {
248  return new NullLockManager( [ 'domain' => $this->getDomainID() ] );
249  }
250  }
251 
256  protected function closeConnection() {
257  $this->conn = null;
258  // Release all locks, via FSLockManager::__destruct, as the base class expects
259  $this->lockMgr = null;
260 
261  return true;
262  }
263 
271  public static function generateFileName( $dir, $dbName ) {
272  if ( $dir == '' ) {
273  throw new DBUnexpectedError( null, __CLASS__ . ": no DB directory specified" );
274  } elseif ( self::isProcessMemoryPath( $dir ) ) {
275  throw new DBUnexpectedError(
276  null,
277  __CLASS__ . ": cannot use process memory directory '$dir'"
278  );
279  } elseif ( !strlen( $dbName ) ) {
280  throw new DBUnexpectedError( null, __CLASS__ . ": no DB name specified" );
281  }
282 
283  return "$dir/$dbName.sqlite";
284  }
285 
290  private static function generateDatabaseName( $path ) {
291  if ( preg_match( '/^(:memory:$|file::memory:)/', $path ) ) {
292  // E.g. "file::memory:?cache=shared" => ":memory":
293  return ':memory:';
294  } elseif ( preg_match( '/^file::([^?]+)\?mode=memory(&|$)/', $path, $m ) ) {
295  // E.g. "file:memdb1?mode=memory" => ":memdb1:"
296  return ":{$m[1]}:";
297  } else {
298  // E.g. "/home/.../some_db.sqlite3" => "some_db"
299  return preg_replace( '/\.sqlite\d?$/', '', basename( $path ) );
300  }
301  }
302 
307  private static function isProcessMemoryPath( $path ) {
308  return preg_match( '/^(:memory:$|file:(:memory:|[^?]+\?mode=memory(&|$)))/', $path );
309  }
310 
315  public static function getFulltextSearchModule() {
316  static $cachedResult = null;
317  if ( $cachedResult !== null ) {
318  return $cachedResult;
319  }
320  $cachedResult = false;
321  $table = 'dummy_search_test';
322 
323  $db = self::newStandaloneInstance( ':memory:' );
324  if ( $db->query(
325  "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)",
326  __METHOD__,
327  IDatabase::QUERY_SILENCE_ERRORS
328  ) ) {
329  $cachedResult = 'FTS3';
330  }
331  $db->close( __METHOD__ );
332 
333  return $cachedResult;
334  }
335 
348  public function attachDatabase( $name, $file = false, $fname = __METHOD__ ) {
349  $file = is_string( $file ) ? $file : self::generateFileName( $this->dbDir, $name );
350  $encFile = $this->addQuotes( $file );
351 
352  return $this->query(
353  "ATTACH DATABASE $encFile AS $name",
354  $fname,
355  self::QUERY_CHANGE_TRX
356  );
357  }
358 
359  protected function isWriteQuery( $sql, $flags ) {
360  return parent::isWriteQuery( $sql, $flags ) && !preg_match( '/^(ATTACH|PRAGMA)\b/i', $sql );
361  }
362 
363  protected function isTransactableQuery( $sql ) {
364  return parent::isTransactableQuery( $sql ) && !in_array(
365  $this->getQueryVerb( $sql ),
366  [ 'ATTACH', 'PRAGMA' ],
367  true
368  );
369  }
370 
375  protected function doQuery( $sql ) {
376  $res = $this->getBindingHandle()->query( $sql );
377  if ( $res === false ) {
378  return false;
379  }
380 
381  $this->lastAffectedRowCount = $res->rowCount();
382  return new SqliteResultWrapper( $res );
383  }
384 
385  protected function doSelectDomain( DatabaseDomain $domain ) {
386  if ( $domain->getSchema() !== null ) {
387  throw new DBExpectedError(
388  $this,
389  __CLASS__ . ": domain '{$domain->getId()}' has a schema component"
390  );
391  }
392 
393  $database = $domain->getDatabase();
394  // A null database means "don't care" so leave it as is and update the table prefix
395  if ( $database === null ) {
396  $this->currentDomain = new DatabaseDomain(
397  $this->currentDomain->getDatabase(),
398  null,
399  $domain->getTablePrefix()
400  );
401 
402  return true;
403  }
404 
405  if ( $database !== $this->getDBname() ) {
406  throw new DBExpectedError(
407  $this,
408  __CLASS__ . ": cannot change database (got '$database')"
409  );
410  }
411 
412  return true;
413  }
414 
422  public function tableName( $name, $format = 'quoted' ) {
423  // table names starting with sqlite_ are reserved
424  if ( strpos( $name, 'sqlite_' ) === 0 ) {
425  return $name;
426  }
427 
428  return str_replace( '"', '', parent::tableName( $name, $format ) );
429  }
430 
436  public function insertId() {
437  // PDO::lastInsertId yields a string :(
438  return intval( $this->getBindingHandle()->lastInsertId() );
439  }
440 
444  public function lastError() {
445  if ( is_object( $this->conn ) ) {
446  $e = $this->conn->errorInfo();
447 
448  return $e[2] ?? '';
449  }
450  return 'No database connection';
451  }
452 
456  public function lastErrno() {
457  if ( is_object( $this->conn ) ) {
458  $info = $this->conn->errorInfo();
459 
460  if ( isset( $info[1] ) ) {
461  return $info[1];
462  }
463  }
464  return 0;
465  }
466 
470  protected function fetchAffectedRowCount() {
471  return $this->lastAffectedRowCount;
472  }
473 
474  public function tableExists( $table, $fname = __METHOD__ ) {
475  $tableRaw = $this->tableName( $table, 'raw' );
476  if ( isset( $this->sessionTempTables[$tableRaw] ) ) {
477  return true; // already known to exist
478  }
479 
480  $encTable = $this->addQuotes( $tableRaw );
481  $res = $this->query(
482  "SELECT 1 FROM sqlite_master WHERE type='table' AND name=$encTable",
483  __METHOD__,
484  self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
485  );
486 
487  return $res->numRows() ? true : false;
488  }
489 
500  public function indexInfo( $table, $index, $fname = __METHOD__ ) {
501  $sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')';
502  $res = $this->query( $sql, $fname, self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE );
503  if ( !$res || $res->numRows() == 0 ) {
504  return false;
505  }
506  $info = [];
507  foreach ( $res as $row ) {
508  $info[] = $row->name;
509  }
510 
511  return $info;
512  }
513 
520  public function indexUnique( $table, $index, $fname = __METHOD__ ) {
521  $row = $this->selectRow( 'sqlite_master', '*',
522  [
523  'type' => 'index',
524  'name' => $this->indexName( $index ),
525  ], $fname );
526  if ( !$row || !isset( $row->sql ) ) {
527  return null;
528  }
529 
530  // $row->sql will be of the form CREATE [UNIQUE] INDEX ...
531  $indexPos = strpos( $row->sql, 'INDEX' );
532  if ( $indexPos === false ) {
533  return null;
534  }
535  $firstPart = substr( $row->sql, 0, $indexPos );
536  $options = explode( ' ', $firstPart );
537 
538  return in_array( 'UNIQUE', $options );
539  }
540 
541  protected function makeSelectOptions( array $options ) {
542  // Remove problematic options that the base implementation converts to SQL
543  foreach ( $options as $k => $v ) {
544  if ( is_numeric( $k ) && ( $v === 'FOR UPDATE' || $v === 'LOCK IN SHARE MODE' ) ) {
545  $options[$k] = '';
546  }
547  }
548 
549  return parent::makeSelectOptions( $options );
550  }
551 
556  protected function makeUpdateOptionsArray( $options ) {
557  $options = parent::makeUpdateOptionsArray( $options );
558  $options = $this->rewriteIgnoreKeyword( $options );
559 
560  return $options;
561  }
562 
567  private function rewriteIgnoreKeyword( $options ) {
568  # SQLite uses OR IGNORE not just IGNORE
569  foreach ( $options as $k => $v ) {
570  if ( $v == 'IGNORE' ) {
571  $options[$k] = 'OR IGNORE';
572  }
573  }
574 
575  return $options;
576  }
577 
579  return [ 'INSERT OR IGNORE INTO', '' ];
580  }
581 
582  protected function doReplace( $table, array $identityKey, array $rows, $fname ) {
583  $encTable = $this->tableName( $table );
584  list( $sqlColumns, $sqlTuples ) = $this->makeInsertLists( $rows );
585  // https://sqlite.org/lang_insert.html
586  $this->query(
587  "REPLACE INTO $encTable ($sqlColumns) VALUES $sqlTuples",
588  $fname,
589  self::QUERY_CHANGE_ROWS
590  );
591  }
592 
601  public function textFieldSize( $table, $field ) {
602  return -1;
603  }
604 
608  public function wasDeadlock() {
609  return $this->lastErrno() == 5; // SQLITE_BUSY
610  }
611 
615  public function wasReadOnlyError() {
616  return $this->lastErrno() == 8; // SQLITE_READONLY;
617  }
618 
619  protected function isConnectionError( $errno ) {
620  return $errno == 17; // SQLITE_SCHEMA;
621  }
622 
623  protected function isKnownStatementRollbackError( $errno ) {
624  // ON CONFLICT ROLLBACK clauses make it so that SQLITE_CONSTRAINT error is
625  // ambiguous with regard to whether it implies a ROLLBACK or an ABORT happened.
626  // https://sqlite.org/lang_createtable.html#uniqueconst
627  // https://sqlite.org/lang_conflict.html
628  return false;
629  }
630 
631  public function getTopologyBasedServerId() {
632  // Sqlite topologies trivially consist of single primary server for the dataset
633  return '0';
634  }
635 
636  public function serverIsReadOnly() {
637  $this->assertHasConnectionHandle();
638 
639  $path = $this->getDbFilePath();
640 
641  return ( !self::isProcessMemoryPath( $path ) && !is_writable( $path ) );
642  }
643 
647  public function getSoftwareLink() {
648  return "[{{int:version-db-sqlite-url}} SQLite]";
649  }
650 
654  public function getServerVersion() {
655  if ( $this->version === null ) {
656  $this->version = $this->getBindingHandle()->getAttribute( PDO::ATTR_SERVER_VERSION );
657  }
658 
659  return $this->version;
660  }
661 
670  public function fieldInfo( $table, $field ) {
671  $tableName = $this->tableName( $table );
672  $res = $this->query(
673  'PRAGMA table_info(' . $this->addQuotes( $tableName ) . ')',
674  __METHOD__,
675  self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
676  );
677  foreach ( $res as $row ) {
678  if ( $row->name == $field ) {
679  return new SQLiteField( $row, $tableName );
680  }
681  }
682 
683  return false;
684  }
685 
686  protected function doBegin( $fname = '' ) {
687  if ( $this->trxMode != '' ) {
688  $this->query( "BEGIN {$this->trxMode}", $fname, self::QUERY_CHANGE_TRX );
689  } else {
690  $this->query( 'BEGIN', $fname, self::QUERY_CHANGE_TRX );
691  }
692  }
693 
698  public function strencode( $s ) {
699  return substr( $this->addQuotes( $s ), 1, -1 );
700  }
701 
706  public function encodeBlob( $b ) {
707  return new Blob( $b );
708  }
709 
714  public function decodeBlob( $b ) {
715  if ( $b instanceof Blob ) {
716  $b = $b->fetch();
717  }
718 
719  return $b;
720  }
721 
726  public function addQuotes( $s ) {
727  if ( $s instanceof Blob ) {
728  return "x'" . bin2hex( $s->fetch() ) . "'";
729  } elseif ( is_bool( $s ) ) {
730  return (string)(int)$s;
731  } elseif ( is_int( $s ) ) {
732  return (string)$s;
733  } elseif ( strpos( (string)$s, "\0" ) !== false ) {
734  // SQLite doesn't support \0 in strings, so use the hex representation as a workaround.
735  // This is a known limitation of SQLite's mprintf function which PDO
736  // should work around, but doesn't. I have reported this to php.net as bug #63419:
737  // https://bugs.php.net/bug.php?id=63419
738  // There was already a similar report for SQLite3::escapeString, bug #62361:
739  // https://bugs.php.net/bug.php?id=62361
740  // There is an additional bug regarding sorting this data after insert
741  // on older versions of sqlite shipped with ubuntu 12.04
742  // https://phabricator.wikimedia.org/T74367
743  $this->queryLogger->debug(
744  __FUNCTION__ .
745  ': Quoting value containing null byte. ' .
746  'For consistency all binary data should have been ' .
747  'first processed with self::encodeBlob()'
748  );
749  return "x'" . bin2hex( (string)$s ) . "'";
750  } else {
751  return $this->getBindingHandle()->quote( (string)$s );
752  }
753  }
754 
761  public function deadlockLoop( ...$args ) {
762  $function = array_shift( $args );
763 
764  return $function( ...$args );
765  }
766 
771  protected function replaceVars( $s ) {
772  $s = parent::replaceVars( $s );
773  if ( preg_match( '/^\s*(CREATE|ALTER) TABLE/i', $s ) ) {
774  // CREATE TABLE hacks to allow schema file sharing with MySQL
775 
776  // binary/varbinary column type -> blob
777  $s = preg_replace( '/\b(var)?binary(\‍(\d+\‍))/i', 'BLOB', $s );
778  // no such thing as unsigned
779  $s = preg_replace( '/\b(un)?signed\b/i', '', $s );
780  // INT -> INTEGER
781  $s = preg_replace( '/\b(tiny|small|medium|big|)int(\s*\‍(\s*\d+\s*\‍)|\b)/i', 'INTEGER', $s );
782  // floating point types -> REAL
783  $s = preg_replace(
784  '/\b(float|double(\s+precision)?)(\s*\‍(\s*\d+\s*(,\s*\d+\s*)?\‍)|\b)/i',
785  'REAL',
786  $s
787  );
788  // varchar -> TEXT
789  $s = preg_replace( '/\b(var)?char\s*\‍(.*?\‍)/i', 'TEXT', $s );
790  // TEXT normalization
791  $s = preg_replace( '/\b(tiny|medium|long)text\b/i', 'TEXT', $s );
792  // BLOB normalization
793  $s = preg_replace( '/\b(tiny|small|medium|long|)blob\b/i', 'BLOB', $s );
794  // BOOL -> INTEGER
795  $s = preg_replace( '/\bbool(ean)?\b/i', 'INTEGER', $s );
796  // DATETIME -> TEXT
797  $s = preg_replace( '/\b(datetime|timestamp)\b/i', 'TEXT', $s );
798  // No ENUM type
799  $s = preg_replace( '/\benum\s*\‍([^)]*\‍)/i', 'TEXT', $s );
800  // binary collation type -> nothing
801  $s = preg_replace( '/\bbinary\b/i', '', $s );
802  // auto_increment -> autoincrement
803  $s = preg_replace( '/\bauto_increment\b/i', 'AUTOINCREMENT', $s );
804  // No explicit options
805  $s = preg_replace( '/\‍)[^);]*(;?)\s*$/', ')\1', $s );
806  // AUTOINCREMENT should immediately follow PRIMARY KEY
807  $s = preg_replace( '/primary key (.*?) autoincrement/i', 'PRIMARY KEY AUTOINCREMENT $1', $s );
808  } elseif ( preg_match( '/^\s*CREATE (\s*(?:UNIQUE|FULLTEXT)\s+)?INDEX/i', $s ) ) {
809  // No truncated indexes
810  $s = preg_replace( '/\‍(\d+\‍)/', '', $s );
811  // No FULLTEXT
812  $s = preg_replace( '/\bfulltext\b/i', '', $s );
813  } elseif ( preg_match( '/^\s*DROP INDEX/i', $s ) ) {
814  // DROP INDEX is database-wide, not table-specific, so no ON <table> clause.
815  $s = preg_replace( '/\sON\s+[^\s]*/i', '', $s );
816  } elseif ( preg_match( '/^\s*INSERT IGNORE\b/i', $s ) ) {
817  // INSERT IGNORE --> INSERT OR IGNORE
818  $s = preg_replace( '/^\s*INSERT IGNORE\b/i', 'INSERT OR IGNORE', $s );
819  }
820 
821  return $s;
822  }
823 
824  public function doLockIsFree( string $lockName, string $method ) {
825  // Only locks by this thread will be checked
826  return true;
827  }
828 
829  public function doLock( string $lockName, string $method, int $timeout ) {
830  $status = $this->lockMgr->lock( [ $lockName ], LockManager::LOCK_EX, $timeout );
831  if (
832  $this->lockMgr instanceof FSLockManager &&
833  $status->hasMessage( 'lockmanager-fail-openlock' )
834  ) {
835  throw new DBError( $this, "Cannot create directory \"{$this->getLockFileDirectory()}\"" );
836  }
837 
838  return $status->isOK() ? microtime( true ) : null;
839  }
840 
841  public function doUnlock( string $lockName, string $method ) {
842  return $this->lockMgr->unlock( [ $lockName ], LockManager::LOCK_EX )->isGood();
843  }
844 
845  public function buildGroupConcatField(
846  $delim, $table, $field, $conds = '', $join_conds = []
847  ) {
848  $fld = "group_concat($field," . $this->addQuotes( $delim ) . ')';
849 
850  return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
851  }
852 
861  public function duplicateTableStructure(
862  $oldName, $newName, $temporary = false, $fname = __METHOD__
863  ) {
864  $res = $this->query(
865  "SELECT sql FROM sqlite_master WHERE tbl_name=" .
866  $this->addQuotes( $oldName ) . " AND type='table'",
867  $fname,
868  self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
869  );
870  $obj = $res->fetchObject();
871  if ( !$obj ) {
872  throw new RuntimeException( "Couldn't retrieve structure for table $oldName" );
873  }
874  $sqlCreateTable = $obj->sql;
875  $sqlCreateTable = preg_replace(
876  '/(?<=\W)"?' .
877  preg_quote( trim( $this->platform->addIdentifierQuotes( $oldName ), '"' ), '/' ) .
878  '"?(?=\W)/',
879  $this->platform->addIdentifierQuotes( $newName ),
880  $sqlCreateTable,
881  1
882  );
883  if ( $temporary ) {
884  if ( preg_match( '/^\\s*CREATE\\s+VIRTUAL\\s+TABLE\b/i', $sqlCreateTable ) ) {
885  $this->queryLogger->debug(
886  "Table $oldName is virtual, can't create a temporary duplicate." );
887  } else {
888  $sqlCreateTable = str_replace(
889  'CREATE TABLE',
890  'CREATE TEMPORARY TABLE',
891  $sqlCreateTable
892  );
893  }
894  }
895 
896  // @phan-suppress-next-line SecurityCheck-SQLInjection SQL is taken from database
897  $res = $this->query(
898  $sqlCreateTable,
899  $fname,
900  self::QUERY_CHANGE_SCHEMA | self::QUERY_PSEUDO_PERMANENT
901  );
902 
903  // Take over indexes
904  $indexList = $this->query(
905  'PRAGMA INDEX_LIST(' . $this->addQuotes( $oldName ) . ')',
906  $fname,
907  self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
908  );
909  foreach ( $indexList as $index ) {
910  if ( strpos( $index->name, 'sqlite_autoindex' ) === 0 ) {
911  continue;
912  }
913 
914  if ( $index->unique ) {
915  $sqlIndex = 'CREATE UNIQUE INDEX';
916  } else {
917  $sqlIndex = 'CREATE INDEX';
918  }
919  // Try to come up with a new index name, given indexes have database scope in SQLite
920  $indexName = $newName . '_' . $index->name;
921  $sqlIndex .= ' ' . $this->platform->addIdentifierQuotes( $indexName ) .
922  ' ON ' . $this->platform->addIdentifierQuotes( $newName );
923 
924  $indexInfo = $this->query(
925  'PRAGMA INDEX_INFO(' . $this->addQuotes( $index->name ) . ')',
926  $fname,
927  self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
928  );
929  $fields = [];
930  foreach ( $indexInfo as $indexInfoRow ) {
931  $fields[$indexInfoRow->seqno] = $this->addQuotes( $indexInfoRow->name );
932  }
933 
934  $sqlIndex .= '(' . implode( ',', $fields ) . ')';
935 
936  $this->query(
937  $sqlIndex,
938  __METHOD__,
939  self::QUERY_CHANGE_SCHEMA | self::QUERY_PSEUDO_PERMANENT
940  );
941  }
942 
943  return $res;
944  }
945 
954  public function listTables( $prefix = null, $fname = __METHOD__ ) {
955  $result = $this->query(
956  "SELECT name FROM sqlite_master WHERE type = 'table'",
957  $fname,
958  self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
959  );
960 
961  $endArray = [];
962 
963  foreach ( $result as $table ) {
964  $vars = get_object_vars( $table );
965  $table = array_pop( $vars );
966 
967  if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
968  if ( strpos( $table, 'sqlite_' ) !== 0 ) {
969  $endArray[] = $table;
970  }
971  }
972  }
973 
974  return $endArray;
975  }
976 
977  public function dropTable( $table, $fname = __METHOD__ ) {
978  if ( !$this->tableExists( $table, $fname ) ) {
979  return false;
980  }
981 
982  // No CASCADE support; https://www.sqlite.org/lang_droptable.html
983  $sql = "DROP TABLE " . $this->tableName( $table );
984  $this->query( $sql, $fname, self::QUERY_CHANGE_SCHEMA );
985 
986  return true;
987  }
988 
989  protected function doTruncate( array $tables, $fname ) {
990  $this->startAtomic( $fname );
991 
992  $encSeqNames = [];
993  foreach ( $tables as $table ) {
994  // Use "truncate" optimization; https://www.sqlite.org/lang_delete.html
995  $sql = "DELETE FROM " . $this->tableName( $table );
996  $this->query( $sql, $fname, self::QUERY_CHANGE_SCHEMA );
997 
998  $encSeqNames[] = $this->addQuotes( $this->tableName( $table, 'raw' ) );
999  }
1000 
1001  $encMasterTable = $this->platform->addIdentifierQuotes( 'sqlite_sequence' );
1002  $this->query(
1003  "DELETE FROM $encMasterTable WHERE name IN(" . implode( ',', $encSeqNames ) . ")",
1004  $fname,
1005  self::QUERY_CHANGE_SCHEMA
1006  );
1007 
1008  $this->endAtomic( $fname );
1009  }
1010 
1011  public function setTableAliases( array $aliases ) {
1012  parent::setTableAliases( $aliases );
1013  if ( $this->isOpen() ) {
1014  $this->attachDatabasesFromTableAliases();
1015  }
1016  }
1017 
1021  private function attachDatabasesFromTableAliases() {
1022  foreach ( $this->tableAliases as $params ) {
1023  if (
1024  $params['dbname'] !== $this->getDBname() &&
1025  !isset( $this->sessionAttachedDbs[$params['dbname']] )
1026  ) {
1027  $this->attachDatabase( $params['dbname'], false, __METHOD__ );
1028  $this->sessionAttachedDbs[$params['dbname']] = true;
1029  }
1030  }
1031  }
1032 
1033  public function databasesAreIndependent() {
1034  return true;
1035  }
1036 
1037  protected function doHandleSessionLossPreconnect() {
1038  $this->sessionAttachedDbs = [];
1039  // Release all locks, via FSLockManager::__destruct, as the base class expects;
1040  $this->lockMgr = null;
1041  // Create a new lock manager instance
1042  $this->lockMgr = $this->makeLockManager();
1043  }
1044 
1045  protected function doFlushSession( $fname ) {
1046  // Release all locks, via FSLockManager::__destruct, as the base class expects
1047  $this->lockMgr = null;
1048  // Create a new lock manager instance
1049  $this->lockMgr = $this->makeLockManager();
1050  }
1051 
1055  protected function getBindingHandle() {
1056  return parent::getBindingHandle();
1057  }
1058 }
1059 
1063 class_alias( DatabaseSqlite::class, 'DatabaseSqlite' );
Simple version of LockManager based on using FS lock files.
Class for handling resource locking.
Definition: LockManager.php:49
const LOCK_EX
Definition: LockManager.php:72
Simple version of LockManager that only does lock reference counting.
Database error base class.
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 If an error occurs, {query} 1....
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)
makeLockManager()
Initialize/reset the LockManager instance.
__construct(array $params)
Additional params include:
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.
isConnectionError( $errno)
Do not use this method outside of Database/DBError classes.
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.
tableExists( $table, $fname=__METHOD__)
Query whether a given table exists.
doSelectDomain(DatabaseDomain $domain)
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...
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)
doFlushSession( $fname)
Reset the server-side session state for named locks and table locks.
LockManager null $lockMgr
(hopefully on the same server as the DB)
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.
Relational database abstraction object.
Definition: Database.php:52
string null $password
Password used to establish the current connection.
Definition: Database.php:87
newExceptionAfterConnectError( $error)
Definition: Database.php:1756
getDomainID()
Return the currently selected domain ID.
Definition: Database.php:781
int $flags
Current bit field of class DBO_* constants.
Definition: Database.php:106
query( $sql, $fname=__METHOD__, $flags=self::QUERY_NORMAL)
Run an SQL query statement and return the result.
Definition: Database.php:1191
getLogContext(array $extras=[])
Create a log context to pass to PSR-3 logger functions.
Definition: Database.php:860
string null $server
Server that this instance is currently connected to.
Definition: Database.php:83
close( $fname=__METHOD__)
Close the database connection.
Definition: Database.php:871
static factory( $type, $params=[], $connect=self::NEW_CONNECTED)
Construct a Database subclass instance given a database type and parameters.
Definition: Database.php:384
getFlag( $flag)
Returns a boolean whether the flag $flag is set for this connection.
Definition: Database.php:777
string null $user
User that this instance is currently connected under the name of.
Definition: Database.php:85
getDBname()
Get the current database name; null if there isn't one.
Definition: Database.php:2784
Interface for query language.
if( $line===false) $args
Definition: mcc.php:124
foreach( $mmfl['setupFiles'] as $fileName) if( $queue) if(empty( $mmfl['quiet'])) $s
const DBO_DEFAULT
Definition: defines.php:13
const DBO_PERSISTENT
Definition: defines.php:14
const DBO_TRX
Definition: defines.php:12
return true
Definition: router.php:90
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42