Merge "Revert "Remove old remapping hacks from Database::indexName()""
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 30 Mar 2017 02:32:53 +0000 (02:32 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 30 Mar 2017 02:32:54 +0000 (02:32 +0000)
1  2 
includes/libs/rdbms/database/Database.php
includes/libs/rdbms/database/DatabaseSqlite.php

   */
  use Psr\Log\LoggerAwareInterface;
  use Psr\Log\LoggerInterface;
 +use Wikimedia\ScopedCallback;
 +use Wikimedia\Rdbms\TransactionProfiler;
 +use Wikimedia\Rdbms\LikeMatch;
 +use Wikimedia\Rdbms\DatabaseDomain;
 +use Wikimedia\Rdbms\ResultWrapper;
 +use Wikimedia\Rdbms\DBMasterPos;
 +use Wikimedia\Rdbms\Blob;
 +use Wikimedia\Timestamp\ConvertibleTimestamp;
 +use Wikimedia\Rdbms\IDatabase;
 +use Wikimedia\Rdbms\IMaintainableDatabase;
  
  /**
 - * Database abstraction object
 + * Relational database abstraction object
 + *
   * @ingroup Database
 + * @since 1.28
   */
 -abstract class Database implements IDatabase, LoggerAwareInterface {
 +abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAwareInterface {
        /** Number of times to re-try an operation in case of deadlock */
        const DEADLOCK_TRIES = 4;
        /** Minimum time to wait before retry, in microseconds */
@@@ -60,8 -48,8 +60,8 @@@
  
        /** @var string SQL query */
        protected $mLastQuery = '';
 -      /** @var bool */
 -      protected $mDoneWrites = false;
 +      /** @var float|bool UNIX timestamp of last write query */
 +      protected $mLastWriteTime = false;
        /** @var string|bool */
        protected $mPHPError = false;
        /** @var string */
@@@ -88,7 -76,7 +88,7 @@@
        /** @var callback Error logging callback */
        protected $errorLogger;
  
 -      /** @var resource Database connection */
 +      /** @var resource|null Database connection */
        protected $mConn = null;
        /** @var bool */
        protected $mOpened = false;
         * Either a short hexidecimal string if a transaction is active or ""
         *
         * @var string
 -       * @see DatabaseBase::mTrxLevel
 +       * @see Database::mTrxLevel
         */
        protected $mTrxShortId = '';
        /**
         * point (possibly more up-to-date since the first SELECT defines the snapshot).
         *
         * @var float|null
 -       * @see DatabaseBase::mTrxLevel
 +       * @see Database::mTrxLevel
         */
        private $mTrxTimestamp = null;
        /** @var float Lag estimate at the time of BEGIN */
         * Used to provide additional context for error reporting.
         *
         * @var string
 -       * @see DatabaseBase::mTrxLevel
 +       * @see Database::mTrxLevel
         */
        private $mTrxFname = null;
        /**
         * Record if possible write queries were done in the last transaction started
         *
         * @var bool
 -       * @see DatabaseBase::mTrxLevel
 +       * @see Database::mTrxLevel
         */
        private $mTrxDoneWrites = false;
        /**
         * Record if the current transaction was started implicitly due to DBO_TRX being set.
         *
         * @var bool
 -       * @see DatabaseBase::mTrxLevel
 +       * @see Database::mTrxLevel
         */
        private $mTrxAutomatic = false;
        /**
         */
        private $mTrxAtomicLevels = [];
        /**
 -       * Record if the current transaction was started implicitly by DatabaseBase::startAtomic
 +       * Record if the current transaction was started implicitly by Database::startAtomic
         *
         * @var bool
         */
        /** @var array Map of (name => 1) for locks obtained via lock() */
        private $mNamedLocksHeld = [];
        /** @var array Map of (table name => 1) for TEMPORARY tables */
 -      private $mSessionTempTables = [];
 +      protected $mSessionTempTables = [];
  
        /** @var IDatabase|null Lazy handle to the master DB this server replicates from */
        private $lazyMasterHandle;
  
 -      /**
 -       * @since 1.21
 -       * @var resource File handle for upgrade
 -       */
 -      protected $fileHandle = null;
 -
 -      /**
 -       * @since 1.22
 -       * @var string[] Process cache of VIEWs names in the database
 -       */
 -      protected $allViews = null;
 -
        /** @var float UNIX timestamp */
        protected $lastPing = 0.0;
  
                $this->agent = str_replace( '/', '-', $params['agent'] );
  
                $this->mFlags = $params['flags'];
 -              if ( $this->mFlags & DBO_DEFAULT ) {
 +              if ( $this->mFlags & self::DBO_DEFAULT ) {
                        if ( $this->cliMode ) {
 -                              $this->mFlags &= ~DBO_TRX;
 +                              $this->mFlags &= ~self::DBO_TRX;
                        } else {
 -                              $this->mFlags |= DBO_TRX;
 +                              $this->mFlags |= self::DBO_TRX;
                        }
                }
  
                $this->trxProfiler = $params['trxProfiler'];
                $this->connLogger = $params['connLogger'];
                $this->queryLogger = $params['queryLogger'];
 +              $this->errorLogger = $params['errorLogger'];
  
                // Set initial dummy domain until open() sets the final DB/prefix
                $this->currentDomain = DatabaseDomain::newUnspecified();
                } else {
                        $driver = $dbType;
                }
 -              if ( $driver === false ) {
 +              if ( $driver === false || $driver === '' ) {
                        throw new InvalidArgumentException( __METHOD__ .
                                " no viable database extension found for type '$dbType'" );
                }
  
                $class = 'Database' . ucfirst( $driver );
 -              if ( class_exists( $class ) && is_subclass_of( $class, 'IDatabase' ) ) {
 +              if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
                        // Resolve some defaults for b/c
                        $p['host'] = isset( $p['host'] ) ? $p['host'] : false;
                        $p['user'] = isset( $p['user'] ) ? $p['user'] : false;
                        }
                        if ( !isset( $p['errorLogger'] ) ) {
                                $p['errorLogger'] = function ( Exception $e ) {
 -                                      trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_WARNING );
 +                                      trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
                                };
                        }
  
        }
  
        public function bufferResults( $buffer = null ) {
 -              $res = !$this->getFlag( DBO_NOBUFFER );
 +              $res = !$this->getFlag( self::DBO_NOBUFFER );
                if ( $buffer !== null ) {
 -                      $buffer ? $this->clearFlag( DBO_NOBUFFER ) : $this->setFlag( DBO_NOBUFFER );
 +                      $buffer
 +                              ? $this->clearFlag( self::DBO_NOBUFFER )
 +                              : $this->setFlag( self::DBO_NOBUFFER );
                }
  
                return $res;
         * @return bool The previous value of the flag.
         */
        protected function ignoreErrors( $ignoreErrors = null ) {
 -              $res = $this->getFlag( DBO_IGNORE );
 +              $res = $this->getFlag( self::DBO_IGNORE );
                if ( $ignoreErrors !== null ) {
 -                      $ignoreErrors ? $this->setFlag( DBO_IGNORE ) : $this->clearFlag( DBO_IGNORE );
 +                      $ignoreErrors
 +                              ? $this->setFlag( self::DBO_IGNORE )
 +                              : $this->clearFlag( self::DBO_IGNORE );
                }
  
                return $res;
                return $old;
        }
  
 -      /**
 -       * Set the filehandle to copy write statements to.
 -       *
 -       * @param resource $fh File handle
 -       */
 -      public function setFileHandle( $fh ) {
 -              $this->fileHandle = $fh;
 -      }
 -
        public function getLBInfo( $name = null ) {
                if ( is_null( $name ) ) {
                        return $this->mLBInfo;
         * @see setLazyMasterHandle()
         * @since 1.27
         */
 -      public function getLazyMasterHandle() {
 +      protected function getLazyMasterHandle() {
                return $this->lazyMasterHandle;
        }
  
        }
  
        public function doneWrites() {
 -              return (bool)$this->mDoneWrites;
 +              return (bool)$this->mLastWriteTime;
        }
  
        public function lastDoneWrites() {
 -              return $this->mDoneWrites ?: false;
 +              return $this->mLastWriteTime ?: false;
        }
  
        public function writesPending() {
                return !!( $this->mFlags & $flag );
        }
  
 +      /**
 +       * @param string $name Class field name
 +       * @return mixed
 +       * @deprecated Since 1.28
 +       */
        public function getProperty( $name ) {
                return $this->$name;
        }
        protected function installErrorHandler() {
                $this->mPHPError = false;
                $this->htmlErrors = ini_set( 'html_errors', '0' );
 -              set_error_handler( [ $this, 'connectionerrorLogger' ] );
 +              set_error_handler( [ $this, 'connectionErrorLogger' ] );
        }
  
        /**
                if ( $this->htmlErrors !== false ) {
                        ini_set( 'html_errors', $this->htmlErrors );
                }
 +
 +              return $this->getLastPHPError();
 +      }
 +
 +      /**
 +       * @return string|bool Last PHP error for this DB (typically connection errors)
 +       */
 +      protected function getLastPHPError() {
                if ( $this->mPHPError ) {
                        $error = preg_replace( '!\[<a.*</a>\]!', '', $this->mPHPError );
                        $error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
  
                        return $error;
 -              } else {
 -                      return false;
                }
 +
 +              return false;
        }
  
        /**
 +       * This method should not be used outside of Database classes
 +       *
         * @param int $errno
         * @param string $errstr
         */
 -      public function connectionerrorLogger( $errno, $errstr ) {
 +      public function connectionErrorLogger( $errno, $errstr ) {
                $this->mPHPError = $errstr;
        }
  
         */
        abstract protected function closeConnection();
  
 -      function reportConnectionError( $error = 'Unknown error' ) {
 +      public function reportConnectionError( $error = 'Unknown error' ) {
                $myError = $this->lastError();
                if ( $myError ) {
                        $error = $myError;
         * @return bool
         */
        protected function isTransactableQuery( $sql ) {
 -              $verb = $this->getQueryVerb( $sql );
 -              return !in_array( $verb, [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET' ], true );
 +              return !in_array(
 +                      $this->getQueryVerb( $sql ),
 +                      [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET', 'CREATE', 'ALTER' ],
 +                      true
 +              );
        }
  
        /**
         */
        protected function registerTempTableOperation( $sql ) {
                if ( preg_match(
 -                      '/^(CREATE|DROP)\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
 +                      '/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
                        $sql,
                        $matches
                ) ) {
 -                      list( , $verb, $table ) = $matches;
 -                      if ( $verb === 'CREATE' ) {
 -                              $this->mSessionTempTables[$table] = 1;
 -                      } else {
 -                              unset( $this->mSessionTempTables[$table] );
 -                      }
 +                      $this->mSessionTempTables[$matches[1]] = 1;
 +
 +                      return true;
 +              } elseif ( preg_match(
 +                      '/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
 +                      $sql,
 +                      $matches
 +              ) ) {
 +                      unset( $this->mSessionTempTables[$matches[1]] );
  
                        return true;
                } elseif ( preg_match(
                                throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
                        }
                        # Set a flag indicating that writes have been done
 -                      $this->mDoneWrites = microtime( true );
 +                      $this->mLastWriteTime = microtime( true );
                }
  
                // Add trace comment to the begin of the sql string, right after the operator.
 -              // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (bug 42598)
 +              // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598)
                $commentedSql = preg_replace( '/\s|$/', " /* $fname {$this->agent} */ ", $sql, 1 );
  
                # Start implicit transactions that wrap the request if DBO_TRX is enabled
 -              if ( !$this->mTrxLevel && $this->getFlag( DBO_TRX )
 +              if ( !$this->mTrxLevel && $this->getFlag( self::DBO_TRX )
                        && $this->isTransactableQuery( $sql )
                ) {
                        $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
                                $this->mServer, $this->mDBname, $this->mTrxShortId );
                }
  
 -              if ( $this->getFlag( DBO_DEBUG ) ) {
 +              if ( $this->getFlag( self::DBO_DEBUG ) ) {
                        $this->queryLogger->debug( "{$this->mDBname} {$commentedSql}" );
                }
  
  
                if ( false === $ret ) {
                        # Deadlocks cause the entire transaction to abort, not just the statement.
 -                      # http://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
 +                      # https://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
                        # https://www.postgresql.org/docs/9.1/static/explicit-locking.html
                        if ( $this->wasDeadlock() ) {
                                if ( $this->explicitTrxActive() || $priorWritesPending ) {
  
        private function handleSessionLoss() {
                $this->mTrxLevel = 0;
 -              $this->mTrxIdleCallbacks = []; // bug 65263
 -              $this->mTrxPreCommitCallbacks = []; // bug 65263
 +              $this->mTrxIdleCallbacks = []; // T67263
 +              $this->mTrxPreCommitCallbacks = []; // T67263
                $this->mSessionTempTables = [];
                $this->mNamedLocksHeld = [];
                try {
         * @param array $options Associative array of options to be turned into
         *   an SQL query, valid keys are listed in the function.
         * @return array
 -       * @see DatabaseBase::select()
 +       * @see Database::select()
         */
 -      public function makeSelectOptions( $options ) {
 +      protected function makeSelectOptions( $options ) {
                $preLimitTail = $postLimitTail = '';
                $startOpts = '';
  
  
                $preLimitTail .= $this->makeOrderBy( $options );
  
 -              // if (isset($options['LIMIT'])) {
 -              //      $tailOpts .= $this->limitResult('', $options['LIMIT'],
 -              //              isset($options['OFFSET']) ? $options['OFFSET']
 -              //              : false);
 -              // }
 -
                if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
                        $postLimitTail .= ' FOR UPDATE';
                }
         *
         * @param array $options Associative array of options
         * @return string
 -       * @see DatabaseBase::select()
 +       * @see Database::select()
         * @since 1.21
         */
 -      public function makeGroupByWithHaving( $options ) {
 +      protected function makeGroupByWithHaving( $options ) {
                $sql = '';
                if ( isset( $options['GROUP BY'] ) ) {
                        $gb = is_array( $options['GROUP BY'] )
         *
         * @param array $options Associative array of options
         * @return string
 -       * @see DatabaseBase::select()
 +       * @see Database::select()
         * @since 1.21
         */
 -      public function makeOrderBy( $options ) {
 +      protected function makeOrderBy( $options ) {
                if ( isset( $options['ORDER BY'] ) ) {
                        $ob = is_array( $options['ORDER BY'] )
                                ? implode( ',', $options['ORDER BY'] )
                return '';
        }
  
 -      // See IDatabase::select for the docs for this function
        public function select( $table, $vars, $conds = '', $fname = __METHOD__,
                $options = [], $join_conds = [] ) {
                $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
                $useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
                        ? $options['USE INDEX']
                        : [];
 -              $ignoreIndexes = ( isset( $options['IGNORE INDEX'] ) && is_array( $options['IGNORE INDEX'] ) )
 +              $ignoreIndexes = (
 +                      isset( $options['IGNORE INDEX'] ) &&
 +                      is_array( $options['IGNORE INDEX'] )
 +              )
                        ? $options['IGNORE INDEX']
                        : [];
  
                if ( is_array( $table ) ) {
                        $from = ' FROM ' .
 -                              $this->tableNamesWithIndexClauseOrJOIN( $table, $useIndexes, $ignoreIndexes, $join_conds );
 +                              $this->tableNamesWithIndexClauseOrJOIN(
 +                                      $table, $useIndexes, $ignoreIndexes, $join_conds );
                } elseif ( $table != '' ) {
                        if ( $table[0] == ' ' ) {
                                $from = ' FROM ' . $table;
                        } else {
                                $from = ' FROM ' .
 -                                      $this->tableNamesWithIndexClauseOrJOIN( [ $table ], $useIndexes, $ignoreIndexes, [] );
 +                                      $this->tableNamesWithIndexClauseOrJOIN(
 +                                              [ $table ], $useIndexes, $ignoreIndexes, [] );
                        }
                } else {
                        $from = '';
                        if ( is_array( $conds ) ) {
                                $conds = $this->makeList( $conds, self::LIST_AND );
                        }
 -                      $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex WHERE $conds $preLimitTail";
 +                      $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex " .
 +                              "WHERE $conds $preLimitTail";
                } else {
                        $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex $preLimitTail";
                }
        ) {
                $rows = 0;
                $sql = $this->selectSQLText( $tables, '1', $conds, $fname, $options, $join_conds );
 -              $res = $this->query( "SELECT COUNT(*) AS rowcount FROM ($sql) tmp_count", $fname );
 +              // The identifier quotes is primarily for MSSQL.
 +              $rowCountCol = $this->addIdentifierQuotes( "rowcount" );
 +              $tableName = $this->addIdentifierQuotes( "tmp_count" );
 +              $res = $this->query( "SELECT COUNT(*) AS $rowCountCol FROM ($sql) $tableName", $fname );
  
                if ( $res ) {
                        $row = $this->fetchRow( $res );
        }
  
        public function tableExists( $table, $fname = __METHOD__ ) {
 +              $tableRaw = $this->tableName( $table, 'raw' );
 +              if ( isset( $this->mSessionTempTables[$tableRaw] ) ) {
 +                      return true; // already known to exist
 +              }
 +
                $table = $this->tableName( $table );
 -              $old = $this->ignoreErrors( true );
 -              $res = $this->query( "SELECT 1 FROM $table LIMIT 1", $fname );
 -              $this->ignoreErrors( $old );
 +              $ignoreErrors = true;
 +              $res = $this->query( "SELECT 1 FROM $table LIMIT 1", $fname, $ignoreErrors );
  
                return (bool)$res;
        }
        }
  
        /**
 -       * Helper for DatabaseBase::insert().
 +       * Helper for Database::insert().
         *
         * @param array $options
         * @return string
        }
  
        /**
 -       * Make UPDATE options array for DatabaseBase::makeUpdateOptions
 +       * Make UPDATE options array for Database::makeUpdateOptions
         *
         * @param array $options
         * @return array
        }
  
        /**
 -       * Make UPDATE options for the DatabaseBase::update function
 +       * Make UPDATE options for the Database::update function
         *
 -       * @param array $options The options passed to DatabaseBase::update
 +       * @param array $options The options passed to Database::update
         * @return string
         */
        protected function makeUpdateOptions( $options ) {
                return implode( ' ', $opts );
        }
  
 -      function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
 +      public function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
                $table = $this->tableName( $table );
                $opts = $this->makeUpdateOptions( $options );
                $sql = "UPDATE $opts $table SET " . $this->makeList( $values, self::LIST_SET );
                        $sql .= " WHERE " . $this->makeList( $conds, self::LIST_AND );
                }
  
 -              return $this->query( $sql, $fname );
 +              return (bool)$this->query( $sql, $fname );
        }
  
        public function makeList( $a, $mode = self::LIST_COMMA ) {
                }
        }
  
 -      /**
 -       * Return aggregated value alias
 -       *
 -       * @param array $valuedata
 -       * @param string $valuename
 -       *
 -       * @return string
 -       */
        public function aggregateValue( $valuedata, $valuename = 'value' ) {
                return $valuename;
        }
                return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
        }
  
 -      /**
 -       * @param string $field Field or column to cast
 -       * @return string
 -       * @since 1.28
 -       */
        public function buildStringCast( $field ) {
                return $field;
        }
                return $this->mServer;
        }
  
 -      /**
 -       * Format a table name ready for use in constructing an SQL query
 -       *
 -       * This does two important things: it quotes the table names to clean them up,
 -       * and it adds a table prefix if only given a table name with no quotes.
 -       *
 -       * All functions of this object which require a table name call this function
 -       * themselves. Pass the canonical name to such functions. This is only needed
 -       * when calling query() directly.
 -       *
 -       * @note This function does not sanitize user input. It is not safe to use
 -       *   this function to escape user input.
 -       * @param string $name Database table name
 -       * @param string $format One of:
 -       *   quoted - Automatically pass the table name through addIdentifierQuotes()
 -       *            so that it can be used in a query.
 -       *   raw - Do not add identifier quotes to the table name
 -       * @return string Full database name
 -       */
        public function tableName( $name, $format = 'quoted' ) {
                # Skip the entire process when we have a string quoted on both ends.
                # Note that we check the end so that we will still quote any use of
                } elseif ( count( $dbDetails ) == 2 ) {
                        list( $database, $table ) = $dbDetails;
                        # We don't want any prefix added in this case
 +                      $prefix = '';
                        # In dbs that support it, $database may actually be the schema
                        # but that doesn't affect any of the functionality here
 -                      $prefix = '';
                        $schema = '';
                } else {
                        list( $table ) = $dbDetails;
                # Quote $table and apply the prefix if not quoted.
                # $tableName might be empty if this is called from Database::replaceVars()
                $tableName = "{$prefix}{$table}";
 -              if ( $format == 'quoted'
 -                      && !$this->isQuotedIdentifier( $tableName ) && $tableName !== ''
 +              if ( $format === 'quoted'
 +                      && !$this->isQuotedIdentifier( $tableName )
 +                      && $tableName !== ''
                ) {
                        $tableName = $this->addIdentifierQuotes( $tableName );
                }
  
 -              # Quote $schema and merge it with the table name if needed
 -              if ( strlen( $schema ) ) {
 -                      if ( $format == 'quoted' && !$this->isQuotedIdentifier( $schema ) ) {
 -                              $schema = $this->addIdentifierQuotes( $schema );
 -                      }
 -                      $tableName = $schema . '.' . $tableName;
 -              }
 -
 -              # Quote $database and merge it with the table name if needed
 -              if ( $database !== '' ) {
 -                      if ( $format == 'quoted' && !$this->isQuotedIdentifier( $database ) ) {
 -                              $database = $this->addIdentifierQuotes( $database );
 -                      }
 -                      $tableName = $database . '.' . $tableName;
 -              }
 +              # Quote $schema and $database and merge them with the table name if needed
 +              $tableName = $this->prependDatabaseOrSchema( $schema, $tableName, $format );
 +              $tableName = $this->prependDatabaseOrSchema( $database, $tableName, $format );
  
                return $tableName;
        }
  
        /**
 -       * Fetch a number of table names into an array
 -       * This is handy when you need to construct SQL for joins
 -       *
 -       * Example:
 -       * extract( $dbr->tableNames( 'user', 'watchlist' ) );
 -       * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
 -       *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
 -       *
 -       * @return array
 +       * @param string|null $namespace Database or schema
 +       * @param string $relation Name of table, view, sequence, etc...
 +       * @param string $format One of (raw, quoted)
 +       * @return string Relation name with quoted and merged $namespace as needed
         */
 +      private function prependDatabaseOrSchema( $namespace, $relation, $format ) {
 +              if ( strlen( $namespace ) ) {
 +                      if ( $format === 'quoted' && !$this->isQuotedIdentifier( $namespace ) ) {
 +                              $namespace = $this->addIdentifierQuotes( $namespace );
 +                      }
 +                      $relation = $namespace . '.' . $relation;
 +              }
 +
 +              return $relation;
 +      }
 +
        public function tableNames() {
                $inArray = func_get_args();
                $retVal = [];
                return $retVal;
        }
  
 -      /**
 -       * Fetch a number of table names into an zero-indexed numerical array
 -       * This is handy when you need to construct SQL for joins
 -       *
 -       * Example:
 -       * list( $user, $watchlist ) = $dbr->tableNamesN( 'user', 'watchlist' );
 -       * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
 -       *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
 -       *
 -       * @return array
 -       */
        public function tableNamesN() {
                $inArray = func_get_args();
                $retVal = [];
         * @param string|bool $alias Alias (optional)
         * @return string SQL name for aliased table. Will not alias a table to its own name
         */
 -      public function tableNameWithAlias( $name, $alias = false ) {
 +      protected function tableNameWithAlias( $name, $alias = false ) {
                if ( !$alias || $alias == $name ) {
                        return $this->tableName( $name );
                } else {
         * @param array $tables [ [alias] => table ]
         * @return string[] See tableNameWithAlias()
         */
 -      public function tableNamesWithAlias( $tables ) {
 +      protected function tableNamesWithAlias( $tables ) {
                $retval = [];
                foreach ( $tables as $alias => $table ) {
                        if ( is_numeric( $alias ) ) {
         * @param string|bool $alias Alias (optional)
         * @return string SQL name for aliased field. Will not alias a field to its own name
         */
 -      public function fieldNameWithAlias( $name, $alias = false ) {
 +      protected function fieldNameWithAlias( $name, $alias = false ) {
                if ( !$alias || (string)$alias === (string)$name ) {
                        return $name;
                } else {
         * @param array $fields [ [alias] => field ]
         * @return string[] See fieldNameWithAlias()
         */
 -      public function fieldNamesWithAlias( $fields ) {
 +      protected function fieldNamesWithAlias( $fields ) {
                $retval = [];
                foreach ( $fields as $alias => $field ) {
                        if ( is_numeric( $alias ) ) {
                                        }
                                }
                                if ( isset( $ignore_index[$alias] ) ) { // has IGNORE INDEX?
 -                                      $ignore = $this->ignoreIndexClause( implode( ',', (array)$ignore_index[$alias] ) );
 +                                      $ignore = $this->ignoreIndexClause(
 +                                              implode( ',', (array)$ignore_index[$alias] ) );
                                        if ( $ignore != '' ) {
                                                $tableClause .= ' ' . $ignore;
                                        }
         * @return string
         */
        protected function indexName( $index ) {
-               return $index;
+               // Backwards-compatibility hack
+               $renamed = [
+                       'ar_usertext_timestamp' => 'usertext_timestamp',
+                       'un_user_id' => 'user_id',
+                       'un_user_ip' => 'user_ip',
+               ];
+               if ( isset( $renamed[$index] ) ) {
+                       return $renamed[$index];
+               } else {
+                       return $index;
+               }
        }
  
        public function addQuotes( $s ) {
                $this->query( $sql, $fname );
        }
  
 -      /**
 -       * Returns the size of a text field, or -1 for "unlimited"
 -       *
 -       * @param string $table
 -       * @param string $field
 -       * @return int
 -       */
        public function textFieldSize( $table, $field ) {
                $table = $this->tableName( $table );
                $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
                return $this->insert( $destTable, $rows, $fname, $insertOptions );
        }
  
 -      public function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
 +      protected function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
                $fname = __METHOD__,
                $insertOptions = [], $selectOptions = []
        ) {
                        $selectOptions );
  
                if ( is_array( $srcTable ) ) {
 -                      $srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
 +                      $srcTable = implode( ',', array_map( [ $this, 'tableName' ], $srcTable ) );
                } else {
                        $srcTable = $this->tableName( $srcTable );
                }
  
 -              $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
 +              $sql = "INSERT $insertOptions" .
 +                      " INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
                        " SELECT $startOpts " . implode( ',', $varMap ) .
                        " FROM $srcTable $useIndex $ignoreIndex ";
  
         */
        public function limitResult( $sql, $limit, $offset = false ) {
                if ( !is_numeric( $limit ) ) {
 -                      throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
 +                      throw new DBUnexpectedError( $this,
 +                              "Invalid non-numeric limit passed to limitResult()\n" );
                }
  
                return "$sql LIMIT "
        }
  
        /**
 -       * Determines if the given query error was a connection drop
 -       * STUB
 +       * Do not use this method outside of Database/DBError classes
         *
         * @param integer|string $errno
 -       * @return bool
 +       * @return bool Whether the given query error was a connection drop
         */
        public function wasConnectionError( $errno ) {
                return false;
        }
  
 -      /**
 -       * Perform a deadlock-prone transaction.
 -       *
 -       * This function invokes a callback function to perform a set of write
 -       * queries. If a deadlock occurs during the processing, the transaction
 -       * will be rolled back and the callback function will be called again.
 -       *
 -       * Avoid using this method outside of Job or Maintenance classes.
 -       *
 -       * Usage:
 -       *   $dbw->deadlockLoop( callback, ... );
 -       *
 -       * Extra arguments are passed through to the specified callback function.
 -       * This method requires that no transactions are already active to avoid
 -       * causing premature commits or exceptions.
 -       *
 -       * Returns whatever the callback function returned on its successful,
 -       * iteration, or false on error, for example if the retry limit was
 -       * reached.
 -       *
 -       * @return mixed
 -       * @throws DBUnexpectedError
 -       * @throws Exception
 -       */
        public function deadlockLoop() {
                $args = func_get_args();
                $function = array_shift( $args );
                return 0;
        }
  
 -      public function getSlavePos() {
 +      public function getReplicaPos() {
                # Stub
                return false;
        }
                        return;
                }
  
 -              $autoTrx = $this->getFlag( DBO_TRX ); // automatic begin() enabled?
 +              $autoTrx = $this->getFlag( self::DBO_TRX ); // automatic begin() enabled?
                /** @var Exception $e */
                $e = null; // first exception
                do { // callbacks may add callbacks :)
                        foreach ( $callbacks as $callback ) {
                                try {
                                        list( $phpCallback ) = $callback;
 -                                      $this->clearFlag( DBO_TRX ); // make each query its own transaction
 +                                      $this->clearFlag( self::DBO_TRX ); // make each query its own transaction
                                        call_user_func_array( $phpCallback, [ $trigger ] );
                                        if ( $autoTrx ) {
 -                                              $this->setFlag( DBO_TRX ); // restore automatic begin()
 +                                              $this->setFlag( self::DBO_TRX ); // restore automatic begin()
                                        } else {
 -                                              $this->clearFlag( DBO_TRX ); // restore auto-commit
 +                                              $this->clearFlag( self::DBO_TRX ); // restore auto-commit
                                        }
                                } catch ( Exception $ex ) {
                                        call_user_func( $this->errorLogger, $ex );
                        $this->begin( $fname, self::TRANSACTION_INTERNAL );
                        // If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
                        // in all changes being in one transaction to keep requests transactional.
 -                      if ( !$this->getFlag( DBO_TRX ) ) {
 +                      if ( !$this->getFlag( self::DBO_TRX ) ) {
                                $this->mTrxAutomaticAtomic = true;
                        }
                }
                                $this->queryLogger->error( $msg );
                                return; // join the main transaction set
                        }
 -              } elseif ( $this->getFlag( DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
 +              } elseif ( $this->getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
                        // @TODO: make this an exception at some point
                        $msg = "$fname: Implicit transaction expected (DBO_TRX set).";
                        $this->queryLogger->error( $msg );
                $this->mTrxTimestamp = microtime( true );
                $this->mTrxFname = $fname;
                $this->mTrxDoneWrites = false;
 -              $this->mTrxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
                $this->mTrxAutomaticAtomic = false;
                $this->mTrxAtomicLevels = [];
                $this->mTrxShortId = sprintf( '%06x', mt_rand( 0, 0xffffff ) );
                // as lag itself just to be safe
                $status = $this->getApproximateLagStatus();
                $this->mTrxReplicaLag = $status['lag'] + ( microtime( true ) - $status['since'] );
 +              // T147697: make explicitTrxActive() return true until begin() finishes. This way, no
 +              // caller will think its OK to muck around with the transaction just because startAtomic()
 +              // has not yet completed (e.g. setting mTrxAtomicLevels).
 +              $this->mTrxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
        }
  
        /**
         * Issues the BEGIN command to the database server.
         *
 -       * @see DatabaseBase::begin()
 +       * @see Database::begin()
         * @param string $fname
         */
        protected function doBegin( $fname ) {
                        }
                } else {
                        if ( !$this->mTrxLevel ) {
 -                              $this->queryLogger->error( "$fname: No transaction to commit, something got out of sync." );
 +                              $this->queryLogger->error(
 +                                      "$fname: No transaction to commit, something got out of sync." );
                                return; // nothing to do
                        } elseif ( $this->mTrxAutomatic ) {
                                // @TODO: make this an exception at some point
                $writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY );
                $this->doCommit( $fname );
                if ( $this->mTrxDoneWrites ) {
 -                      $this->mDoneWrites = microtime( true );
 +                      $this->mLastWriteTime = microtime( true );
                        $this->trxProfiler->transactionWritingOut(
                                $this->mServer, $this->mDBname, $this->mTrxShortId, $writeTime );
                }
        /**
         * Issues the COMMIT command to the database server.
         *
 -       * @see DatabaseBase::commit()
 +       * @see Database::commit()
         * @param string $fname
         */
        protected function doCommit( $fname ) {
                                $this->queryLogger->error(
                                        "$fname: No transaction to rollback, something got out of sync." );
                                return; // nothing to do
 -                      } elseif ( $this->getFlag( DBO_TRX ) ) {
 +                      } elseif ( $this->getFlag( self::DBO_TRX ) ) {
                                throw new DBUnexpectedError(
                                        $this,
                                        "$fname: Expected mass rollback of all peer databases (DBO_TRX set)."
        /**
         * Issues the ROLLBACK command to the database server.
         *
 -       * @see DatabaseBase::rollback()
 +       * @see Database::rollback()
         * @param string $fname
         */
        protected function doRollback( $fname ) {
                        $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
                        throw new DBUnexpectedError(
                                $this,
 -                              "$fname: Cannot COMMIT to clear snapshot because writes are pending ($fnames)."
 +                              "$fname: Cannot flush snapshot because writes are pending ($fnames)."
                        );
                }
  
                return $this->mTrxLevel && ( $this->mTrxAtomicLevels || !$this->mTrxAutomatic );
        }
  
 -      /**
 -       * Creates a new table with structure copied from existing table
 -       * Note that unlike most database abstraction functions, this function does not
 -       * automatically append database prefix, because it works at a lower
 -       * abstraction level.
 -       * The table names passed to this function shall not be quoted (this
 -       * function calls addIdentifierQuotes when needed).
 -       *
 -       * @param string $oldName Name of table whose structure should be copied
 -       * @param string $newName Name of table to be created
 -       * @param bool $temporary Whether the new table should be temporary
 -       * @param string $fname Calling function name
 -       * @throws RuntimeException
 -       * @return bool True if operation was successful
 -       */
 -      public function duplicateTableStructure( $oldName, $newName, $temporary = false,
 -              $fname = __METHOD__
 +      public function duplicateTableStructure(
 +              $oldName, $newName, $temporary = false, $fname = __METHOD__
        ) {
                throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
        }
  
 -      function listTables( $prefix = null, $fname = __METHOD__ ) {
 +      public function listTables( $prefix = null, $fname = __METHOD__ ) {
                throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
        }
  
 -      /**
 -       * Reset the views process cache set by listViews()
 -       * @since 1.22
 -       */
 -      final public function clearViewsCache() {
 -              $this->allViews = null;
 -      }
 -
 -      /**
 -       * Lists all the VIEWs in the database
 -       *
 -       * For caching purposes the list of all views should be stored in
 -       * $this->allViews. The process cache can be cleared with clearViewsCache()
 -       *
 -       * @param string $prefix Only show VIEWs with this prefix, eg. unit_test_
 -       * @param string $fname Name of calling function
 -       * @throws RuntimeException
 -       * @return array
 -       * @since 1.22
 -       */
        public function listViews( $prefix = null, $fname = __METHOD__ ) {
                throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
        }
  
 -      /**
 -       * Differentiates between a TABLE and a VIEW
 -       *
 -       * @param string $name Name of the database-structure to test.
 -       * @throws RuntimeException
 -       * @return bool
 -       * @since 1.22
 -       */
 -      public function isView( $name ) {
 -              throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
 -      }
 -
        public function timestamp( $ts = 0 ) {
 -              $t = new ConvertableTimestamp( $ts );
 +              $t = new ConvertibleTimestamp( $ts );
                // Let errors bubble up to avoid putting garbage in the DB
                return $t->getTimestamp( TS_MW );
        }
         * necessary. Boolean values are passed through as is, to indicate success
         * of write queries or failure.
         *
 -       * Once upon a time, DatabaseBase::query() returned a bare MySQL result
 +       * Once upon a time, Database::query() returned a bare MySQL result
         * resource, and it was necessary to call this function to convert it to
         * a wrapper. Nowadays, raw database objects are never exposed to external
         * callers, so this is unnecessary in external code.
                }
  
                // This will reconnect if possible or return false if not
 -              $this->clearFlag( DBO_TRX, self::REMEMBER_PRIOR );
 +              $this->clearFlag( self::DBO_TRX, self::REMEMBER_PRIOR );
                $ok = ( $this->query( self::PING_QUERY, __METHOD__, true ) !== false );
                $this->restoreFlags( self::RESTORE_PRIOR );
  
         * @return array|null ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN)
         * @since 1.27
         */
 -      public function getTransactionLagStatus() {
 +      protected function getTransactionLagStatus() {
                return $this->mTrxLevel
                        ? [ 'lag' => $this->mTrxReplicaLag, 'since' => $this->trxTimestamp() ]
                        : null;
         * @return array ('lag': seconds or false on error, 'since': UNIX timestamp of estimate)
         * @since 1.27
         */
 -      public function getApproximateLagStatus() {
 +      protected function getApproximateLagStatus() {
                return [
                        'lag'   => $this->getLBInfo( 'replica' ) ? $this->getLag() : 0,
                        'since' => microtime( true )
                return 0;
        }
  
 -      function maxListLen() {
 +      public function maxListLen() {
                return 0;
        }
  
        public function setSessionOptions( array $options ) {
        }
  
 -      /**
 -       * Read and execute SQL commands from a file.
 -       *
 -       * Returns true on success, error string or exception on failure (depending
 -       * on object's error ignore settings).
 -       *
 -       * @param string $filename File name to open
 -       * @param bool|callable $lineCallback Optional function called before reading each line
 -       * @param bool|callable $resultCallback Optional function called for each MySQL result
 -       * @param bool|string $fname Calling function name or false if name should be
 -       *   generated dynamically using $filename
 -       * @param bool|callable $inputCallback Optional function called for each
 -       *   complete line sent
 -       * @return bool|string
 -       * @throws Exception
 -       */
        public function sourceFile(
 -              $filename, $lineCallback = false, $resultCallback = false, $fname = false, $inputCallback = false
 +              $filename,
 +              callable $lineCallback = null,
 +              callable $resultCallback = null,
 +              $fname = false,
 +              callable $inputCallback = null
        ) {
                MediaWiki\suppressWarnings();
                $fp = fopen( $filename, 'r' );
                }
  
                try {
 -                      $error = $this->sourceStream( $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
 +                      $error = $this->sourceStream(
 +                              $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
                } catch ( Exception $e ) {
                        fclose( $fp );
                        throw $e;
                $this->mSchemaVars = $vars;
        }
  
 -      /**
 -       * Read and execute commands from an open file handle.
 -       *
 -       * Returns true on success, error string or exception on failure (depending
 -       * on object's error ignore settings).
 -       *
 -       * @param resource $fp File handle
 -       * @param bool|callable $lineCallback Optional function called before reading each query
 -       * @param bool|callable $resultCallback Optional function called for each MySQL result
 -       * @param string $fname Calling function name
 -       * @param bool|callable $inputCallback Optional function called for each complete query sent
 -       * @return bool|string
 -       */
 -      public function sourceStream( $fp, $lineCallback = false, $resultCallback = false,
 -              $fname = __METHOD__, $inputCallback = false
 +      public function sourceStream(
 +              $fp,
 +              callable $lineCallback = null,
 +              callable $resultCallback = null,
 +              $fname = __METHOD__,
 +              callable $inputCallback = null
        ) {
                $cmd = '';
  
                        if ( $done || feof( $fp ) ) {
                                $cmd = $this->replaceVars( $cmd );
  
 -                              if ( ( $inputCallback && call_user_func( $inputCallback, $cmd ) ) || !$inputCallback ) {
 +                              if ( !$inputCallback || call_user_func( $inputCallback, $cmd ) ) {
                                        $res = $this->query( $cmd, $fname );
  
                                        if ( $resultCallback ) {
        /**
         * Called by sourceStream() to check if we've reached a statement end
         *
 -       * @param string $sql SQL assembled so far
 -       * @param string $newLine New line about to be added to $sql
 +       * @param string &$sql SQL assembled so far
 +       * @param string &$newLine New line about to be added to $sql
         * @return bool Whether $newLine contains end of the statement
         */
        public function streamStatementEnd( &$sql, &$newLine ) {
                if ( $this->delimiter ) {
                        $prev = $newLine;
 -                      $newLine = preg_replace( '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
 +                      $newLine = preg_replace(
 +                              '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
                        if ( $newLine != $prev ) {
                                return true;
                        }
                        $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
                        throw new DBUnexpectedError(
                                $this,
 -                              "$fname: Cannot COMMIT to clear snapshot because writes are pending ($fnames)."
 +                              "$fname: Cannot flush pre-lock snapshot because writes are pending ($fnames)."
                        );
                }
  
                        return 'infinity';
                }
  
 -              try {
 -                      $t = new ConvertableTimestamp( $expiry );
 -
 -                      return $t->getTimestamp( $format );
 -              } catch ( TimestampException $e ) {
 -                      return false;
 -              }
 +              return ConvertibleTimestamp::convert( $format, $expiry );
        }
  
        public function setBigSelects( $value = true ) {
                return true;
        }
  
 +      /**
 +       * Get the underlying binding handle, mConn
 +       *
 +       * Makes sure that mConn is set (disconnects and ping() failure can unset it).
 +       * This catches broken callers than catch and ignore disconnection exceptions.
 +       * Unlike checking isOpen(), this is safe to call inside of open().
 +       *
 +       * @return resource|object
 +       * @throws DBUnexpectedError
 +       * @since 1.26
 +       */
 +      protected function getBindingHandle() {
 +              if ( !$this->mConn ) {
 +                      throw new DBUnexpectedError(
 +                              $this,
 +                              'DB connection was already closed or the connection dropped.'
 +                      );
 +              }
 +
 +              return $this->mConn;
 +      }
 +
        /**
         * @since 1.19
         * @return string
         */
        public function __clone() {
                $this->connLogger->warning(
 -                      "Cloning " . get_class( $this ) . " is not recomended; forking connection:\n" .
 +                      "Cloning " . static::class . " is not recomended; forking connection:\n" .
                        ( new RuntimeException() )->getTraceAsString()
                );
  
                        // Open a new connection resource without messing with the old one
                        $this->mOpened = false;
                        $this->mConn = false;
 -                      $this->mTrxLevel = 0; // no trx anymore
 +                      $this->mTrxEndCallbacks = []; // don't copy
 +                      $this->handleSessionLoss(); // no trx or locks anymore
                        $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
                        $this->lastPing = microtime( true );
                }
                }
  
                if ( $this->mConn ) {
 -                      // Avoid connection leaks for sanity
 +                      // Avoid connection leaks for sanity. Normally, resources close at script completion.
 +                      // The connection might already be closed in zend/hhvm by now, so suppress warnings.
 +                      \MediaWiki\suppressWarnings();
                        $this->closeConnection();
 +                      \MediaWiki\restoreWarnings();
                        $this->mConn = false;
                        $this->mOpened = false;
                }
        }
  }
 +
 +class_alias( Database::class, 'DatabaseBase' );
   * @file
   * @ingroup Database
   */
 +use Wikimedia\Rdbms\Blob;
 +use Wikimedia\Rdbms\SQLiteField;
 +use Wikimedia\Rdbms\ResultWrapper;
  
  /**
   * @ingroup Database
   */
 -class DatabaseSqlite extends DatabaseBase {
 +class DatabaseSqlite extends Database {
        /** @var bool Whether full text is enabled */
        private static $fulltextEnabled = null;
  
        /** @var string Directory */
        protected $dbDir;
 -
        /** @var string File name for SQLite database file */
        protected $dbPath;
 -
        /** @var string Transaction mode */
        protected $trxMode;
  
        /** @var int The number of rows affected as an integer */
        protected $mAffectedRows;
 -
        /** @var resource */
        protected $mLastResult;
  
                $p['schema'] = false;
                $p['tablePrefix'] = '';
  
 -              return DatabaseBase::factory( 'sqlite', $p );
 +              return Database::factory( 'sqlite', $p );
        }
  
        /**
         * @param string $dbName
         *
         * @throws DBConnectionError
 -       * @return PDO
 +       * @return bool
         */
        function open( $server, $user, $pass, $dbName ) {
                $this->close();
                }
                $this->openFile( $fileName );
  
 -              return $this->mConn;
 +              return (bool)$this->mConn;
        }
  
        /**
  
                $this->dbPath = $fileName;
                try {
 -                      if ( $this->mFlags & DBO_PERSISTENT ) {
 +                      if ( $this->mFlags & self::DBO_PERSISTENT ) {
                                $this->mConn = new PDO( "sqlite:$fileName", '', '',
                                        [ PDO::ATTR_PERSISTENT => true ] );
                        } else {
                return false;
        }
  
 +      public function selectDB( $db ) {
 +              return false; // doesn't make sense
 +      }
 +
        /**
         * @return string SQLite DB file path
         * @since 1.25
        }
  
        /**
 -       * Attaches external database to our connection, see http://sqlite.org/lang_attach.html
 +       * Attaches external database to our connection, see https://sqlite.org/lang_attach.html
         * for details.
         *
         * @param string $name Database name to be used in queries like
                return str_replace( '"', '', parent::tableName( $name, $format ) );
        }
  
+       /**
+        * Index names have DB scope
+        *
+        * @param string $index
+        * @return string
+        */
+       protected function indexName( $index ) {
+               return $index;
+       }
        /**
         * This must be called after nextSequenceVal
         *
         * @param string $table
         * @param string $index
         * @param string $fname
 -       * @return array
 +       * @return array|false
         */
        function indexInfo( $table, $index, $fname = __METHOD__ ) {
                $sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')';
                $res = $this->query( $sql, $fname );
 -              if ( !$res ) {
 -                      return null;
 -              }
 -              if ( $res->numRows() == 0 ) {
 +              if ( !$res || $res->numRows() == 0 ) {
                        return false;
                }
                $info = [];
        }
  
        /**
 -       * @param string $sqls
 +       * @param string[] $sqls
         * @param bool $all Whether to "UNION ALL" or not
         * @return string
         */