Merge "rdbms: remove various deprecated methods"
[lhc/web/wiklou.git] / includes / libs / rdbms / database / Database.php
index 894a262..8b65397 100644 (file)
@@ -30,7 +30,7 @@ use Psr\Log\LoggerInterface;
 use Psr\Log\NullLogger;
 use Wikimedia\ScopedCallback;
 use Wikimedia\Timestamp\ConvertibleTimestamp;
-use Wikimedia;
+use Wikimedia\AtEase\AtEase;
 use BagOStuff;
 use HashBagOStuff;
 use LogicException;
@@ -61,7 +61,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        protected $cliMode;
        /** @var string Agent name for query profiling */
        protected $agent;
-       /** @var int Bitfield of class DBO_* constants */
+       /** @var int Bit field of class DBO_* constants */
        protected $flags;
        /** @var array LoadBalancer tracking information */
        protected $lbInfo = [];
@@ -198,21 +198,37 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        /** @var int Writes to this temporary table effect lastDoneWrites() */
        private static $TEMP_PSEUDO_PERMANENT = 2;
 
-       /** Number of times to re-try an operation in case of deadlock */
+       /** @var int Number of times to re-try an operation in case of deadlock */
        private static $DEADLOCK_TRIES = 4;
-       /** Minimum time to wait before retry, in microseconds */
+       /** @var int Minimum time to wait before retry, in microseconds */
        private static $DEADLOCK_DELAY_MIN = 500000;
-       /** Maximum time to wait before retry */
+       /** @var int Maximum time to wait before retry */
        private static $DEADLOCK_DELAY_MAX = 1500000;
 
-       /** How long before it is worth doing a dummy query to test the connection */
+       /** @var int How long before it is worth doing a dummy query to test the connection */
        private static $PING_TTL = 1.0;
+       /** @var string Dummy SQL query */
        private static $PING_QUERY = 'SELECT 1 AS ping';
 
+       /** @var float Guess of how many seconds it takes to replicate a small insert */
        private static $TINY_WRITE_SEC = 0.010;
+       /** @var float Consider a write slow if it took more than this many seconds */
        private static $SLOW_WRITE_SEC = 0.500;
+       /** @var float Assume an insert of this many rows or less should be fast to replicate */
        private static $SMALL_WRITE_ROWS = 100;
 
+       /** @var string[] List of DBO_* flags that can be changed after connection */
+       protected static $MUTABLE_FLAGS = [
+               'DBO_DEBUG',
+               'DBO_NOBUFFER',
+               'DBO_TRX',
+               'DBO_DDLMODE',
+       ];
+       /** @var int Bit field of all DBO_* flags that can be changed after connection */
+       protected static $DBO_MUTABLE = (
+               self::DBO_DEBUG | self::DBO_NOBUFFER | self::DBO_TRX | self::DBO_DDLMODE
+       );
+
        /**
         * @note exceptions for missing libraries/drivers should be thrown in initConnection()
         * @param array $params Parameters passed from Database::factory()
@@ -279,32 +295,27 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        /**
         * Actually connect to the database over the wire (or to local files)
         *
-        * @throws InvalidArgumentException
         * @throws DBConnectionError
         * @since 1.31
         */
        protected function doInitConnection() {
-               if ( strlen( $this->connectionParams['user'] ) ) {
-                       $this->open(
-                               $this->connectionParams['host'],
-                               $this->connectionParams['user'],
-                               $this->connectionParams['password'],
-                               $this->connectionParams['dbname'],
-                               $this->connectionParams['schema'],
-                               $this->connectionParams['tablePrefix']
-                       );
-               } else {
-                       throw new InvalidArgumentException( "No database user provided" );
-               }
+               $this->open(
+                       $this->connectionParams['host'],
+                       $this->connectionParams['user'],
+                       $this->connectionParams['password'],
+                       $this->connectionParams['dbname'],
+                       $this->connectionParams['schema'],
+                       $this->connectionParams['tablePrefix']
+               );
        }
 
        /**
         * Open a new connection to the database (closing any existing one)
         *
-        * @param string $server Database server host
-        * @param string $user Database user name
-        * @param string $password Database user password
-        * @param string $dbName Database name
+        * @param string|null $server Database server host
+        * @param string|null $user Database user name
+        * @param string|null $password Database user password
+        * @param string|null $dbName Database name
         * @param string|null $schema Database schema name
         * @param string $tablePrefix Table prefix
         * @throws DBConnectionError
@@ -316,8 +327,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         *
         * This also connects to the database immediately upon object construction
         *
-        * @param string $dbType A possible DB type (sqlite, mysql, postgres,...)
-        * @param array $p Parameter map with keys:
+        * @param string $type A possible DB type (sqlite, mysql, postgres,...)
+        * @param array $params Parameter map with keys:
         *   - host : The hostname of the DB server
         *   - user : The name of the database user the client operates under
         *   - password : The password for the database user
@@ -331,7 +342,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         *      equivalent to a "database" in MySQL. Note that MySQL and SQLite do not use schemas.
         *   - tablePrefix : Optional table prefix that is implicitly added on to all table names
         *      recognized in queries. This can be used in place of schemas for handle site farms.
-        *   - flags : Optional bitfield of DBO_* constants that define connection, protocol,
+        *   - flags : Optional bit field of DBO_* constants that define connection, protocol,
         *      buffering, and transaction behavior. It is STRONGLY adviced to leave the DBO_DEFAULT
         *      flag in place UNLESS this this database simply acts as a key/value store.
         *   - driver: Optional name of a specific DB client driver. For MySQL, there is only the
@@ -356,45 +367,51 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * @throws InvalidArgumentException If the database driver or extension cannot be found
         * @since 1.18
         */
-       final public static function factory( $dbType, $p = [], $connect = self::NEW_CONNECTED ) {
-               $class = self::getClass( $dbType, $p['driver'] ?? null );
+       final public static function factory( $type, $params = [], $connect = self::NEW_CONNECTED ) {
+               $class = self::getClass( $type, $params['driver'] ?? null );
 
                if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
-                       // Resolve some defaults for b/c
-                       $p['host'] = $p['host'] ?? false;
-                       $p['user'] = $p['user'] ?? false;
-                       $p['password'] = $p['password'] ?? false;
-                       $p['dbname'] = $p['dbname'] ?? false;
-                       $p['flags'] = $p['flags'] ?? 0;
-                       $p['variables'] = $p['variables'] ?? [];
-                       $p['tablePrefix'] = $p['tablePrefix'] ?? '';
-                       $p['schema'] = $p['schema'] ?? null;
-                       $p['cliMode'] = $p['cliMode'] ?? ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' );
-                       $p['agent'] = $p['agent'] ?? '';
-                       if ( !isset( $p['connLogger'] ) ) {
-                               $p['connLogger'] = new NullLogger();
-                       }
-                       if ( !isset( $p['queryLogger'] ) ) {
-                               $p['queryLogger'] = new NullLogger();
-                       }
-                       $p['profiler'] = $p['profiler'] ?? null;
-                       if ( !isset( $p['trxProfiler'] ) ) {
-                               $p['trxProfiler'] = new TransactionProfiler();
-                       }
-                       if ( !isset( $p['errorLogger'] ) ) {
-                               $p['errorLogger'] = function ( Exception $e ) {
+                       $params += [
+                               'host' => null,
+                               'user' => null,
+                               'password' => null,
+                               'dbname' => null,
+                               'schema' => null,
+                               'tablePrefix' => '',
+                               'flags' => 0,
+                               'variables' => [],
+                               'cliMode' => ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ),
+                               'agent' => basename( $_SERVER['SCRIPT_NAME'] ) . '@' . gethostname()
+                       ];
+
+                       $normalizedParams = [
+                               // Configuration
+                               'host' => strlen( $params['host'] ) ? $params['host'] : null,
+                               'user' => strlen( $params['user'] ) ? $params['user'] : null,
+                               'password' => is_string( $params['password'] ) ? $params['password'] : null,
+                               'dbname' => strlen( $params['dbname'] ) ? $params['dbname'] : null,
+                               'schema' => strlen( $params['schema'] ) ? $params['schema'] : null,
+                               'tablePrefix' => (string)$params['tablePrefix'],
+                               'flags' => (int)$params['flags'],
+                               'variables' => $params['variables'],
+                               'cliMode' => (bool)$params['cliMode'],
+                               'agent' => (string)$params['agent'],
+                               // Objects and callbacks
+                               'profiler' => $params['profiler'] ?? null,
+                               'trxProfiler' => $params['trxProfiler'] ?? new TransactionProfiler(),
+                               'connLogger' => $params['connLogger'] ?? new NullLogger(),
+                               'queryLogger' => $params['queryLogger'] ?? new NullLogger(),
+                               'errorLogger' => $params['errorLogger'] ?? function ( Exception $e ) {
                                        trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
-                               };
-                       }
-                       if ( !isset( $p['deprecationLogger'] ) ) {
-                               $p['deprecationLogger'] = function ( $msg ) {
+                               },
+                               'deprecationLogger' => $params['deprecationLogger'] ?? function ( $msg ) {
                                        trigger_error( $msg, E_USER_DEPRECATED );
-                               };
-                       }
+                               }
+                       ] + $params;
 
                        /** @var Database $conn */
-                       $conn = new $class( $p );
-                       if ( $connect == self::NEW_CONNECTED ) {
+                       $conn = new $class( $normalizedParams );
+                       if ( $connect === self::NEW_CONNECTED ) {
                                $conn->initConnection();
                        }
                } else {
@@ -576,11 +593,17 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                return null;
        }
 
-       public function setLBInfo( $name, $value = null ) {
-               if ( is_null( $value ) ) {
-                       $this->lbInfo = $name;
+       public function setLBInfo( $nameOrArray, $value = null ) {
+               if ( is_array( $nameOrArray ) ) {
+                       $this->lbInfo = $nameOrArray;
+               } elseif ( is_string( $nameOrArray ) ) {
+                       if ( $value !== null ) {
+                               $this->lbInfo[$nameOrArray] = $value;
+                       } else {
+                               unset( $this->lbInfo[$nameOrArray] );
+                       }
                } else {
-                       $this->lbInfo[$name] = $value;
+                       throw new InvalidArgumentException( "Got non-string key" );
                }
        }
 
@@ -597,10 +620,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                return $this->lazyMasterHandle;
        }
 
-       public function implicitGroupby() {
-               return true;
-       }
-
        public function implicitOrderby() {
                return true;
        }
@@ -609,10 +628,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                return $this->lastQuery;
        }
 
-       public function doneWrites() {
-               return (bool)$this->lastWriteTime;
-       }
-
        public function lastDoneWrites() {
                return $this->lastWriteTime ?: false;
        }
@@ -725,24 +740,32 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
-               if ( ( $flag & self::DBO_IGNORE ) ) {
-                       throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed" );
+               if ( $flag & ~static::$DBO_MUTABLE ) {
+                       throw new DBUnexpectedError(
+                               $this,
+                               "Got $flag (allowed: " . implode( ', ', static::$MUTABLE_FLAGS ) . ')'
+                       );
                }
 
                if ( $remember === self::REMEMBER_PRIOR ) {
                        array_push( $this->priorFlags, $this->flags );
                }
+
                $this->flags |= $flag;
        }
 
        public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
-               if ( ( $flag & self::DBO_IGNORE ) ) {
-                       throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed" );
+               if ( $flag & ~static::$DBO_MUTABLE ) {
+                       throw new DBUnexpectedError(
+                               $this,
+                               "Got $flag (allowed: " . implode( ', ', static::$MUTABLE_FLAGS ) . ')'
+                       );
                }
 
                if ( $remember === self::REMEMBER_PRIOR ) {
                        array_push( $this->priorFlags, $this->flags );
                }
+
                $this->flags &= ~$flag;
        }
 
@@ -760,26 +783,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        public function getFlag( $flag ) {
-               return (bool)( $this->flags & $flag );
-       }
-
-       /**
-        * @param string $name Class field name
-        * @return mixed
-        * @deprecated Since 1.28
-        */
-       public function getProperty( $name ) {
-               return $this->$name;
+               return ( ( $this->flags & $flag ) === $flag );
        }
 
        public function getDomainID() {
                return $this->currentDomain->getId();
        }
 
-       final public function getWikiID() {
-               return $this->getDomainID();
-       }
-
        /**
         * Get information about an index into an object
         * @param string $table Table name
@@ -913,7 +923,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $closed = true; // already closed; nothing to do
                }
 
-               $this->conn = false;
+               $this->conn = null;
 
                // Throw any unexpected errors after having disconnected
                if ( $exception instanceof Exception ) {
@@ -1161,7 +1171,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         *
         * @param string $sql Original SQL query
         * @param string $fname Name of the calling function
-        * @param int $flags Bitfield of class QUERY_* constants
+        * @param int $flags Bit field of class QUERY_* constants
         * @return array An n-tuple of:
         *   - mixed|bool: An object, resource, or true on success; false on failure
         *   - string: The result of calling lastError()
@@ -1249,7 +1259,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * @param string $commentedSql SQL query with debugging/trace comment
         * @param bool $isPermWrite Whether the query is a (non-temporary table) write
         * @param string $fname Name of the calling function
-        * @param int $flags Bitfield of class QUERY_* constants
+        * @param int $flags Bit field of class QUERY_* constants
         * @return array An n-tuple of:
         *   - mixed|bool: An object, resource, or true on success; false on failure
         *   - string: The result of calling lastError()
@@ -1554,9 +1564,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                if ( $ignore ) {
                        $this->queryLogger->debug( "SQL ERROR (ignored): $error" );
                } else {
-                       $exception = $this->getQueryExceptionAndLog( $error, $errno, $sql, $fname );
-
-                       throw $exception;
+                       throw $this->getQueryExceptionAndLog( $error, $errno, $sql, $fname );
                }
        }
 
@@ -1568,19 +1576,18 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * @return DBError
         */
        private function getQueryExceptionAndLog( $error, $errno, $sql, $fname ) {
-               $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
                $this->queryLogger->error(
                        "{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
                        $this->getLogContext( [
                                'method' => __METHOD__,
                                'errno' => $errno,
                                'error' => $error,
-                               'sql1line' => $sql1line,
+                               'sql1line' => mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 ),
                                'fname' => $fname,
                                'trace' => ( new RuntimeException() )->getTraceAsString()
                        ] )
                );
-               $this->queryLogger->debug( "SQL ERROR: " . $error . "" );
+
                if ( $this->wasQueryTimeout( $error, $errno ) ) {
                        $e = new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname );
                } elseif ( $this->wasConnectionError( $errno ) ) {
@@ -1592,6 +1599,25 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                return $e;
        }
 
+       /**
+        * @param string $error
+        * @return DBConnectionError
+        */
+       final protected function newExceptionAfterConnectError( $error ) {
+               // Connection was not fully initialized and is not safe for use
+               $this->conn = null;
+
+               $this->connLogger->error(
+                       "Error connecting to {db_server} as user {db_user}: {error}",
+                       $this->getLogContext( [
+                               'error' => $error,
+                               'trace' => ( new RuntimeException() )->getTraceAsString()
+                       ] )
+               );
+
+               return new DBConnectionError( $this, $error );
+       }
+
        public function freeResult( $res ) {
        }
 
@@ -4281,7 +4307,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         */
        protected function replaceLostConnection( $fname ) {
                $this->closeConnection();
-               $this->conn = false;
+               $this->conn = null;
 
                $this->handleSessionLossPreconnect();
 
@@ -4290,8 +4316,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                $this->server,
                                $this->user,
                                $this->password,
-                               $this->getDBname(),
-                               $this->dbSchema(),
+                               $this->currentDomain->getDatabase(),
+                               $this->currentDomain->getSchema(),
                                $this->tablePrefix()
                        );
                        $this->lastPing = microtime( true );
@@ -4429,9 +4455,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                $fname = false,
                callable $inputCallback = null
        ) {
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $fp = fopen( $filename, 'r' );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
 
                if ( $fp === false ) {
                        throw new RuntimeException( "Could not open \"{$filename}\"" );
@@ -4860,7 +4886,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
 
                if ( $this->isOpen() ) {
                        // Open a new connection resource without messing with the old one
-                       $this->conn = false;
+                       $this->conn = null;
                        $this->trxEndCallbacks = []; // don't copy
                        $this->trxSectionCancelCallbacks = []; // don't copy
                        $this->handleSessionLossPreconnect(); // no trx or locks anymore
@@ -4868,8 +4894,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                $this->server,
                                $this->user,
                                $this->password,
-                               $this->getDBname(),
-                               $this->dbSchema(),
+                               $this->currentDomain->getDatabase(),
+                               $this->currentDomain->getSchema(),
                                $this->tablePrefix()
                        );
                        $this->lastPing = microtime( true );
@@ -4903,10 +4929,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                if ( $this->conn ) {
                        // 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.
-                       Wikimedia\suppressWarnings();
+                       AtEase::suppressWarnings();
                        $this->closeConnection();
-                       Wikimedia\restoreWarnings();
-                       $this->conn = false;
+                       AtEase::restoreWarnings();
+                       $this->conn = null;
                }
        }
 }