/** Maximum time to wait before retry */
const DEADLOCK_DELAY_MAX = 1500000;
+ /** How many row changes in a write query trigger a log entry */
+ const LOG_WRITE_THRESHOLD = 300;
+
protected $mLastQuery = '';
protected $mDoneWrites = false;
protected $mPHPError = false;
return !preg_match( '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\(SELECT)\b/i', $sql );
}
+ /**
+ * Determine whether a SQL statement is sensitive to isolation level.
+ * A SQL statement is considered transactable if its result could vary
+ * depending on the transaction isolation level. Operational commands
+ * such as 'SET' and 'SHOW' are not considered to be transactable.
+ *
+ * @param string $sql
+ * @return bool
+ */
+ public function isTransactableQuery( $sql ) {
+ $verb = substr( $sql, 0, strcspn( $sql, " \t\r\n" ) );
+ return !in_array( $verb, array( 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET' ) );
+ }
+
/**
* Run an SQL query and return the result. Normally throws a DBQueryError
* on failure. If errors are ignored, returns false instead.
global $wgUser, $wgDebugDBTransactions, $wgDebugDumpSqlLength;
$this->mLastQuery = $sql;
- if ( $this->isWriteQuery( $sql ) ) {
+
+ $isWriteQuery = $this->isWriteQuery( $sql );
+ if ( $isWriteQuery ) {
# Set a flag indicating that writes have been done
wfDebug( __METHOD__ . ': Writes done: ' . DatabaseBase::generalizeSQL( $sql ) . "\n" );
$this->mDoneWrites = microtime( true );
// Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (bug 42598)
$commentedSql = preg_replace( '/\s|$/', " /* $fname $userName */ ", $sql, 1 );
- # If DBO_TRX is set, start a transaction
- if ( ( $this->mFlags & DBO_TRX ) && !$this->mTrxLevel &&
- $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK'
- ) {
- # Avoid establishing transactions for SHOW and SET statements too -
- # that would delay transaction initializations to once connection
- # is really used by application
- $sqlstart = substr( $sql, 0, 10 ); // very much worth it, benchmark certified(tm)
- if ( strpos( $sqlstart, "SHOW " ) !== 0 && strpos( $sqlstart, "SET " ) !== 0 ) {
- if ( $wgDebugDBTransactions ) {
- wfDebug( "Implicit transaction start.\n" );
- }
- $this->begin( __METHOD__ . " ($fname)" );
- $this->mTrxAutomatic = true;
+ if ( !$this->mTrxLevel && $this->getFlag( DBO_TRX ) && $this->isTransactableQuery( $sql ) ) {
+ if ( $wgDebugDBTransactions ) {
+ wfDebug( "Implicit transaction start.\n" );
}
+ $this->begin( __METHOD__ . " ($fname)" );
+ $this->mTrxAutomatic = true;
}
# Keep track of whether the transaction has write queries pending
- if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $this->isWriteQuery( $sql ) ) {
+ if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWriteQuery ) {
$this->mTrxDoneWrites = true;
Profiler::instance()->getTransactionProfiler()->transactionWritingIn(
$this->mServer, $this->mDBname, $this->mTrxShortId );
if ( false === $ret ) {
$this->reportQueryError( $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
+ } else {
+ $n = $this->affectedRows();
+ if ( $isWriteQuery && $n > self::LOG_WRITE_THRESHOLD && PHP_SAPI !== 'cli' ) {
+ wfDebugLog( 'DBPerformance',
+ "Query affected $n rows:\n" .
+ DatabaseBase::generalizeSQL( $sql ) . "\n" . wfBacktrace( true ) );
+ }
}
- return $this->resultObject( $ret );
+ $res = $this->resultObject( $ret );
+
+ // Destroy profile sections in the opposite order to their creation
+ $queryProfSection = false;
+ $totalProfSection = false;
+
+ return $res;
}
/**
} elseif ( ( $mode == LIST_SET ) && is_numeric( $field ) ) {
$list .= "$value";
} elseif ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_array( $value ) ) {
- if ( count( $value ) == 0 ) {
+ // Remove null from array to be handled separately if found
+ $includeNull = false;
+ foreach ( array_keys( $value, null, true ) as $nullKey ) {
+ $includeNull = true;
+ unset( $value[$nullKey] );
+ }
+ if ( count( $value ) == 0 && !$includeNull ) {
throw new MWException( __METHOD__ . ": empty input for field $field" );
- } elseif ( count( $value ) == 1 ) {
- // Special-case single values, as IN isn't terribly efficient
- // Don't necessarily assume the single key is 0; we don't
- // enforce linear numeric ordering on other arrays here.
- $value = array_values( $value );
- $list .= $field . " = " . $this->addQuotes( $value[0] );
+ } elseif ( count( $value ) == 0 ) {
+ // only check if $field is null
+ $list .= "$field IS NULL";
} else {
- $list .= $field . " IN (" . $this->makeList( $value ) . ") ";
+ // IN clause contains at least one valid element
+ if ( $includeNull ) {
+ // Group subconditions to ensure correct precedence
+ $list .= '(';
+ }
+ if ( count( $value ) == 1 ) {
+ // Special-case single values, as IN isn't terribly efficient
+ // Don't necessarily assume the single key is 0; we don't
+ // enforce linear numeric ordering on other arrays here.
+ $value = array_values( $value );
+ $list .= $field . " = " . $this->addQuotes( $value[0] );
+ } else {
+ $list .= $field . " IN (" . $this->makeList( $value ) . ") ";
+ }
+ // if null present in array, append IS NULL
+ if ( $includeNull ) {
+ $list .= " OR $field IS NULL)";
+ }
}
} elseif ( $value === null ) {
if ( $mode == LIST_AND || $mode == LIST_OR ) {
* calling rollback when no transaction is in progress. This will silently
* break any ongoing explicit transaction. Only set the flush flag if you
* are sure that it is safe to ignore these warnings in your context.
+ * @throws DBUnexpectedError
* @since 1.23 Added $flush parameter
*/
final public function rollback( $fname = __METHOD__, $flush = '' ) {