);
}
+ public function preCommitCallbacksPending() {
+ return $this->trxLevel && $this->trxPreCommitCallbacks;
+ }
+
/**
* @return string|null
*/
}
/**
- * Get the list of method names that have pending write queries or callbacks
- * for this transaction
+ * List the methods that have write queries or callbacks for the current transaction
*
- * @return array
+ * This method should not be used outside of Database/LoadBalancer
+ *
+ * @return string[]
+ * @since 1.32
*/
- protected function pendingWriteAndCallbackCallers() {
- if ( !$this->trxLevel ) {
- return [];
- }
-
- $fnames = $this->trxWriteCallers;
+ public function pendingWriteAndCallbackCallers() {
+ $fnames = $this->pendingWriteCallers();
foreach ( [
$this->trxIdleCallbacks,
$this->trxPreCommitCallbacks,
}
// Sanity check that no callbacks are dangling
- if (
- $this->trxIdleCallbacks || $this->trxPreCommitCallbacks || $this->trxEndCallbacks
- ) {
+ $fnames = $this->pendingWriteAndCallbackCallers();
+ if ( $fnames ) {
throw new RuntimeException(
- "Transaction callbacks are still pending:\n" .
- implode( ', ', $this->pendingWriteAndCallbackCallers() )
+ "Transaction callbacks are still pending:\n" . implode( ', ', $fnames )
);
}
abstract protected function closeConnection();
/**
- * @param string $error Fallback error message, used if none is given by DB
+ * @deprecated since 1.32
+ * @param string $error Fallback message, if none is given by DB
* @throws DBConnectionError
*/
public function reportConnectionError( $error = 'Unknown error' ) {
- $myError = $this->lastError();
- if ( $myError ) {
- $error = $myError;
- }
-
- # New method
- throw new DBConnectionError( $this, $error );
+ call_user_func( $this->deprecationLogger, 'Use of ' . __METHOD__ . ' is deprecated.' );
+ throw new DBConnectionError( $this, $this->lastError() ?: $error );
}
/**
return '';
}
- public function select( $table, $vars, $conds = '', $fname = __METHOD__,
- $options = [], $join_conds = [] ) {
+ public function select(
+ $table, $vars, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
+ ) {
$sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
return $this->query( $sql, $fname );
$options = [], $join_conds = []
) {
if ( is_array( $vars ) ) {
- $vars = implode( ',', $this->fieldNamesWithAlias( $vars ) );
+ $fields = implode( ',', $this->fieldNamesWithAlias( $vars ) );
+ } else {
+ $fields = $vars;
}
$options = (array)$options;
? $options['IGNORE INDEX']
: [];
+ if (
+ $this->selectOptionsIncludeLocking( $options ) &&
+ $this->selectFieldsOrOptionsAggregate( $vars, $options )
+ ) {
+ // Some DB types (postgres/oracle) disallow FOR UPDATE with aggregate
+ // functions. Discourage use of such queries to encourage compatibility.
+ call_user_func(
+ $this->deprecationLogger,
+ __METHOD__ . ": aggregation used with a locking SELECT ($fname)."
+ );
+ }
+
if ( is_array( $table ) ) {
$from = ' FROM ' .
$this->tableNamesWithIndexClauseOrJOIN(
}
if ( $conds === '' ) {
- $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex $preLimitTail";
+ $sql = "SELECT $startOpts $fields $from $useIndex $ignoreIndex $preLimitTail";
} elseif ( is_string( $conds ) ) {
- $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex " .
+ $sql = "SELECT $startOpts $fields $from $useIndex $ignoreIndex " .
"WHERE $conds $preLimitTail";
} else {
throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
}
+ /**
+ * @param string|array $options
+ * @return bool
+ */
+ private function selectOptionsIncludeLocking( $options ) {
+ $options = (array)$options;
+ foreach ( [ 'FOR UPDATE', 'LOCK IN SHARE MODE' ] as $lock ) {
+ if ( in_array( $lock, $options, true ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param array|string $fields
+ * @param array|string $options
+ * @return bool
+ */
+ private function selectFieldsOrOptionsAggregate( $fields, $options ) {
+ foreach ( (array)$options as $key => $value ) {
+ if ( is_string( $key ) ) {
+ if ( preg_match( '/^(?:GROUP BY|HAVING)$/i', $key ) ) {
+ return true;
+ }
+ } elseif ( is_string( $value ) ) {
+ if ( preg_match( '/^(?:DISTINCT|DISTINCTROW)$/i', $value ) ) {
+ return true;
+ }
+ }
+ }
+
+ $regex = '/^(?:COUNT|MIN|MAX|SUM|GROUP_CONCAT|LISTAGG|ARRAY_AGG)\s*\\(/i';
+ foreach ( (array)$fields as $field ) {
+ if ( is_string( $field ) && preg_match( $regex, $field ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
/**
* @param array|string $conds
* @param string $fname
$this->trxEndCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
}
- final public function onTransactionIdle( callable $callback, $fname = __METHOD__ ) {
+ final public function onTransactionCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
if ( !$this->trxLevel && $this->getTransactionRoundId() ) {
// Start an implicit transaction similar to how query() does
$this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
}
}
+ final public function onTransactionIdle( callable $callback, $fname = __METHOD__ ) {
+ $this->onTransactionCommitOrIdle( $callback, $fname );
+ }
+
final public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
if ( !$this->trxLevel && $this->getTransactionRoundId() ) {
// Start an implicit transaction similar to how query() does
// No transaction is active nor will start implicitly, so make one for this callback
$this->startAtomic( __METHOD__, self::ATOMIC_CANCELABLE );
try {
- call_user_func( $callback );
+ call_user_func( $callback, $this );
$this->endAtomic( __METHOD__ );
} catch ( Exception $e ) {
$this->cancelAtomic( __METHOD__ );
if ( in_array( $entry[2], $sectionIds, true ) ) {
$callback = $entry[0];
$this->trxEndCallbacks[$key][0] = function () use ( $callback ) {
- return $callback( self::TRIGGER_ROLLBACK );
+ return $callback( self::TRIGGER_ROLLBACK, $this );
};
}
}
}
/**
- * Actually run and consume any "on transaction idle/resolution" callbacks.
+ * Actually consume and run any "on transaction idle/resolution" callbacks.
*
* This method should not be used outside of Database/LoadBalancer
*
* @param int $trigger IDatabase::TRIGGER_* constant
+ * @return int Number of callbacks attempted
* @since 1.20
* @throws Exception
*/
public function runOnTransactionIdleCallbacks( $trigger ) {
+ if ( $this->trxLevel ) { // sanity
+ throw new DBUnexpectedError( $this, __METHOD__ . ': a transaction is still open.' );
+ }
+
if ( $this->trxEndCallbacksSuppressed ) {
- return;
+ return 0;
}
+ $count = 0;
$autoTrx = $this->getFlag( self::DBO_TRX ); // automatic begin() enabled?
/** @var Exception $e */
$e = null; // first exception
$this->trxEndCallbacks = []; // consumed (recursion guard)
foreach ( $callbacks as $callback ) {
try {
+ ++$count;
list( $phpCallback ) = $callback;
$this->clearFlag( self::DBO_TRX ); // make each query its own transaction
- call_user_func_array( $phpCallback, [ $trigger ] );
+ call_user_func( $phpCallback, $trigger, $this );
if ( $autoTrx ) {
$this->setFlag( self::DBO_TRX ); // restore automatic begin()
} else {
if ( $e instanceof Exception ) {
throw $e; // re-throw any first exception
}
+
+ return $count;
}
/**
- * Actually run and consume any "on transaction pre-commit" callbacks.
+ * Actually consume and run any "on transaction pre-commit" callbacks.
*
* This method should not be used outside of Database/LoadBalancer
*
* @since 1.22
+ * @return int Number of callbacks attempted
* @throws Exception
*/
public function runOnTransactionPreCommitCallbacks() {
+ $count = 0;
+
$e = null; // first exception
do { // callbacks may add callbacks :)
$callbacks = $this->trxPreCommitCallbacks;
$this->trxPreCommitCallbacks = []; // consumed (and recursion guard)
foreach ( $callbacks as $callback ) {
try {
+ ++$count;
list( $phpCallback ) = $callback;
- call_user_func( $phpCallback );
+ call_user_func( $phpCallback, $this );
} catch ( Exception $ex ) {
call_user_func( $this->errorLogger, $ex );
$e = $e ?: $ex;
if ( $e instanceof Exception ) {
throw $e; // re-throw any first exception
}
+
+ return $count;
}
/**
$savepointId = $cancelable === self::ATOMIC_CANCELABLE ? self::$NOT_APPLICABLE : null;
if ( !$this->trxLevel ) {
- $this->begin( $fname, self::TRANSACTION_INTERNAL );
+ $this->begin( $fname, self::TRANSACTION_INTERNAL ); // sets trxAutomatic
// 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( self::DBO_TRX ) ) {
);
}
- $this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
- $this->runTransactionListenerCallbacks( self::TRIGGER_COMMIT );
+ // With FLUSHING_ALL_PEERS, callbacks will be explicitly run later
+ if ( $flush !== self::FLUSHING_ALL_PEERS ) {
+ $this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
+ $this->runTransactionListenerCallbacks( self::TRIGGER_COMMIT );
+ }
}
/**
$this->trxIdleCallbacks = [];
$this->trxPreCommitCallbacks = [];
- if ( $trxActive ) {
+ // With FLUSHING_ALL_PEERS, callbacks will be explicitly run later
+ if ( $trxActive && $flush !== self::FLUSHING_ALL_PEERS ) {
try {
$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
} catch ( Exception $e ) {