rdbms: small cleanups to session loss handling
[lhc/web/wiklou.git] / includes / libs / rdbms / database / Database.php
index c699374..014c4af 100644 (file)
@@ -1015,12 +1015,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                $this->queryLogger->warning( $msg, $params +
                                        [ 'trace' => ( new RuntimeException() )->getTraceAsString() ] );
 
-                               if ( !$recoverable ) {
-                                       # Callers may catch the exception and continue to use the DB
-                                       $this->reportQueryError( $lastError, $lastErrno, $sql, $fname );
-                               } else {
+                               if ( $recoverable ) {
                                        # Should be safe to silently retry the query
                                        $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
+                               } else {
+                                       # Callers may catch the exception and continue to use the DB
+                                       $this->reportQueryError( $lastError, $lastErrno, $sql, $fname );
                                }
                        } else {
                                $msg = __METHOD__ . ': lost connection to {dbserver} permanently';
@@ -1181,19 +1181,29 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         */
        private function handleSessionLoss() {
                $this->trxLevel = 0;
-               $this->trxIdleCallbacks = []; // T67263
-               $this->trxPreCommitCallbacks = []; // T67263
+               $this->trxIdleCallbacks = []; // T67263; transaction already lost
+               $this->trxPreCommitCallbacks = []; // T67263; transaction already lost
                $this->sessionTempTables = [];
                $this->namedLocksHeld = [];
+
+               // Note: if callback suppression is set then some *Callbacks arrays are not cleared here
+               $e = null;
                try {
                        // Handle callbacks in trxEndCallbacks
                        $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
+               } catch ( Exception $ex ) {
+                       // Already logged; move on...
+                       $e = $e ?: $ex;
+               }
+               try {
+                       // Handle callbacks in trxRecurringCallbacks
                        $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
-                       return null;
-               } catch ( Exception $e ) {
+               } catch ( Exception $ex ) {
                        // Already logged; move on...
-                       return $e;
+                       $e = $e ?: $ex;
                }
+
+               return $e;
        }
 
        /**
@@ -1461,14 +1471,27 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
                        $this->makeSelectOptions( $options );
 
-               if ( !empty( $conds ) ) {
-                       if ( is_array( $conds ) ) {
-                               $conds = $this->makeList( $conds, self::LIST_AND );
-                       }
+               if ( is_array( $conds ) ) {
+                       $conds = $this->makeList( $conds, self::LIST_AND );
+               }
+
+               if ( $conds === null || $conds === false ) {
+                       $this->queryLogger->warning(
+                               __METHOD__
+                               . ' called from '
+                               . $fname
+                               . ' with incorrect parameters: $conds must be a string or an array'
+                       );
+                       $conds = '';
+               }
+
+               if ( $conds === '' ) {
+                       $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex $preLimitTail";
+               } elseif ( is_string( $conds ) ) {
                        $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex " .
                                "WHERE $conds $preLimitTail";
                } else {
-                       $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex $preLimitTail";
+                       throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
                }
 
                if ( isset( $options['LIMIT'] ) ) {
@@ -1841,10 +1864,48 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
        }
 
+       public function buildSubstring( $input, $startPosition, $length = null ) {
+               $this->assertBuildSubstringParams( $startPosition, $length );
+               $functionBody = "$input FROM $startPosition";
+               if ( $length !== null ) {
+                       $functionBody .= " FOR $length";
+               }
+               return 'SUBSTRING(' . $functionBody . ')';
+       }
+
+       /**
+        * Check type and bounds for parameters to self::buildSubstring()
+        *
+        * All supported databases have substring functions that behave the same for
+        * positive $startPosition and non-negative $length, but behaviors differ when
+        * given 0 or negative $startPosition or negative $length. The simplest
+        * solution to that is to just forbid those values.
+        *
+        * @param int $startPosition
+        * @param int|null $length
+        * @since 1.31
+        */
+       protected function assertBuildSubstringParams( $startPosition, $length ) {
+               if ( !is_int( $startPosition ) || $startPosition <= 0 ) {
+                       throw new InvalidArgumentException(
+                               '$startPosition must be a positive integer'
+                       );
+               }
+               if ( !( is_int( $length ) && $length >= 0 || $length === null ) ) {
+                       throw new InvalidArgumentException(
+                               '$length must be null or an integer greater than or equal to 0'
+                       );
+               }
+       }
+
        public function buildStringCast( $field ) {
                return $field;
        }
 
+       public function buildIntegerCast( $field ) {
+               return 'CAST( ' . $field . ' AS INTEGER )';
+       }
+
        public function databasesAreIndependent() {
                return false;
        }
@@ -2145,8 +2206,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                }
 
                // We can't separate explicit JOIN clauses with ',', use ' ' for those
-               $implicitJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
-               $explicitJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
+               $implicitJoins = $ret ? implode( ',', $ret ) : "";
+               $explicitJoins = $retJOIN ? implode( ' ', $retJOIN ) : "";
 
                // Compile our final table clause
                return implode( ' ', [ $implicitJoins, $explicitJoins ] );
@@ -2292,11 +2353,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $rows = [ $rows ];
                }
 
-               $useTrx = !$this->trxLevel;
-               if ( $useTrx ) {
-                       $this->begin( $fname, self::TRANSACTION_INTERNAL );
-               }
                try {
+                       $this->startAtomic( $fname );
                        $affectedRowCount = 0;
                        foreach ( $rows as $row ) {
                                // Delete rows which collide with this one
@@ -2329,17 +2387,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                $this->insert( $table, $row, $fname );
                                $affectedRowCount += $this->affectedRows();
                        }
+                       $this->endAtomic( $fname );
+                       $this->affectedRowCount = $affectedRowCount;
                } catch ( Exception $e ) {
-                       if ( $useTrx ) {
-                               $this->rollback( $fname, self::FLUSHING_INTERNAL );
-                       }
+                       $this->rollback( $fname, self::FLUSHING_INTERNAL );
                        throw $e;
                }
-               if ( $useTrx ) {
-                       $this->commit( $fname, self::FLUSHING_INTERNAL );
-               }
-
-               $this->affectedRowCount = $affectedRowCount;
        }
 
        /**
@@ -2405,11 +2458,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                }
 
                $affectedRowCount = 0;
-               $useTrx = !$this->trxLevel;
-               if ( $useTrx ) {
-                       $this->begin( $fname, self::TRANSACTION_INTERNAL );
-               }
                try {
+                       $this->startAtomic( $fname );
                        # Update any existing conflicting row(s)
                        if ( $where !== false ) {
                                $ok = $this->update( $table, $set, $where, $fname );
@@ -2420,16 +2470,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        # Now insert any non-conflicting row(s)
                        $ok = $this->insert( $table, $rows, $fname, [ 'IGNORE' ] ) && $ok;
                        $affectedRowCount += $this->affectedRows();
+                       $this->endAtomic( $fname );
+                       $this->affectedRowCount = $affectedRowCount;
                } catch ( Exception $e ) {
-                       if ( $useTrx ) {
-                               $this->rollback( $fname, self::FLUSHING_INTERNAL );
-                       }
+                       $this->rollback( $fname, self::FLUSHING_INTERNAL );
                        throw $e;
                }
-               if ( $useTrx ) {
-                       $this->commit( $fname, self::FLUSHING_INTERNAL );
-               }
-               $this->affectedRowCount = $affectedRowCount;
 
                return $ok;
        }
@@ -2487,11 +2533,16 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                return $this->query( $sql, $fname );
        }
 
-       public function insertSelect(
+       final public function insertSelect(
                $destTable, $srcTable, $varMap, $conds,
                $fname = __METHOD__, $insertOptions = [], $selectOptions = [], $selectJoinConds = []
        ) {
-               if ( $this->cliMode ) {
+               static $hints = [ 'NO_AUTO_COLUMNS' ];
+
+               $insertOptions = (array)$insertOptions;
+               $selectOptions = (array)$selectOptions;
+
+               if ( $this->cliMode && $this->isInsertSelectSafe( $insertOptions, $selectOptions ) ) {
                        // For massive migrations with downtime, we don't want to select everything
                        // into memory and OOM, so do all this native on the server side if possible.
                        return $this->nativeInsertSelect(
@@ -2500,7 +2551,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                $varMap,
                                $conds,
                                $fname,
-                               $insertOptions,
+                               array_diff( $insertOptions, $hints ),
                                $selectOptions,
                                $selectJoinConds
                        );
@@ -2512,12 +2563,22 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $varMap,
                        $conds,
                        $fname,
-                       $insertOptions,
+                       array_diff( $insertOptions, $hints ),
                        $selectOptions,
                        $selectJoinConds
                );
        }
 
+       /**
+        * @param array $insertOptions INSERT options
+        * @param array $selectOptions SELECT options
+        * @return bool Whether an INSERT SELECT with these options will be replication safe
+        * @since 1.31
+        */
+       protected function isInsertSelectSafe( array $insertOptions, array $selectOptions ) {
+               return true;
+       }
+
        /**
         * Implementation of insertSelect() based on select() and insert()
         *
@@ -2537,8 +2598,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                $fname = __METHOD__,
                $insertOptions = [], $selectOptions = [], $selectJoinConds = []
        ) {
-               $insertOptions = array_diff( (array)$insertOptions, [ 'NO_AUTO_COLUMNS' ] );
-
                // For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
                // on only the master (without needing row-based-replication). It also makes it easy to
                // know how big the INSERT is going to be.
@@ -2583,12 +2642,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                $this->affectedRowCount = $affectedRowCount;
                        } else {
                                $this->rollback( $fname, self::FLUSHING_INTERNAL );
-                               $this->affectedRowCount = 0;
                        }
                        return $ok;
                } catch ( Exception $e ) {
                        $this->rollback( $fname, self::FLUSHING_INTERNAL );
-                       $this->affectedRowCount = 0;
                        throw $e;
                }
        }
@@ -2617,7 +2674,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                if ( !is_array( $insertOptions ) ) {
                        $insertOptions = [ $insertOptions ];
                }
-               $insertOptions = array_diff( (array)$insertOptions, [ 'NO_AUTO_COLUMNS' ] );
 
                $insertOptions = $this->makeInsertOptions( $insertOptions );
 
@@ -3220,6 +3276,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                } catch ( Exception $e ) {
                        // already logged; let LoadBalancer move on during mass-rollback
                }
+
+               $this->affectedRowCount = 0; // for the sake of consistency
        }
 
        /**