MediaWiki  master
DatabaseSqlite.php
Go to the documentation of this file.
1 <?php
24 namespace Wikimedia\Rdbms;
25 
26 use NullLockManager;
27 use PDO;
28 use PDOException;
29 use Exception;
30 use LockManager;
31 use FSLockManager;
33 use stdClass;
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 static $VALID_TRX_MODES = [ '', 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ];
63 
71  public function __construct( array $params ) {
72  if ( isset( $params['dbFilePath'] ) ) {
73  $this->dbPath = $params['dbFilePath'];
74  if ( !strlen( $params['dbname'] ) ) {
75  $params['dbname'] = self::generateDatabaseName( $this->dbPath );
76  }
77  } elseif ( isset( $params['dbDirectory'] ) ) {
78  $this->dbDir = $params['dbDirectory'];
79  }
80 
81  parent::__construct( $params );
82 
83  $this->trxMode = strtoupper( $params['trxMode'] ?? '' );
84 
85  $lockDirectory = $this->getLockFileDirectory();
86  if ( $lockDirectory !== null ) {
87  $this->lockMgr = new FSLockManager( [
88  'domain' => $this->getDomainID(),
89  'lockDirectory' => $lockDirectory
90  ] );
91  } else {
92  $this->lockMgr = new NullLockManager( [ 'domain' => $this->getDomainID() ] );
93  }
94  }
95 
96  protected static function getAttributes() {
97  return [
98  self::ATTR_DB_IS_FILE => true,
99  self::ATTR_DB_LEVEL_LOCKING => true
100  ];
101  }
102 
112  public static function newStandaloneInstance( $filename, array $p = [] ) {
113  $p['dbFilePath'] = $filename;
114  $p['schema'] = null;
115  $p['tablePrefix'] = '';
117  $db = Database::factory( 'sqlite', $p );
118 
119  return $db;
120  }
121 
125  public function getType() {
126  return 'sqlite';
127  }
128 
129  protected function open( $server, $user, $pass, $dbName, $schema, $tablePrefix ) {
130  $this->close();
131 
132  // Note that for SQLite, $server, $user, and $pass are ignored
133 
134  if ( $schema !== null ) {
135  throw $this->newExceptionAfterConnectError( "Got schema '$schema'; not supported." );
136  }
137 
138  if ( $this->dbPath !== null ) {
140  } elseif ( $this->dbDir !== null ) {
141  $path = self::generateFileName( $this->dbDir, $dbName );
142  } else {
143  throw $this->newExceptionAfterConnectError( "DB path or directory required" );
144  }
145 
146  // Check if the database file already exists but is non-readable
147  if (
148  !self::isProcessMemoryPath( $path ) &&
149  file_exists( $path ) &&
150  !is_readable( $path )
151  ) {
152  throw $this->newExceptionAfterConnectError( 'SQLite database file is not readable' );
153  } elseif ( !in_array( $this->trxMode, self::$VALID_TRX_MODES, true ) ) {
154  throw $this->newExceptionAfterConnectError( "Got mode '{$this->trxMode}' for BEGIN" );
155  }
156 
157  $this->server = 'localhost';
158 
159  $attributes = [];
160  if ( $this->getFlag( self::DBO_PERSISTENT ) ) {
161  // Persistent connections can avoid some schema index reading overhead.
162  // On the other hand, they can cause horrible contention with DBO_TRX.
163  if ( $this->getFlag( self::DBO_TRX ) || $this->getFlag( self::DBO_DEFAULT ) ) {
164  $this->connLogger->warning(
165  __METHOD__ . ": ignoring DBO_PERSISTENT due to DBO_TRX or DBO_DEFAULT",
166  $this->getLogContext()
167  );
168  } else {
169  $attributes[PDO::ATTR_PERSISTENT] = true;
170  }
171  }
172 
173  try {
174  // Open the database file, creating it if it does not yet exist
175  $this->conn = new PDO( "sqlite:$path", null, null, $attributes );
176  } catch ( PDOException $e ) {
177  throw $this->newExceptionAfterConnectError( $e->getMessage() );
178  }
179 
180  $this->currentDomain = new DatabaseDomain( $dbName, null, $tablePrefix );
181 
182  try {
183  $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_NO_RETRY;
184  // Enforce LIKE to be case sensitive, just like MySQL
185  $this->query( 'PRAGMA case_sensitive_like = 1', __METHOD__, $flags );
186  // Apply optimizations or requirements regarding fsync() usage
187  $sync = $this->connectionVariables['synchronous'] ?? null;
188  if ( in_array( $sync, [ 'EXTRA', 'FULL', 'NORMAL', 'OFF' ], true ) ) {
189  $this->query( "PRAGMA synchronous = $sync", __METHOD__, $flags );
190  }
192  } catch ( Exception $e ) {
193  throw $this->newExceptionAfterConnectError( $e->getMessage() );
194  }
195  }
196 
202  public function getDbFilePath() {
203  return $this->dbPath ?? self::generateFileName( $this->dbDir, $this->getDBname() );
204  }
205 
209  public function getLockFileDirectory() {
210  if ( $this->dbPath !== null && !self::isProcessMemoryPath( $this->dbPath ) ) {
211  return dirname( $this->dbPath ) . '/locks';
212  } elseif ( $this->dbDir !== null && !self::isProcessMemoryPath( $this->dbDir ) ) {
213  return $this->dbDir . '/locks';
214  }
215 
216  return null;
217  }
218 
223  protected function closeConnection() {
224  $this->conn = null;
225 
226  return true;
227  }
228 
236  public static function generateFileName( $dir, $dbName ) {
237  if ( $dir == '' ) {
238  throw new DBUnexpectedError( null, __CLASS__ . ": no DB directory specified" );
239  } elseif ( self::isProcessMemoryPath( $dir ) ) {
240  throw new DBUnexpectedError(
241  null,
242  __CLASS__ . ": cannot use process memory directory '$dir'"
243  );
244  } elseif ( !strlen( $dbName ) ) {
245  throw new DBUnexpectedError( null, __CLASS__ . ": no DB name specified" );
246  }
247 
248  return "$dir/$dbName.sqlite";
249  }
250 
255  private static function generateDatabaseName( $path ) {
256  if ( preg_match( '/^(:memory:$|file::memory:)/', $path ) ) {
257  // E.g. "file::memory:?cache=shared" => ":memory":
258  return ':memory:';
259  } elseif ( preg_match( '/^file::([^?]+)\?mode=memory(&|$)/', $path, $m ) ) {
260  // E.g. "file:memdb1?mode=memory" => ":memdb1:"
261  return ":{$m[1]}:";
262  } else {
263  // E.g. "/home/.../some_db.sqlite3" => "some_db"
264  return preg_replace( '/\.sqlite\d?$/', '', basename( $path ) );
265  }
266  }
267 
272  private static function isProcessMemoryPath( $path ) {
273  return preg_match( '/^(:memory:$|file:(:memory:|[^?]+\?mode=memory(&|$)))/', $path );
274  }
275 
280  static function getFulltextSearchModule() {
281  static $cachedResult = null;
282  if ( $cachedResult !== null ) {
283  return $cachedResult;
284  }
285  $cachedResult = false;
286  $table = 'dummy_search_test';
287 
288  $db = self::newStandaloneInstance( ':memory:' );
289  if ( $db->query( "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)", __METHOD__, true ) ) {
290  $cachedResult = 'FTS3';
291  }
292  $db->close();
293 
294  return $cachedResult;
295  }
296 
309  public function attachDatabase( $name, $file = false, $fname = __METHOD__ ) {
310  $file = is_string( $file ) ? $file : self::generateFileName( $this->dbDir, $name );
311  $encFile = $this->addQuotes( $file );
312 
313  return $this->query(
314  "ATTACH DATABASE $encFile AS $name",
315  $fname,
316  self::QUERY_IGNORE_DBO_TRX
317  );
318  }
319 
320  protected function isWriteQuery( $sql ) {
321  return parent::isWriteQuery( $sql ) && !preg_match( '/^(ATTACH|PRAGMA)\b/i', $sql );
322  }
323 
324  protected function isTransactableQuery( $sql ) {
325  return parent::isTransactableQuery( $sql ) && !in_array(
326  $this->getQueryVerb( $sql ),
327  [ 'ATTACH', 'PRAGMA' ],
328  true
329  );
330  }
331 
338  protected function doQuery( $sql ) {
339  $res = $this->getBindingHandle()->query( $sql );
340  if ( $res === false ) {
341  return false;
342  }
343 
344  $resource = ResultWrapper::unwrap( $res );
345  $this->lastAffectedRowCount = $resource->rowCount();
346  $res = new ResultWrapper( $this, $resource->fetchAll() );
347 
348  return $res;
349  }
350 
354  function freeResult( $res ) {
355  if ( $res instanceof ResultWrapper ) {
356  $res->free();
357  }
358  }
359 
364  function fetchObject( $res ) {
365  $resource =& ResultWrapper::unwrap( $res );
366 
367  $cur = current( $resource );
368  if ( is_array( $cur ) ) {
369  next( $resource );
370  $obj = new stdClass;
371  foreach ( $cur as $k => $v ) {
372  if ( !is_numeric( $k ) ) {
373  $obj->$k = $v;
374  }
375  }
376 
377  return $obj;
378  }
379 
380  return false;
381  }
382 
387  function fetchRow( $res ) {
388  $resource =& ResultWrapper::unwrap( $res );
389  $cur = current( $resource );
390  if ( is_array( $cur ) ) {
391  next( $resource );
392 
393  return $cur;
394  }
395 
396  return false;
397  }
398 
405  function numRows( $res ) {
406  // false does not implement Countable
407  $resource = ResultWrapper::unwrap( $res );
408 
409  return is_array( $resource ) ? count( $resource ) : 0;
410  }
411 
416  function numFields( $res ) {
417  $resource = ResultWrapper::unwrap( $res );
418  if ( is_array( $resource ) && count( $resource ) > 0 ) {
419  // The size of the result array is twice the number of fields. (T67578)
420  return count( $resource[0] ) / 2;
421  } else {
422  // If the result is empty return 0
423  return 0;
424  }
425  }
426 
432  function fieldName( $res, $n ) {
433  $resource = ResultWrapper::unwrap( $res );
434  if ( is_array( $resource ) ) {
435  $keys = array_keys( $resource[0] );
436 
437  return $keys[$n];
438  }
439 
440  return false;
441  }
442 
443  protected function doSelectDomain( DatabaseDomain $domain ) {
444  if ( $domain->getSchema() !== null ) {
445  throw new DBExpectedError(
446  $this,
447  __CLASS__ . ": domain '{$domain->getId()}' has a schema component"
448  );
449  }
450 
451  $database = $domain->getDatabase();
452  // A null database means "don't care" so leave it as is and update the table prefix
453  if ( $database === null ) {
454  $this->currentDomain = new DatabaseDomain(
455  $this->currentDomain->getDatabase(),
456  null,
457  $domain->getTablePrefix()
458  );
459 
460  return true;
461  }
462 
463  if ( $database !== $this->getDBname() ) {
464  throw new DBExpectedError(
465  $this,
466  __CLASS__ . ": cannot change database (got '$database')"
467  );
468  }
469 
470  return true;
471  }
472 
480  function tableName( $name, $format = 'quoted' ) {
481  // table names starting with sqlite_ are reserved
482  if ( strpos( $name, 'sqlite_' ) === 0 ) {
483  return $name;
484  }
485 
486  return str_replace( '"', '', parent::tableName( $name, $format ) );
487  }
488 
494  function insertId() {
495  // PDO::lastInsertId yields a string :(
496  return intval( $this->getBindingHandle()->lastInsertId() );
497  }
498 
503  function dataSeek( $res, $row ) {
504  $resource =& ResultWrapper::unwrap( $res );
505  reset( $resource );
506  if ( $row > 0 ) {
507  for ( $i = 0; $i < $row; $i++ ) {
508  next( $resource );
509  }
510  }
511  }
512 
516  function lastError() {
517  if ( !is_object( $this->conn ) ) {
518  return "Cannot return last error, no db connection";
519  }
520  $e = $this->conn->errorInfo();
521 
522  return $e[2] ?? '';
523  }
524 
528  function lastErrno() {
529  if ( !is_object( $this->conn ) ) {
530  return "Cannot return last error, no db connection";
531  } else {
532  $info = $this->conn->errorInfo();
533 
534  return $info[1];
535  }
536  }
537 
541  protected function fetchAffectedRowCount() {
543  }
544 
545  function tableExists( $table, $fname = __METHOD__ ) {
546  $tableRaw = $this->tableName( $table, 'raw' );
547  if ( isset( $this->sessionTempTables[$tableRaw] ) ) {
548  return true; // already known to exist
549  }
550 
551  $encTable = $this->addQuotes( $tableRaw );
552  $res = $this->query(
553  "SELECT 1 FROM sqlite_master WHERE type='table' AND name=$encTable",
554  __METHOD__,
555  self::QUERY_IGNORE_DBO_TRX
556  );
557 
558  return $res->numRows() ? true : false;
559  }
560 
571  function indexInfo( $table, $index, $fname = __METHOD__ ) {
572  $sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')';
573  $res = $this->query( $sql, $fname, self::QUERY_IGNORE_DBO_TRX );
574  if ( !$res || $res->numRows() == 0 ) {
575  return false;
576  }
577  $info = [];
578  foreach ( $res as $row ) {
579  $info[] = $row->name;
580  }
581 
582  return $info;
583  }
584 
591  function indexUnique( $table, $index, $fname = __METHOD__ ) {
592  $row = $this->selectRow( 'sqlite_master', '*',
593  [
594  'type' => 'index',
595  'name' => $this->indexName( $index ),
596  ], $fname );
597  if ( !$row || !isset( $row->sql ) ) {
598  return null;
599  }
600 
601  // $row->sql will be of the form CREATE [UNIQUE] INDEX ...
602  $indexPos = strpos( $row->sql, 'INDEX' );
603  if ( $indexPos === false ) {
604  return null;
605  }
606  $firstPart = substr( $row->sql, 0, $indexPos );
607  $options = explode( ' ', $firstPart );
608 
609  return in_array( 'UNIQUE', $options );
610  }
611 
612  protected function makeSelectOptions( array $options ) {
613  // Remove problematic options that the base implementation converts to SQL
614  foreach ( $options as $k => $v ) {
615  if ( is_numeric( $k ) && ( $v === 'FOR UPDATE' || $v === 'LOCK IN SHARE MODE' ) ) {
616  $options[$k] = '';
617  }
618  }
619 
620  return parent::makeSelectOptions( $options );
621  }
622 
627  protected function makeUpdateOptionsArray( $options ) {
628  $options = parent::makeUpdateOptionsArray( $options );
629  $options = $this->rewriteIgnoreKeyword( $options );
630 
631  return $options;
632  }
633 
638  private function rewriteIgnoreKeyword( $options ) {
639  # SQLite uses OR IGNORE not just IGNORE
640  foreach ( $options as $k => $v ) {
641  if ( $v == 'IGNORE' ) {
642  $options[$k] = 'OR IGNORE';
643  }
644  }
645 
646  return $options;
647  }
648 
653  protected function makeInsertOptions( $options ) {
654  $options = self::rewriteIgnoreKeyword( $options );
655 
656  return parent::makeInsertOptions( $options );
657  }
658 
659  public function insert( $table, $rows, $fname = __METHOD__, $options = [] ) {
660  if ( !count( $rows ) ) {
661  return true;
662  }
663 
664  # SQLite can't handle multi-row inserts, so divide up into multiple single-row inserts
665  $multi = $this->isMultiRowArray( $rows );
666  if ( $multi ) {
667  $affectedRowCount = 0;
668  try {
669  $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
670  foreach ( $rows as $row ) {
671  parent::insert( $table, $row, "$fname/multi-row", $options );
672  $affectedRowCount += $this->affectedRows();
673  }
674  $this->endAtomic( $fname );
675  } catch ( Exception $e ) {
676  $this->cancelAtomic( $fname );
677  throw $e;
678  }
679  $this->affectedRowCount = $affectedRowCount;
680  } else {
681  parent::insert( $table, $rows, "$fname/single-row", $options );
682  }
683 
684  return true;
685  }
686 
693  function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
694  if ( !count( $rows ) ) {
695  return;
696  }
697 
698  # SQLite can't handle multi-row replaces, so divide up into multiple single-row queries
699  if ( isset( $rows[0] ) && is_array( $rows[0] ) ) {
700  $affectedRowCount = 0;
701  try {
702  $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
703  foreach ( $rows as $v ) {
704  $this->nativeReplace( $table, $v, "$fname/multi-row" );
705  $affectedRowCount += $this->affectedRows();
706  }
707  $this->endAtomic( $fname );
708  } catch ( Exception $e ) {
709  $this->cancelAtomic( $fname );
710  throw $e;
711  }
712  $this->affectedRowCount = $affectedRowCount;
713  } else {
714  $this->nativeReplace( $table, $rows, "$fname/single-row" );
715  }
716  }
717 
726  function textFieldSize( $table, $field ) {
727  return -1;
728  }
729 
734  return false;
735  }
736 
742  function unionQueries( $sqls, $all ) {
743  $glue = $all ? ' UNION ALL ' : ' UNION ';
744 
745  return implode( $glue, $sqls );
746  }
747 
751  function wasDeadlock() {
752  return $this->lastErrno() == 5; // SQLITE_BUSY
753  }
754 
758  function wasReadOnlyError() {
759  return $this->lastErrno() == 8; // SQLITE_READONLY;
760  }
761 
762  public function wasConnectionError( $errno ) {
763  return $errno == 17; // SQLITE_SCHEMA;
764  }
765 
766  protected function wasKnownStatementRollbackError() {
767  // ON CONFLICT ROLLBACK clauses make it so that SQLITE_CONSTRAINT error is
768  // ambiguous with regard to whether it implies a ROLLBACK or an ABORT happened.
769  // https://sqlite.org/lang_createtable.html#uniqueconst
770  // https://sqlite.org/lang_conflict.html
771  return false;
772  }
773 
774  public function serverIsReadOnly() {
775  $this->assertHasConnectionHandle();
776 
777  $path = $this->getDbFilePath();
778 
779  return ( !self::isProcessMemoryPath( $path ) && !is_writable( $path ) );
780  }
781 
785  public function getSoftwareLink() {
786  return "[{{int:version-db-sqlite-url}} SQLite]";
787  }
788 
792  public function getServerVersion() {
793  if ( $this->version === null ) {
794  $this->version = $this->getBindingHandle()->getAttribute( PDO::ATTR_SERVER_VERSION );
795  }
796 
797  return $this->version;
798  }
799 
808  function fieldInfo( $table, $field ) {
809  $tableName = $this->tableName( $table );
810  $sql = 'PRAGMA table_info(' . $this->addQuotes( $tableName ) . ')';
811  $res = $this->query( $sql, __METHOD__, self::QUERY_IGNORE_DBO_TRX );
812  foreach ( $res as $row ) {
813  if ( $row->name == $field ) {
814  return new SQLiteField( $row, $tableName );
815  }
816  }
817 
818  return false;
819  }
820 
821  protected function doBegin( $fname = '' ) {
822  if ( $this->trxMode != '' ) {
823  $this->query( "BEGIN {$this->trxMode}", $fname );
824  } else {
825  $this->query( 'BEGIN', $fname );
826  }
827  }
828 
833  function strencode( $s ) {
834  return substr( $this->addQuotes( $s ), 1, -1 );
835  }
836 
841  function encodeBlob( $b ) {
842  return new Blob( $b );
843  }
844 
849  function decodeBlob( $b ) {
850  if ( $b instanceof Blob ) {
851  $b = $b->fetch();
852  }
853 
854  return $b;
855  }
856 
861  function addQuotes( $s ) {
862  if ( $s instanceof Blob ) {
863  return "x'" . bin2hex( $s->fetch() ) . "'";
864  } elseif ( is_bool( $s ) ) {
865  return (string)(int)$s;
866  } elseif ( is_int( $s ) ) {
867  return (string)$s;
868  } elseif ( strpos( (string)$s, "\0" ) !== false ) {
869  // SQLite doesn't support \0 in strings, so use the hex representation as a workaround.
870  // This is a known limitation of SQLite's mprintf function which PDO
871  // should work around, but doesn't. I have reported this to php.net as bug #63419:
872  // https://bugs.php.net/bug.php?id=63419
873  // There was already a similar report for SQLite3::escapeString, bug #62361:
874  // https://bugs.php.net/bug.php?id=62361
875  // There is an additional bug regarding sorting this data after insert
876  // on older versions of sqlite shipped with ubuntu 12.04
877  // https://phabricator.wikimedia.org/T74367
878  $this->queryLogger->debug(
879  __FUNCTION__ .
880  ': Quoting value containing null byte. ' .
881  'For consistency all binary data should have been ' .
882  'first processed with self::encodeBlob()'
883  );
884  return "x'" . bin2hex( (string)$s ) . "'";
885  } else {
886  return $this->getBindingHandle()->quote( (string)$s );
887  }
888  }
889 
890  public function buildSubstring( $input, $startPosition, $length = null ) {
891  $this->assertBuildSubstringParams( $startPosition, $length );
892  $params = [ $input, $startPosition ];
893  if ( $length !== null ) {
894  $params[] = $length;
895  }
896  return 'SUBSTR(' . implode( ',', $params ) . ')';
897  }
898 
904  public function buildStringCast( $field ) {
905  return 'CAST ( ' . $field . ' AS TEXT )';
906  }
907 
914  public function deadlockLoop( ...$args ) {
915  $function = array_shift( $args );
916 
917  return $function( ...$args );
918  }
919 
924  protected function replaceVars( $s ) {
925  $s = parent::replaceVars( $s );
926  if ( preg_match( '/^\s*(CREATE|ALTER) TABLE/i', $s ) ) {
927  // CREATE TABLE hacks to allow schema file sharing with MySQL
928 
929  // binary/varbinary column type -> blob
930  $s = preg_replace( '/\b(var)?binary(\(\d+\))/i', 'BLOB', $s );
931  // no such thing as unsigned
932  $s = preg_replace( '/\b(un)?signed\b/i', '', $s );
933  // INT -> INTEGER
934  $s = preg_replace( '/\b(tiny|small|medium|big|)int(\s*\(\s*\d+\s*\)|\b)/i', 'INTEGER', $s );
935  // floating point types -> REAL
936  $s = preg_replace(
937  '/\b(float|double(\s+precision)?)(\s*\(\s*\d+\s*(,\s*\d+\s*)?\)|\b)/i',
938  'REAL',
939  $s
940  );
941  // varchar -> TEXT
942  $s = preg_replace( '/\b(var)?char\s*\(.*?\)/i', 'TEXT', $s );
943  // TEXT normalization
944  $s = preg_replace( '/\b(tiny|medium|long)text\b/i', 'TEXT', $s );
945  // BLOB normalization
946  $s = preg_replace( '/\b(tiny|small|medium|long|)blob\b/i', 'BLOB', $s );
947  // BOOL -> INTEGER
948  $s = preg_replace( '/\bbool(ean)?\b/i', 'INTEGER', $s );
949  // DATETIME -> TEXT
950  $s = preg_replace( '/\b(datetime|timestamp)\b/i', 'TEXT', $s );
951  // No ENUM type
952  $s = preg_replace( '/\benum\s*\([^)]*\)/i', 'TEXT', $s );
953  // binary collation type -> nothing
954  $s = preg_replace( '/\bbinary\b/i', '', $s );
955  // auto_increment -> autoincrement
956  $s = preg_replace( '/\bauto_increment\b/i', 'AUTOINCREMENT', $s );
957  // No explicit options
958  $s = preg_replace( '/\)[^);]*(;?)\s*$/', ')\1', $s );
959  // AUTOINCREMENT should immedidately follow PRIMARY KEY
960  $s = preg_replace( '/primary key (.*?) autoincrement/i', 'PRIMARY KEY AUTOINCREMENT $1', $s );
961  } elseif ( preg_match( '/^\s*CREATE (\s*(?:UNIQUE|FULLTEXT)\s+)?INDEX/i', $s ) ) {
962  // No truncated indexes
963  $s = preg_replace( '/\(\d+\)/', '', $s );
964  // No FULLTEXT
965  $s = preg_replace( '/\bfulltext\b/i', '', $s );
966  } elseif ( preg_match( '/^\s*DROP INDEX/i', $s ) ) {
967  // DROP INDEX is database-wide, not table-specific, so no ON <table> clause.
968  $s = preg_replace( '/\sON\s+[^\s]*/i', '', $s );
969  } elseif ( preg_match( '/^\s*INSERT IGNORE\b/i', $s ) ) {
970  // INSERT IGNORE --> INSERT OR IGNORE
971  $s = preg_replace( '/^\s*INSERT IGNORE\b/i', 'INSERT OR IGNORE', $s );
972  }
973 
974  return $s;
975  }
976 
977  public function lock( $lockName, $method, $timeout = 5 ) {
978  $status = $this->lockMgr->lock( [ $lockName ], LockManager::LOCK_EX, $timeout );
979  if (
980  $this->lockMgr instanceof FSLockManager &&
981  $status->hasMessage( 'lockmanager-fail-openlock' )
982  ) {
983  throw new DBError( $this, "Cannot create directory \"{$this->getLockFileDirectory()}\"" );
984  }
985 
986  return $status->isOK();
987  }
988 
989  public function unlock( $lockName, $method ) {
990  return $this->lockMgr->unlock( [ $lockName ], LockManager::LOCK_EX )->isGood();
991  }
992 
999  function buildConcat( $stringList ) {
1000  return '(' . implode( ') || (', $stringList ) . ')';
1001  }
1002 
1003  public function buildGroupConcatField(
1004  $delim, $table, $field, $conds = '', $join_conds = []
1005  ) {
1006  $fld = "group_concat($field," . $this->addQuotes( $delim ) . ')';
1007 
1008  return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
1009  }
1010 
1019  function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
1020  $res = $this->query(
1021  "SELECT sql FROM sqlite_master WHERE tbl_name=" .
1022  $this->addQuotes( $oldName ) . " AND type='table'",
1023  $fname
1024  );
1025  $obj = $this->fetchObject( $res );
1026  if ( !$obj ) {
1027  throw new RuntimeException( "Couldn't retrieve structure for table $oldName" );
1028  }
1029  $sql = $obj->sql;
1030  $sql = preg_replace(
1031  '/(?<=\W)"?' .
1032  preg_quote( trim( $this->addIdentifierQuotes( $oldName ), '"' ), '/' ) .
1033  '"?(?=\W)/',
1034  $this->addIdentifierQuotes( $newName ),
1035  $sql,
1036  1
1037  );
1038  if ( $temporary ) {
1039  if ( preg_match( '/^\\s*CREATE\\s+VIRTUAL\\s+TABLE\b/i', $sql ) ) {
1040  $this->queryLogger->debug(
1041  "Table $oldName is virtual, can't create a temporary duplicate.\n" );
1042  } else {
1043  $sql = str_replace( 'CREATE TABLE', 'CREATE TEMPORARY TABLE', $sql );
1044  }
1045  }
1046 
1047  $res = $this->query( $sql, $fname, self::QUERY_PSEUDO_PERMANENT );
1048 
1049  // Take over indexes
1050  $indexList = $this->query( 'PRAGMA INDEX_LIST(' . $this->addQuotes( $oldName ) . ')' );
1051  foreach ( $indexList as $index ) {
1052  if ( strpos( $index->name, 'sqlite_autoindex' ) === 0 ) {
1053  continue;
1054  }
1055 
1056  if ( $index->unique ) {
1057  $sql = 'CREATE UNIQUE INDEX';
1058  } else {
1059  $sql = 'CREATE INDEX';
1060  }
1061  // Try to come up with a new index name, given indexes have database scope in SQLite
1062  $indexName = $newName . '_' . $index->name;
1063  $sql .= ' ' . $indexName . ' ON ' . $newName;
1064 
1065  $indexInfo = $this->query( 'PRAGMA INDEX_INFO(' . $this->addQuotes( $index->name ) . ')' );
1066  $fields = [];
1067  foreach ( $indexInfo as $indexInfoRow ) {
1068  $fields[$indexInfoRow->seqno] = $indexInfoRow->name;
1069  }
1070 
1071  $sql .= '(' . implode( ',', $fields ) . ')';
1072 
1073  $this->query( $sql );
1074  }
1075 
1076  return $res;
1077  }
1078 
1087  function listTables( $prefix = null, $fname = __METHOD__ ) {
1088  $result = $this->select(
1089  'sqlite_master',
1090  'name',
1091  "type='table'"
1092  );
1093 
1094  $endArray = [];
1095 
1096  foreach ( $result as $table ) {
1097  $vars = get_object_vars( $table );
1098  $table = array_pop( $vars );
1099 
1100  if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
1101  if ( strpos( $table, 'sqlite_' ) !== 0 ) {
1102  $endArray[] = $table;
1103  }
1104  }
1105  }
1106 
1107  return $endArray;
1108  }
1109 
1118  public function dropTable( $tableName, $fName = __METHOD__ ) {
1119  if ( !$this->tableExists( $tableName, $fName ) ) {
1120  return false;
1121  }
1122  $sql = "DROP TABLE " . $this->tableName( $tableName );
1123 
1124  return $this->query( $sql, $fName, self::QUERY_IGNORE_DBO_TRX );
1125  }
1126 
1127  public function setTableAliases( array $aliases ) {
1128  parent::setTableAliases( $aliases );
1129  if ( $this->isOpen() ) {
1131  }
1132  }
1133 
1137  private function attachDatabasesFromTableAliases() {
1138  foreach ( $this->tableAliases as $params ) {
1139  if (
1140  $params['dbname'] !== $this->getDBname() &&
1141  !isset( $this->sessionAttachedDbs[$params['dbname']] )
1142  ) {
1143  $this->attachDatabase( $params['dbname'] );
1144  $this->sessionAttachedDbs[$params['dbname']] = true;
1145  }
1146  }
1147  }
1148 
1149  public function resetSequenceForTable( $table, $fname = __METHOD__ ) {
1150  $encTable = $this->addIdentifierQuotes( 'sqlite_sequence' );
1151  $encName = $this->addQuotes( $this->tableName( $table, 'raw' ) );
1152  $this->query(
1153  "DELETE FROM $encTable WHERE name = $encName",
1154  $fname,
1155  self::QUERY_IGNORE_DBO_TRX
1156  );
1157  }
1158 
1159  public function databasesAreIndependent() {
1160  return true;
1161  }
1162 
1163  protected function doHandleSessionLossPreconnect() {
1164  $this->sessionAttachedDbs = [];
1165  }
1166 
1170  protected function getBindingHandle() {
1171  return parent::getBindingHandle();
1172  }
1173 }
1174 
1178 class_alias( DatabaseSqlite::class, 'DatabaseSqlite' );
deadlockLoop(... $args)
No-op version of deadlockLoop.
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
static newStandaloneInstance( $filename, array $p=[])
close( $fname=__METHOD__, $owner=null)
Close the database connection.
Definition: Database.php:892
FSLockManager $lockMgr
(hopefully on the same server as the DB)
tableName( $name, $format='quoted')
Use MySQL&#39;s naming (accounts for prefix etc) but remove surrounding backticks.
getFlag( $flag)
Returns a boolean whether the flag $flag is set for this connection.
Definition: Database.php:802
string null $dbDir
Directory for SQLite database files listed under their DB name.
affectedRows()
Get the number of rows affected by the last write query.
Definition: Database.php:4272
resetSequenceForTable( $table, $fname=__METHOD__)
unlock( $lockName, $method)
Release a lock.
replace( $table, $uniqueIndexes, $rows, $fname=__METHOD__)
Result wrapper for grabbing data queried from an IDatabase object.
__construct(array $params)
Additional params include:
assertBuildSubstringParams( $startPosition, $length)
Check type and bounds for parameters to self::buildSubstring()
Definition: Database.php:2366
int $flags
Current bit field of class DBO_* constants.
Definition: Database.php:96
getDomainID()
Return the currently selected domain ID.
Definition: Database.php:806
startAtomic( $fname=__METHOD__, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Begin an atomic section of SQL statements.
Definition: Database.php:3834
static getFulltextSearchModule()
Returns version of currently supported SQLite fulltext search module or false if none present...
nativeReplace( $table, $rows, $fname)
REPLACE query wrapper for MySQL and SQLite, which have a native REPLACE statement.
Definition: Database.php:2926
attachDatabase( $name, $file=false, $fname=__METHOD__)
Attaches external database to the connection handle.
endAtomic( $fname=__METHOD__)
Ends an atomic section of SQL statements.
Definition: Database.php:3864
query( $sql, $fname=__METHOD__, $flags=0)
Run an SQL query and return the result.
Definition: Database.php:1155
string $server
Server that this instance is currently connected to.
Definition: Database.php:75
attachDatabasesFromTableAliases()
Issue ATTATCH statements for all unattached foreign DBs in table aliases.
insertId()
This must be called after nextSequenceVal.
newExceptionAfterConnectError( $error)
Definition: Database.php:1634
const DBO_PERSISTENT
Definition: defines.php:14
getDBname()
Get the current DB name.
Definition: Database.php:2426
listTables( $prefix=null, $fname=__METHOD__)
List all tables on the database.
textFieldSize( $table, $field)
Returns the size of a text field, or -1 for "unlimited" In SQLite this is SQLITE_MAX_LENGTH, by default 1GB.
if( $line===false) $args
Definition: mcc.php:124
doQuery( $sql)
SQLite doesn&#39;t allow buffered results or data seeking etc, so we&#39;ll use fetchAll as the result...
assertHasConnectionHandle()
Make sure there is an open connection handle (alive or not) as a sanity check.
Definition: Database.php:970
setTableAliases(array $aliases)
Make certain table names use their own database, schema, and table prefix when passed into SQL querie...
indexName( $index)
Allows for index remapping in queries where this is not consistent across DBMS.
Definition: Database.php:2751
const LOCK_EX
Definition: LockManager.php:70
fieldInfo( $table, $field)
Get information about a given field Returns false if the field does not exist.
insert( $table, $rows, $fname=__METHOD__, $options=[])
INSERT wrapper, inserts an array into a table.
integer null $affectedRowCount
Rows affected by the last query to query() or its CRUD wrappers.
Definition: Database.php:171
indexInfo( $table, $index, $fname=__METHOD__)
Returns information about an index Returns false if the index does not exist.
tableExists( $table, $fname=__METHOD__)
Query whether a given table exists.
databasesAreIndependent()
Returns true if DBs are assumed to be on potentially different servers.
selectSQLText( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Take the same arguments as IDatabase::select() and return the SQL it would use.
Definition: Database.php:1831
const DBO_TRX
Definition: defines.php:12
string null $dbPath
Explicit path for the SQLite database file.
cancelAtomic( $fname=__METHOD__, AtomicSectionIdentifier $sectionId=null)
Cancel an atomic section of SQL statements.
Definition: Database.php:3898
static generateFileName( $dir, $dbName)
Generates a database file name.
numRows( $res)
The PDO::Statement class implements the array interface so count() will work.
const DBO_DEFAULT
Definition: defines.php:13
Class to handle database/schema/prefix specifications for IDatabase.
doSelectDomain(DatabaseDomain $domain)
getLogContext(array $extras=[])
Create a log context to pass to PSR-3 logger functions.
Definition: Database.php:881
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
Wrapper to IDatabase::select() that only fetches one row (via LIMIT)
Definition: Database.php:1914
Relational database abstraction object.
Definition: Database.php:49
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
Definition: Database.php:1823
lock( $lockName, $method, $timeout=5)
Acquire a named lock.
closeConnection()
Does not actually close the connection, just destroys the reference for GC to do its work...
indexUnique( $table, $index, $fname=__METHOD__)
dropTable( $tableName, $fName=__METHOD__)
Override due to no CASCADE support.
string $user
User that this instance is currently connected under the name of.
Definition: Database.php:77
addIdentifierQuotes( $s)
Escape a SQL identifier (e.g.
Definition: Database.php:2770
buildGroupConcatField( $delim, $table, $field, $conds='', $join_conds=[])
Build a GROUP_CONCAT or equivalent statement for a query.
duplicateTableStructure( $oldName, $newName, $temporary=false, $fname=__METHOD__)
buildConcat( $stringList)
Build a concatenation list to feed into a SQL query.
open( $server, $user, $pass, $dbName, $schema, $tablePrefix)
static string [] $VALID_TRX_MODES
See https://www.sqlite.org/lang_transaction.html.
int $lastAffectedRowCount
The number of rows affected as an integer.
buildSubstring( $input, $startPosition, $length=null)
static factory( $type, $params=[], $connect=self::NEW_CONNECTED)
Construct a Database subclass instance given a database type and parameters.
Definition: Database.php:389
string $trxMode
Transaction mode.
array $sessionAttachedDbs
List of shared database already attached to this connection.
return true
Definition: router.php:92
Database error base class.
Definition: DBError.php:30
static & unwrap(&$res)
Get the underlying RDBMS driver-specific result resource.
Base class for the more common types of database errors.