Merge "Fix use of GenderCache in ApiPageSet::processTitlesArray"
[lhc/web/wiklou.git] / includes / libs / rdbms / database / DatabaseSqlite.php
index 11dda2f..0e2dc8f 100644 (file)
@@ -55,10 +55,10 @@ class DatabaseSqlite extends Database {
        protected $lockMgr;
 
        /** @var array List of shared database already attached to this connection */
-       private $alreadyAttached = [];
+       private $sessionAttachedDbs = [];
 
-       /** @var bool Whether full text is enabled */
-       private static $fulltextEnabled = null;
+       /** @var string[] See https://www.sqlite.org/lang_transaction.html */
+       private static $VALID_TRX_MODES = [ '', 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ];
 
        /**
         * Additional params include:
@@ -77,8 +77,7 @@ class DatabaseSqlite extends Database {
                        $this->dbDir = $p['dbDirectory'];
                }
 
-               // Set a dummy user to make initConnection() trigger open()
-               parent::__construct( [ 'user' => '@' ] + $p );
+               parent::__construct( $p );
 
                $this->trxMode = strtoupper( $p['trxMode'] ?? '' );
 
@@ -123,22 +122,13 @@ class DatabaseSqlite extends Database {
                return 'sqlite';
        }
 
-       /**
-        * @todo Check if it should be true like parent class
-        *
-        * @return bool
-        */
-       public function implicitGroupby() {
-               return false;
-       }
-
        protected function open( $server, $user, $pass, $dbName, $schema, $tablePrefix ) {
                $this->close();
 
                // Note that for SQLite, $server, $user, and $pass are ignored
 
                if ( $schema !== null ) {
-                       throw new DBExpectedError( $this, __CLASS__ . ": cannot use schemas ('$schema')" );
+                       throw $this->newExceptionAfterConnectError( "Got schema '$schema'; not supported." );
                }
 
                if ( $this->dbPath !== null ) {
@@ -146,59 +136,55 @@ class DatabaseSqlite extends Database {
                } elseif ( $this->dbDir !== null ) {
                        $path = self::generateFileName( $this->dbDir, $dbName );
                } else {
-                       throw new DBExpectedError( $this, __CLASS__ . ": DB path or directory required" );
+                       throw $this->newExceptionAfterConnectError( "DB path or directory required" );
                }
 
-               if ( !in_array( $this->trxMode, [ '', 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ], true ) ) {
-                       throw new DBExpectedError(
-                               $this,
-                               __CLASS__ . ": invalid transaction mode '{$this->trxMode}'"
-                       );
+               // Check if the database file already exists but is non-readable
+               if (
+                       !self::isProcessMemoryPath( $path ) &&
+                       file_exists( $path ) &&
+                       !is_readable( $path )
+               ) {
+                       throw $this->newExceptionAfterConnectError( 'SQLite database file is not readable' );
+               } elseif ( !in_array( $this->trxMode, self::$VALID_TRX_MODES, true ) ) {
+                       throw $this->newExceptionAfterConnectError( "Got mode '{$this->trxMode}' for BEGIN" );
                }
 
-               if ( !self::isProcessMemoryPath( $path ) && !is_readable( $path ) ) {
-                       $error = "SQLite database file not readable";
-                       $this->connLogger->error(
-                               "Error connecting to {db_server}: {error}",
-                               $this->getLogContext( [ 'method' => __METHOD__, 'error' => $error ] )
-                       );
-                       throw new DBConnectionError( $this, $error );
+               $attributes = [];
+               if ( $this->getFlag( self::DBO_PERSISTENT ) ) {
+                       // Persistent connections can avoid some schema index reading overhead.
+                       // On the other hand, they can cause horrible contention with DBO_TRX.
+                       if ( $this->getFlag( self::DBO_TRX ) || $this->getFlag( self::DBO_DEFAULT ) ) {
+                               $this->connLogger->warning(
+                                       __METHOD__ . ": ignoring DBO_PERSISTENT due to DBO_TRX or DBO_DEFAULT",
+                                       $this->getLogContext()
+                               );
+                       } else {
+                               $attributes[PDO::ATTR_PERSISTENT] = true;
+                       }
                }
 
                try {
-                       $conn = new PDO(
-                               "sqlite:$path",
-                               '',
-                               '',
-                               [ PDO::ATTR_PERSISTENT => (bool)( $this->flags & self::DBO_PERSISTENT ) ]
-                       );
-                       // Set error codes only, don't raise exceptions
-                       $conn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
+                       // Open the database file, creating it if it does not yet exist
+                       $this->conn = new PDO( "sqlite:$path", null, null, $attributes );
                } catch ( PDOException $e ) {
-                       $error = $e->getMessage();
-                       $this->connLogger->error(
-                               "Error connecting to {db_server}: {error}",
-                               $this->getLogContext( [ 'method' => __METHOD__, 'error' => $error ] )
-                       );
-                       throw new DBConnectionError( $this, $error );
+                       throw $this->newExceptionAfterConnectError( $e->getMessage() );
                }
 
-               $this->conn = $conn;
                $this->currentDomain = new DatabaseDomain( $dbName, null, $tablePrefix );
 
                try {
                        $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_NO_RETRY;
                        // Enforce LIKE to be case sensitive, just like MySQL
                        $this->query( 'PRAGMA case_sensitive_like = 1', __METHOD__, $flags );
-                       // Apply an optimizations or requirements regarding fsync() usage
+                       // Apply optimizations or requirements regarding fsync() usage
                        $sync = $this->connectionVariables['synchronous'] ?? null;
                        if ( in_array( $sync, [ 'EXTRA', 'FULL', 'NORMAL', 'OFF' ], true ) ) {
                                $this->query( "PRAGMA synchronous = $sync", __METHOD__, $flags );
                        }
+                       $this->attachDatabasesFromTableAliases();
                } catch ( Exception $e ) {
-                       // Connection was not fully initialized and is not safe for use
-                       $this->conn = false;
-                       throw $e;
+                       throw $this->newExceptionAfterConnectError( $e->getMessage() );
                }
        }
 
@@ -281,28 +267,6 @@ class DatabaseSqlite extends Database {
                return preg_match( '/^(:memory:$|file:(:memory:|[^?]+\?mode=memory(&|$)))/', $path );
        }
 
-       /**
-        * Check if the searchindext table is FTS enabled.
-        * @return bool False if not enabled.
-        */
-       public function checkForEnabledSearch() {
-               if ( self::$fulltextEnabled === null ) {
-                       self::$fulltextEnabled = false;
-                       $table = $this->tableName( 'searchindex' );
-                       $res = $this->query(
-                               "SELECT sql FROM sqlite_master WHERE tbl_name = '$table'",
-                               __METHOD__,
-                               self::QUERY_IGNORE_DBO_TRX
-                       );
-                       if ( $res ) {
-                               $row = $res->fetchRow();
-                               self::$fulltextEnabled = stristr( $row['sql'], 'fts' ) !== false;
-                       }
-               }
-
-               return self::$fulltextEnabled;
-       }
-
        /**
         * Returns version of currently supported SQLite fulltext search module or false if none present.
         * @return string
@@ -325,8 +289,9 @@ class DatabaseSqlite extends Database {
        }
 
        /**
-        * Attaches external database to our connection, see https://sqlite.org/lang_attach.html
-        * for details.
+        * Attaches external database to the connection handle
+        *
+        * @see https://sqlite.org/lang_attach.html
         *
         * @param string $name Database name to be used in queries like
         *   SELECT foo FROM dbname.table
@@ -469,6 +434,36 @@ class DatabaseSqlite extends Database {
                return false;
        }
 
+       protected function doSelectDomain( DatabaseDomain $domain ) {
+               if ( $domain->getSchema() !== null ) {
+                       throw new DBExpectedError(
+                               $this,
+                               __CLASS__ . ": domain '{$domain->getId()}' has a schema component"
+                       );
+               }
+
+               $database = $domain->getDatabase();
+               // A null database means "don't care" so leave it as is and update the table prefix
+               if ( $database === null ) {
+                       $this->currentDomain = new DatabaseDomain(
+                               $this->currentDomain->getDatabase(),
+                               null,
+                               $domain->getTablePrefix()
+                       );
+
+                       return true;
+               }
+
+               if ( $database !== $this->getDBname() ) {
+                       throw new DBExpectedError(
+                               $this,
+                               __CLASS__ . ": cannot change database (got '$database')"
+                       );
+               }
+
+               return true;
+       }
+
        /**
         * Use MySQL's naming (accounts for prefix etc) but remove surrounding backticks
         *
@@ -608,15 +603,10 @@ class DatabaseSqlite extends Database {
                return in_array( 'UNIQUE', $options );
        }
 
-       /**
-        * Filter the options used in SELECT statements
-        *
-        * @param array $options
-        * @return array
-        */
-       function makeSelectOptions( $options ) {
+       protected function makeSelectOptions( array $options ) {
+               // Remove problematic options that the base implementation converts to SQL
                foreach ( $options as $k => $v ) {
-                       if ( is_numeric( $k ) && ( $v == 'FOR UPDATE' || $v == 'LOCK IN SHARE MODE' ) ) {
+                       if ( is_numeric( $k ) && ( $v === 'FOR UPDATE' || $v === 'LOCK IN SHARE MODE' ) ) {
                                $options[$k] = '';
                        }
                }
@@ -783,6 +773,8 @@ class DatabaseSqlite extends Database {
        }
 
        public function serverIsReadOnly() {
+               $this->assertHasConnectionHandle();
+
                $path = $this->getDbFilePath();
 
                return ( !self::isProcessMemoryPath( $path ) && !is_writable( $path ) );
@@ -1128,12 +1120,23 @@ class DatabaseSqlite extends Database {
 
        public function setTableAliases( array $aliases ) {
                parent::setTableAliases( $aliases );
+               if ( $this->isOpen() ) {
+                       $this->attachDatabasesFromTableAliases();
+               }
+       }
+
+       /**
+        * Issue ATTATCH statements for all unattached foreign DBs in table aliases
+        */
+       private function attachDatabasesFromTableAliases() {
                foreach ( $this->tableAliases as $params ) {
-                       if ( isset( $this->alreadyAttached[$params['dbname']] ) ) {
-                               continue;
+                       if (
+                               $params['dbname'] !== $this->getDBname() &&
+                               !isset( $this->sessionAttachedDbs[$params['dbname']] )
+                       ) {
+                               $this->attachDatabase( $params['dbname'] );
+                               $this->sessionAttachedDbs[$params['dbname']] = true;
                        }
-                       $this->attachDatabase( $params['dbname'] );
-                       $this->alreadyAttached[$params['dbname']] = true;
                }
        }
 
@@ -1151,6 +1154,10 @@ class DatabaseSqlite extends Database {
                return true;
        }
 
+       protected function doHandleSessionLossPreconnect() {
+               $this->sessionAttachedDbs = [];
+       }
+
        /**
         * @return PDO
         */