Merge "Check Database::mSessionTempTables in Database::tableExists()"
[lhc/web/wiklou.git] / includes / libs / rdbms / database / Database.php
index de0de6e..03a12d7 100644 (file)
@@ -203,6 +203,8 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
 
        /** @var array Map of (name => 1) for locks obtained via lock() */
        private $mNamedLocksHeld = [];
+       /** @var array Map of (table name => 1) for TEMPORARY tables */
+       protected $mSessionTempTables = [];
 
        /** @var IDatabase|null Lazy handle to the master DB this server replicates from */
        private $lazyMasterHandle;
@@ -231,35 +233,27 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
        protected $trxProfiler;
 
        /**
-        * Constructor.
-        *
-        * FIXME: It is possible to construct a Database object with no associated
-        * connection object, by specifying no parameters to __construct(). This
-        * feature is deprecated and should be removed.
+        * Constructor and database handle and attempt to connect to the DB server
         *
         * IDatabase classes should not be constructed directly in external
-        * code. DatabaseBase::factory() should be used instead.
+        * code. Database::factory() should be used instead.
         *
-        * @param array $params Parameters passed from DatabaseBase::factory()
+        * @param array $params Parameters passed from Database::factory()
         */
        function __construct( array $params ) {
                $server = $params['host'];
                $user = $params['user'];
                $password = $params['password'];
                $dbName = $params['dbname'];
-               $flags = $params['flags'];
 
                $this->mSchema = $params['schema'];
                $this->mTablePrefix = $params['tablePrefix'];
 
-               $this->cliMode = isset( $params['cliMode'] )
-                       ? $params['cliMode']
-                       : ( PHP_SAPI === 'cli' );
-               $this->agent = isset( $params['agent'] )
-                       ? str_replace( '/', '-', $params['agent'] ) // escape for comment
-                       : '';
+               $this->cliMode = $params['cliMode'];
+               // Agent name is added to SQL queries in a comment, so make sure it can't break out
+               $this->agent = str_replace( '/', '-', $params['agent'] );
 
-               $this->mFlags = $flags;
+               $this->mFlags = $params['flags'];
                if ( $this->mFlags & DBO_DEFAULT ) {
                        if ( $this->cliMode ) {
                                $this->mFlags &= ~DBO_TRX;
@@ -274,50 +268,70 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                        ? $params['srvCache']
                        : new HashBagOStuff();
 
-               $this->profiler = isset( $params['profiler'] ) ? $params['profiler'] : null;
-               $this->trxProfiler = isset( $params['trxProfiler'] )
-                       ? $params['trxProfiler']
-                       : new TransactionProfiler();
-               $this->connLogger = isset( $params['connLogger'] )
-                       ? $params['connLogger']
-                       : new \Psr\Log\NullLogger();
-               $this->queryLogger = isset( $params['queryLogger'] )
-                       ? $params['queryLogger']
-                       : new \Psr\Log\NullLogger();
+               $this->profiler = $params['profiler'];
+               $this->trxProfiler = $params['trxProfiler'];
+               $this->connLogger = $params['connLogger'];
+               $this->queryLogger = $params['queryLogger'];
+
+               // Set initial dummy domain until open() sets the final DB/prefix
+               $this->currentDomain = DatabaseDomain::newUnspecified();
 
                if ( $user ) {
                        $this->open( $server, $user, $password, $dbName );
-               }
-
-               $this->currentDomain = ( $this->mDBname != '' )
-                       ? new DatabaseDomain( $this->mDBname, null, $this->mTablePrefix )
-                       : DatabaseDomain::newUnspecified();
-       }
-
-       /**
-        * Given a DB type, construct the name of the appropriate child class of
-        * IDatabase. This is designed to replace all of the manual stuff like:
-        *    $class = 'Database' . ucfirst( strtolower( $dbType ) );
-        * as well as validate against the canonical list of DB types we have
-        *
-        * This factory function is mostly useful for when you need to connect to a
-        * database other than the MediaWiki default (such as for external auth,
-        * an extension, et cetera). Do not use this to connect to the MediaWiki
-        * database. Example uses in core:
-        * @see LoadBalancer::reallyOpenConnection()
-        * @see ForeignDBRepo::getMasterDB()
-        * @see WebInstallerDBConnect::execute()
-        *
-        * @since 1.18
-        *
-        * @param string $dbType A possible DB type
-        * @param array $p An array of options to pass to the constructor.
-        *    Valid options are: host, user, password, dbname, flags, tablePrefix, schema, driver
-        * @return IDatabase|null If the database driver or extension cannot be found
+               } elseif ( $this->requiresDatabaseUser() ) {
+                       throw new InvalidArgumentException( "No database user provided." );
+               }
+
+               // Set the domain object after open() sets the relevant fields
+               if ( $this->mDBname != '' ) {
+                       // Domains with server scope but a table prefix are not used by IDatabase classes
+                       $this->currentDomain = new DatabaseDomain( $this->mDBname, null, $this->mTablePrefix );
+               }
+       }
+
+       /**
+        * Construct a Database subclass instance given a database type and parameters
+        *
+        * 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:
+        *   - 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
+        *   - dbname : The name of the database to use where queries do not specify one.
+        *      The database must exist or an error might be thrown. Setting this to the empty string
+        *      will avoid any such errors and make the handle have no implicit database scope. This is
+        *      useful for queries like SHOW STATUS, CREATE DATABASE, or DROP DATABASE. Note that a
+        *      "database" in Postgres is rougly equivalent to an entire MySQL server. This the domain
+        *      in which user names and such are defined, e.g. users are database-specific in Postgres.
+        *   - schema : The database schema to use (if supported). A "schema" in Postgres is roughly
+        *      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,
+        *      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 the old
+        *      'mysql' driver and the newer 'mysqli' driver.
+        *   - variables: Optional map of session variables to set after connecting. This can be
+        *      used to adjust lock timeouts or encoding modes and the like.
+        *   - connLogger: Optional PSR-3 logger interface instance.
+        *   - queryLogger: Optional PSR-3 logger interface instance.
+        *   - profiler: Optional class name or object with profileIn()/profileOut() methods.
+        *      These will be called in query(), using a simplified version of the SQL that also
+        *      includes the agent as a SQL comment.
+        *   - trxProfiler: Optional TransactionProfiler instance.
+        *   - errorLogger: Optional callback that takes an Exception and logs it.
+        *   - cliMode: Whether to consider the execution context that of a CLI script.
+        *   - agent: Optional name used to identify the end-user in query profiling/logging.
+        *   - srvCache: Optional BagOStuff instance to an APC-style cache.
+        * @return Database|null If the database driver or extension cannot be found
         * @throws InvalidArgumentException If the database driver or extension cannot be found
+        * @since 1.18
         */
        final public static function factory( $dbType, $p = [] ) {
-               $canonicalDBTypes = [
+               static $canonicalDBTypes = [
                        'mysql' => [ 'mysqli', 'mysql' ],
                        'postgres' => [],
                        'sqlite' => [],
@@ -363,22 +377,25 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                        $p['variables'] = isset( $p['variables'] ) ? $p['variables'] : [];
                        $p['tablePrefix'] = isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : '';
                        $p['schema'] = isset( $p['schema'] ) ? $p['schema'] : '';
-                       $p['foreign'] = isset( $p['foreign'] ) ? $p['foreign'] : false;
-
-                       $conn = new $class( $p );
-                       if ( isset( $p['connLogger'] ) ) {
-                               $conn->connLogger = $p['connLogger'];
+                       $p['cliMode'] = isset( $p['cliMode'] ) ? $p['cliMode'] : ( PHP_SAPI === 'cli' );
+                       $p['agent'] = isset( $p['agent'] ) ? $p['agent'] : '';
+                       if ( !isset( $p['connLogger'] ) ) {
+                               $p['connLogger'] = new \Psr\Log\NullLogger();
                        }
-                       if ( isset( $p['queryLogger'] ) ) {
-                               $conn->queryLogger = $p['queryLogger'];
+                       if ( !isset( $p['queryLogger'] ) ) {
+                               $p['queryLogger'] = new \Psr\Log\NullLogger();
                        }
-                       if ( isset( $p['errorLogger'] ) ) {
-                               $conn->errorLogger = $p['errorLogger'];
-                       } else {
-                               $conn->errorLogger = function ( Exception $e ) {
+                       $p['profiler'] = isset( $p['profiler'] ) ? $p['profiler'] : null;
+                       if ( !isset( $p['trxProfiler'] ) ) {
+                               $p['trxProfiler'] = new TransactionProfiler();
+                       }
+                       if ( !isset( $p['errorLogger'] ) ) {
+                               $p['errorLogger'] = function ( Exception $e ) {
                                        trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_WARNING );
                                };
                        }
+
+                       $conn = new $class( $p );
                } else {
                        $conn = null;
                }
@@ -667,7 +684,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
        }
 
        /**
-        * Create a log context to pass to PSR logging functions.
+        * Create a log context to pass to PSR-3 logger functions.
         *
         * @param array $extras Additional data to add to context
         * @return array
@@ -772,11 +789,43 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                return !in_array( $verb, [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET' ], true );
        }
 
+       /**
+        * @param string $sql A SQL query
+        * @return bool Whether $sql is SQL for creating/dropping a new TEMPORARY table
+        */
+       protected function registerTempTableOperation( $sql ) {
+               if ( preg_match(
+                       '/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
+                       $sql,
+                       $matches
+               ) ) {
+                       $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(
+                       '/^(?:INSERT\s+(?:\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+[`"\']?(\w+)[`"\']?/i',
+                       $sql,
+                       $matches
+               ) ) {
+                       return isset( $this->mSessionTempTables[$matches[1]] );
+               }
+
+               return false;
+       }
+
        public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
                $priorWritesPending = $this->writesOrCallbacksPending();
                $this->mLastQuery = $sql;
 
-               $isWrite = $this->isWriteQuery( $sql );
+               $isWrite = $this->isWriteQuery( $sql ) && !$this->registerTempTableOperation( $sql );
                if ( $isWrite ) {
                        $reason = $this->getReadOnlyReason();
                        if ( $reason !== false ) {
@@ -822,7 +871,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                        $lastError = $this->lastError();
                        $lastErrno = $this->lastErrno();
                        # Update state tracking to reflect transaction loss due to disconnection
-                       $this->handleTransactionLoss();
+                       $this->handleSessionLoss();
                        if ( $this->reconnect() ) {
                                $msg = __METHOD__ . ": lost connection to {$this->getServer()}; reconnected";
                                $this->connLogger->warning( $msg );
@@ -851,7 +900,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                                        $tempIgnore = false; // not recoverable
                                }
                                # Update state tracking to reflect transaction loss
-                               $this->handleTransactionLoss();
+                               $this->handleSessionLoss();
                        }
 
                        $this->reportQueryError(
@@ -903,7 +952,11 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                $this->trxProfiler->recordQueryCompletion(
                        $queryProf, $startTime, $isWrite, $this->affectedRows()
                );
-               MWDebug::query( $sql, $fname, $isMaster, $queryRuntime );
+               $this->queryLogger->debug( $sql, [
+                       'method' => $fname,
+                       'master' => $isMaster,
+                       'runtime' => $queryRuntime,
+               ] );
 
                return $ret;
        }
@@ -960,10 +1013,12 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                return true;
        }
 
-       private function handleTransactionLoss() {
+       private function handleSessionLoss() {
                $this->mTrxLevel = 0;
                $this->mTrxIdleCallbacks = []; // bug 65263
                $this->mTrxPreCommitCallbacks = []; // bug 65263
+               $this->mSessionTempTables = [];
+               $this->mNamedLocksHeld = [];
                try {
                        // Handle callbacks in mTrxEndCallbacks
                        $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
@@ -1159,7 +1214,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                }
                if ( isset( $options['HAVING'] ) ) {
                        $having = is_array( $options['HAVING'] )
-                               ? $this->makeList( $options['HAVING'], LIST_AND )
+                               ? $this->makeList( $options['HAVING'], self::LIST_AND )
                                : $options['HAVING'];
                        $sql .= ' HAVING ' . $having;
                }
@@ -1229,7 +1284,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
 
                if ( !empty( $conds ) ) {
                        if ( is_array( $conds ) ) {
-                               $conds = $this->makeList( $conds, LIST_AND );
+                               $conds = $this->makeList( $conds, self::LIST_AND );
                        }
                        $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex WHERE $conds $preLimitTail";
                } else {
@@ -1348,6 +1403,11 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
        }
 
        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 );
@@ -1463,16 +1523,16 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
        function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
                $table = $this->tableName( $table );
                $opts = $this->makeUpdateOptions( $options );
-               $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
+               $sql = "UPDATE $opts $table SET " . $this->makeList( $values, self::LIST_SET );
 
                if ( $conds !== [] && $conds !== '*' ) {
-                       $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
+                       $sql .= " WHERE " . $this->makeList( $conds, self::LIST_AND );
                }
 
                return $this->query( $sql, $fname );
        }
 
-       public function makeList( $a, $mode = LIST_COMMA ) {
+       public function makeList( $a, $mode = self::LIST_COMMA ) {
                if ( !is_array( $a ) ) {
                        throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
                }
@@ -1482,9 +1542,9 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
 
                foreach ( $a as $field => $value ) {
                        if ( !$first ) {
-                               if ( $mode == LIST_AND ) {
+                               if ( $mode == self::LIST_AND ) {
                                        $list .= ' AND ';
-                               } elseif ( $mode == LIST_OR ) {
+                               } elseif ( $mode == self::LIST_OR ) {
                                        $list .= ' OR ';
                                } else {
                                        $list .= ',';
@@ -1493,11 +1553,13 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                                $first = false;
                        }
 
-                       if ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_numeric( $field ) ) {
+                       if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) {
                                $list .= "($value)";
-                       } elseif ( ( $mode == LIST_SET ) && is_numeric( $field ) ) {
+                       } elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) {
                                $list .= "$value";
-                       } elseif ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_array( $value ) ) {
+                       } elseif (
+                               ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array( $value )
+                       ) {
                                // Remove null from array to be handled separately if found
                                $includeNull = false;
                                foreach ( array_keys( $value, null, true ) as $nullKey ) {
@@ -1505,7 +1567,8 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                                        unset( $value[$nullKey] );
                                }
                                if ( count( $value ) == 0 && !$includeNull ) {
-                                       throw new InvalidArgumentException( __METHOD__ . ": empty input for field $field" );
+                                       throw new InvalidArgumentException(
+                                               __METHOD__ . ": empty input for field $field" );
                                } elseif ( count( $value ) == 0 ) {
                                        // only check if $field is null
                                        $list .= "$field IS NULL";
@@ -1530,17 +1593,19 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                                        }
                                }
                        } elseif ( $value === null ) {
-                               if ( $mode == LIST_AND || $mode == LIST_OR ) {
+                               if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
                                        $list .= "$field IS ";
-                               } elseif ( $mode == LIST_SET ) {
+                               } elseif ( $mode == self::LIST_SET ) {
                                        $list .= "$field = ";
                                }
                                $list .= 'NULL';
                        } else {
-                               if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
+                               if (
+                                       $mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET
+                               ) {
                                        $list .= "$field = ";
                                }
-                               $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
+                               $list .= $mode == self::LIST_NAMES ? $value : $this->addQuotes( $value );
                        }
                }
 
@@ -1554,12 +1619,12 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                        if ( count( $sub ) ) {
                                $conds[] = $this->makeList(
                                        [ $baseKey => $base, $subKey => array_keys( $sub ) ],
-                                       LIST_AND );
+                                       self::LIST_AND );
                        }
                }
 
                if ( $conds ) {
-                       return $this->makeList( $conds, LIST_OR );
+                       return $this->makeList( $conds, self::LIST_OR );
                } else {
                        // Nothing to search for...
                        return false;
@@ -1880,7 +1945,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                                                $tableClause .= ' ' . $ignore;
                                        }
                                }
-                               $on = $this->makeList( (array)$conds, LIST_AND );
+                               $on = $this->makeList( (array)$conds, self::LIST_AND );
                                if ( $on != '' ) {
                                        $tableClause .= ' ON (' . $on . ')';
                                }
@@ -1944,6 +2009,8 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                }
                if ( $s === null ) {
                        return 'NULL';
+               } elseif ( is_bool( $s ) ) {
+                       return (int)$s;
                } else {
                        # This will also quote numeric values. This should be harmless,
                        # and protects against weird problems that occur when they really
@@ -2149,10 +2216,10 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                                        foreach ( $index as $column ) {
                                                $rowKey[$column] = $row[$column];
                                        }
-                                       $clauses[] = $this->makeList( $rowKey, LIST_AND );
+                                       $clauses[] = $this->makeList( $rowKey, self::LIST_AND );
                                }
                        }
-                       $where = [ $this->makeList( $clauses, LIST_OR ) ];
+                       $where = [ $this->makeList( $clauses, self::LIST_OR ) ];
                } else {
                        $where = false;
                }
@@ -2194,7 +2261,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                $joinTable = $this->tableName( $joinTable );
                $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
                if ( $conds != '*' ) {
-                       $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
+                       $sql .= 'WHERE ' . $this->makeList( $conds, self::LIST_AND );
                }
                $sql .= ')';
 
@@ -2235,7 +2302,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
 
                if ( $conds != '*' ) {
                        if ( is_array( $conds ) ) {
-                               $conds = $this->makeList( $conds, LIST_AND );
+                               $conds = $this->makeList( $conds, self::LIST_AND );
                        }
                        $sql .= ' WHERE ' . $conds;
                }
@@ -2313,7 +2380,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
 
                if ( $conds != '*' ) {
                        if ( is_array( $conds ) ) {
-                               $conds = $this->makeList( $conds, LIST_AND );
+                               $conds = $this->makeList( $conds, self::LIST_AND );
                        }
                        $sql .= " WHERE $conds";
                }
@@ -2364,7 +2431,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
 
        public function conditional( $cond, $trueVal, $falseVal ) {
                if ( is_array( $cond ) ) {
-                       $cond = $this->makeList( $cond, LIST_AND );
+                       $cond = $this->makeList( $cond, self::LIST_AND );
                }
 
                return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
@@ -3447,6 +3514,14 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                $this->tableAliases = $aliases;
        }
 
+       /**
+        * @return bool Whether a DB user is required to access the DB
+        * @since 1.28
+        */
+       protected function requiresDatabaseUser() {
+               return true;
+       }
+
        /**
         * @since 1.19
         * @return string
@@ -3455,6 +3530,26 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                return (string)$this->mConn;
        }
 
+       /**
+        * Make sure that copies do not share the same client binding handle
+        * @throws DBConnectionError
+        */
+       public function __clone() {
+               $this->connLogger->warning(
+                       "Cloning " . get_class( $this ) . " is not recomended; forking connection:\n" .
+                       ( new RuntimeException() )->getTraceAsString()
+               );
+
+               if ( $this->isOpen() ) {
+                       // Open a new connection resource without messing with the old one
+                       $this->mOpened = false;
+                       $this->mConn = false;
+                       $this->mTrxLevel = 0; // no trx anymore
+                       $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
+                       $this->lastPing = microtime( true );
+               }
+       }
+
        /**
         * Called by serialize. Throw an exception when DB connection is serialized.
         * This causes problems on some database engines because the connection is
@@ -3466,7 +3561,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
        }
 
        /**
-        * Run a few simple sanity checks
+        * Run a few simple sanity checks and close dangling connections
         */
        public function __destruct() {
                if ( $this->mTrxLevel && $this->mTrxDoneWrites ) {
@@ -3478,5 +3573,12 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                        $fnames = implode( ', ', $danglingWriters );
                        trigger_error( "DB transaction writes or callbacks still pending ($fnames)." );
                }
+
+               if ( $this->mConn ) {
+                       // Avoid connection leaks for sanity
+                       $this->closeConnection();
+                       $this->mConn = false;
+                       $this->mOpened = false;
+               }
        }
 }