* (T188472) The 'comma' value for $wgArticleCountMethod is no longer supported for
performance reasons, and installations with this setting will now work as if it
was configured with 'any'.
+* $wgLogAutopatrol now defaults to false instead of true.
=== New features in 1.31 ===
* (T76554) User sub-pages named ….json are now protected in the same way that ….js
* (T189785) Added a monthly heartbeat ping to the pingback feature.
* The CLI installer (maintenance/install.php) learned to detect and include
extensions. Pass --with-extensions to enable that feature.
+* (T184791) rc_patrolled now has three states: "0" for unpatrolled,
+ "1" for manually patrolled and "2" for autopatrolled actions.
=== External library changes in 1.31 ===
'Wikimedia\\Rdbms\\DBReplicationWaitError' => __DIR__ . '/includes/libs/rdbms/exception/DBReplicationWaitError.php',
'Wikimedia\\Rdbms\\DBTransactionError' => __DIR__ . '/includes/libs/rdbms/exception/DBTransactionError.php',
'Wikimedia\\Rdbms\\DBTransactionSizeError' => __DIR__ . '/includes/libs/rdbms/exception/DBTransactionSizeError.php',
+ 'Wikimedia\\Rdbms\\DBTransactionStateError' => __DIR__ . '/includes/libs/rdbms/exception/DBTransactionStateError.php',
'Wikimedia\\Rdbms\\DBUnexpectedError' => __DIR__ . '/includes/libs/rdbms/exception/DBUnexpectedError.php',
'Wikimedia\\Rdbms\\Database' => __DIR__ . '/includes/libs/rdbms/database/Database.php',
'Wikimedia\\Rdbms\\DatabaseDomain' => __DIR__ . '/includes/libs/rdbms/database/DatabaseDomain.php',
/**
* Log autopatrol actions to the log table
+ * The default used to be true before 1.31
+ *
+ * @since 1.22
*/
-$wgLogAutopatrol = true;
+$wgLogAutopatrol = false;
/**
* Provide syndication feeds (RSS, Atom) for, e.g., Recentchanges, Newpages
* @ingroup API
*/
class ApiPurge extends ApiBase {
- private $mPageSet;
+ private $mPageSet = null;
/**
* Purges the cache of a page
* @return ApiPageSet
*/
private function getPageSet() {
- if ( !isset( $this->mPageSet ) ) {
+ if ( $this->mPageSet === null ) {
$this->mPageSet = new ApiPageSet( $this );
}
*/
class ApiSetNotificationTimestamp extends ApiBase {
- private $mPageSet;
+ private $mPageSet = null;
public function execute() {
$user = $this->getUser();
* @return ApiPageSet
*/
private function getPageSet() {
- if ( !isset( $this->mPageSet ) ) {
+ if ( $this->mPageSet === null ) {
$this->mPageSet = new ApiPageSet( $this );
}
/** @var integer|null Rows affected by the last query to query() or its CRUD wrappers */
protected $affectedRowCount;
+ /**
+ * @var int Transaction status
+ */
+ protected $trxStatus = self::STATUS_TRX_NONE;
+ /**
+ * @var Exception|null The last error that caused the status to become STATUS_TRX_ERROR
+ */
+ protected $trxStatusCause;
/**
* Either 1 if a transaction is active or 0 otherwise.
* The other Trx fields may not be meaningfull if this is 0.
/** @var int */
protected $nonNativeInsertSelectBatchSize = 10000;
+ /** @var int Transaction is in a error state requiring a full or savepoint rollback */
+ const STATUS_TRX_ERROR = 1;
+ /** @var int Transaction is active and in a normal state */
+ const STATUS_TRX_OK = 2;
+ /** @var int No transaction is active */
+ const STATUS_TRX_NONE = 3;
+
/**
* @note: exceptions for missing libraries/drivers should be thrown in initConnection()
* @param array $params Parameters passed from Database::factory()
return $this->trxLevel ? $this->trxTimestamp : null;
}
+ /**
+ * @return int One of the STATUS_TRX_* class constants
+ * @since 1.31
+ */
+ public function trxStatus() {
+ return $this->trxStatus;
+ }
+
public function tablePrefix( $prefix = null ) {
$old = $this->tablePrefix;
if ( $prefix !== null ) {
return $fnames;
}
+ /**
+ * @return string
+ */
+ private function flatAtomicSectionList() {
+ return array_reduce( $this->trxAtomicLevels, function ( $accum, $v ) {
+ return $accum === null ? $v[0] : "$accum, " . $v[0];
+ } );
+ }
+
public function isOpen() {
return $this->opened;
}
);
}
- public function close() {
+ final public function close() {
+ $exception = null; // error to throw after disconnecting
+
if ( $this->conn ) {
// Resolve any dangling transaction first
- if ( $this->trxLevel() ) {
+ if ( $this->trxLevel ) {
// Meaningful transactions should ideally have been resolved by now
if ( $this->writesOrCallbacksPending() ) {
$this->queryLogger->warning(
__METHOD__ . ": writes or callbacks still pending.",
[ 'trace' => ( new RuntimeException() )->getTraceAsString() ]
);
+ // Cannot let incomplete atomic sections be committed
+ if ( $this->trxAtomicLevels ) {
+ $levels = $this->flatAtomicSectionList();
+ $exception = new DBUnexpectedError(
+ $this,
+ __METHOD__ . ": atomic sections $levels are still open."
+ );
+ // Check if it is possible to properly commit and trigger callbacks
+ } elseif ( $this->trxEndCallbacksSuppressed ) {
+ $exception = new DBUnexpectedError(
+ $this,
+ __METHOD__ . ': callbacks are suppressed; cannot properly commit.'
+ );
+ }
}
- // Check if it is possible to properly commit and trigger callbacks
- if ( $this->trxEndCallbacksSuppressed ) {
- throw new DBUnexpectedError(
- $this,
- __METHOD__ . ': callbacks are suppressed; cannot properly commit.'
- );
+ // Commit or rollback the changes and run any callbacks as needed
+ if ( $this->trxStatus === self::STATUS_TRX_OK && !$exception ) {
+ $this->commit( __METHOD__, self::TRANSACTION_INTERNAL );
+ } else {
+ $this->rollback( __METHOD__, self::TRANSACTION_INTERNAL );
}
- // Commit the changes and run any callbacks as needed
- $this->commit( __METHOD__, self::FLUSHING_INTERNAL );
}
// Close the actual connection in the binding handle
$closed = $this->closeConnection();
$this->conn = false;
- // Sanity check that no callbacks are dangling
- if (
- $this->trxIdleCallbacks || $this->trxPreCommitCallbacks || $this->trxEndCallbacks
- ) {
- throw new RuntimeException( "Transaction callbacks still pending." );
- }
} else {
$closed = true; // already closed; nothing to do
}
$this->opened = false;
+ // Throw any unexpected errors after having disconnected
+ if ( $exception instanceof Exception ) {
+ throw $exception;
+ }
+
+ // Sanity check that no callbacks are dangling
+ if (
+ $this->trxIdleCallbacks || $this->trxPreCommitCallbacks || $this->trxEndCallbacks
+ ) {
+ throw new RuntimeException(
+ "Transaction callbacks are still pending:\n" .
+ implode( ', ', $this->pendingWriteAndCallbackCallers() )
+ );
+ }
+
return $closed;
}
}
public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
+ $this->assertTransactionStatus( $sql, $fname );
+
$priorWritesPending = $this->writesOrCallbacksPending();
$this->lastQuery = $sql;
}
if ( $ret === false ) {
- # Deadlocks cause the entire transaction to abort, not just the statement.
- # https://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
- # https://www.postgresql.org/docs/9.1/static/explicit-locking.html
- if ( $this->wasDeadlock() ) {
+ if ( $this->trxLevel && !$this->wasKnownStatementRollbackError() ) {
+ # Either the query was aborted or all queries after BEGIN where aborted.
if ( $this->explicitTrxActive() || $priorWritesPending ) {
- $tempIgnore = false; // not recoverable
+ # In the first case, the only options going forward are (a) ROLLBACK, or
+ # (b) ROLLBACK TO SAVEPOINT (if one was set). If the later case, the only
+ # option is ROLLBACK, since the snapshots would have been released.
+ if ( is_object( $tempIgnore ) ) {
+ // Ugly hack to know that savepoints are in use for postgres
+ // FIXME: remove this and make DatabasePostgres use ATOMIC_CANCELABLE
+ } else {
+ $this->trxStatus = self::STATUS_TRX_ERROR;
+ $this->trxStatusCause =
+ $this->makeQueryException( $lastError, $lastErrno, $sql, $fname );
+ $tempIgnore = false; // cannot recover
+ }
+ } else {
+ # Nothing prior was there to lose from the transaction
+ $this->trxStatus = self::STATUS_TRX_OK;
}
- # Usually the transaction is rolled back to BEGIN, leaving an empty transaction.
- # Destroy any such transaction so the rollback callbacks run in AUTO-COMMIT mode
- # as normal. Also, if DBO_TRX is set and an explicit transaction rolled back here,
- # further queries should be back in AUTO-COMMIT mode, not stuck in a transaction.
- $this->doRollback( __METHOD__ );
- # Update state tracking to reflect transaction loss
- $this->handleTransactionLoss();
}
$this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $tempIgnore );
}
}
+ /**
+ * @param string $sql
+ * @param string $fname
+ * @throws DBTransactionStateError
+ */
+ private function assertTransactionStatus( $sql, $fname ) {
+ if (
+ $this->trxStatus < self::STATUS_TRX_OK &&
+ $this->getQueryVerb( $sql ) !== 'ROLLBACK' // transaction/savepoint
+ ) {
+ throw new DBTransactionStateError(
+ $this,
+ "Cannot execute query from $fname while transaction status is ERROR. ",
+ [],
+ $this->trxStatusCause
+ );
+ }
+ }
+
/**
* Determine whether or not it is safe to retry queries after a database
* connection is lost
} elseif ( $sql === 'ROLLBACK' ) {
return true; // transaction lost...which is also what was requested :)
} elseif ( $this->explicitTrxActive() ) {
- return false; // don't drop atomocity
+ return false; // don't drop atomocity and explicit snapshots
} elseif ( $priorWritesPending ) {
return false; // prior writes lost from implicit transaction
}
if ( $tempIgnore ) {
$this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
} else {
- $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,
- 'fname' => $fname,
- ] )
- );
- $this->queryLogger->debug( "SQL ERROR: " . $error . "\n" );
- $wasQueryTimeout = $this->wasQueryTimeout( $error, $errno );
- if ( $wasQueryTimeout ) {
- throw new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname );
- } else {
- throw new DBQueryError( $this, $error, $errno, $sql, $fname );
- }
+ $exception = $this->makeQueryException( $error, $errno, $sql, $fname );
+
+ throw $exception;
}
}
+ /**
+ * @param string $error
+ * @param string|int $errno
+ * @param string $sql
+ * @param string $fname
+ * @return DBError
+ */
+ private function makeQueryException( $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,
+ 'fname' => $fname,
+ ] )
+ );
+ $this->queryLogger->debug( "SQL ERROR: " . $error . "\n" );
+ $wasQueryTimeout = $this->wasQueryTimeout( $error, $errno );
+ if ( $wasQueryTimeout ) {
+ $e = new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname );
+ } else {
+ $e = new DBQueryError( $this, $error, $errno, $sql, $fname );
+ }
+
+ return $e;
+ }
+
public function freeResult( $res ) {
}
return false;
}
+ /**
+ * @return bool Whether it is safe to assume the given error only caused statement rollback
+ * @note This is for backwards compatibility for callers catching DBError exceptions in
+ * order to ignore problems like duplicate key errors or foriegn key violations
+ * @since 1.31
+ */
+ protected function wasKnownStatementRollbackError() {
+ return false; // don't know; it could have caused a transaction rollback
+ }
+
public function deadlockLoop() {
$args = func_get_args();
$function = array_shift( $args );
$this->rollback( $fname, self::FLUSHING_INTERNAL );
} elseif ( $savepointId !== 'n/a' ) {
$this->doRollbackToSavepoint( $savepointId, $fname );
+ $this->trxStatus = self::STATUS_TRX_OK; // no exception; recovered
}
$this->affectedRowCount = 0; // for the sake of consistency
// Protect against mismatched atomic section, transaction nesting, and snapshot loss
if ( $this->trxLevel ) {
if ( $this->trxAtomicLevels ) {
- $levels = array_reduce( $this->trxAtomicLevels, function ( $accum, $v ) {
- return $accum === null ? $v[0] : "$accum, " . $v[0];
- } );
+ $levels = $this->flatAtomicSectionList();
$msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
throw new DBUnexpectedError( $this, $msg );
} elseif ( !$this->trxAutomatic ) {
$this->assertOpen();
$this->doBegin( $fname );
+ $this->trxStatus = self::STATUS_TRX_OK;
$this->trxAtomicCounter = 0;
$this->trxTimestamp = microtime( true );
$this->trxFname = $fname;
final public function commit( $fname = __METHOD__, $flush = '' ) {
if ( $this->trxLevel && $this->trxAtomicLevels ) {
// There are still atomic sections open. This cannot be ignored
- $levels = array_reduce( $this->trxAtomicLevels, function ( $accum, $v ) {
- return $accum === null ? $v[0] : "$accum, " . $v[0];
- } );
+ $levels = $this->flatAtomicSectionList();
throw new DBUnexpectedError(
$this,
"$fname: Got COMMIT while atomic sections $levels are still open."
$this->runOnTransactionPreCommitCallbacks();
$writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY );
$this->doCommit( $fname );
+ $this->trxStatus = self::STATUS_TRX_NONE;
if ( $this->trxDoneWrites ) {
$this->lastWriteTime = microtime( true );
$this->trxProfiler->transactionWritingOut(
$this->assertOpen();
$this->doRollback( $fname );
+ $this->trxStatus = self::STATUS_TRX_NONE;
$this->trxAtomicLevels = [];
if ( $this->trxDoneWrites ) {
$this->trxProfiler->transactionWritingOut(
}
}
+ protected function wasKnownStatementRollbackError() {
+ $errors = sqlsrv_errors( SQLSRV_ERR_ALL );
+ if ( !$errors ) {
+ return false;
+ }
+ // The transaction vs statement rollback behavior depends on XACT_ABORT, so make sure
+ // that the "statement has been terminated" error (3621) is specifically present.
+ // https://docs.microsoft.com/en-us/sql/t-sql/statements/set-xact-abort-transact-sql
+ $statementOnly = false;
+ $codeWhitelist = [ '2601', '2627', '547' ];
+ foreach ( $errors as $error ) {
+ if ( $error['code'] == '3621' ) {
+ $statementOnly = true;
+ } elseif ( !in_array( $error['code'], $codeWhitelist ) ) {
+ $statementOnly = false;
+ break;
+ }
+ }
+
+ return $statementOnly;
+ }
+
/**
* @return int
*/
return $errno == 2013 || $errno == 2006;
}
+ protected function wasKnownStatementRollbackError() {
+ $errno = $this->lastErrno();
+
+ if ( $errno === 1205 ) { // lock wait timeout
+ // Note that this is uncached to avoid stale values of SET is used
+ $row = $this->selectRow(
+ false,
+ [ 'innodb_rollback_on_timeout' => '@@innodb_rollback_on_timeout' ],
+ [],
+ __METHOD__
+ );
+ // https://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
+ // https://dev.mysql.com/doc/refman/5.5/en/innodb-parameters.html
+ return $row->innodb_rollback_on_timeout ? false : true;
+ }
+
+ // See https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html
+ return in_array( $errno, [ 1022, 1216, 1217, 1137 ], true );
+ }
+
/**
* @param string $oldName
* @param string $newName
}
}
- public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
- if ( $tempIgnore ) {
- /* Check for constraint violation */
- if ( $errno === '23505' ) {
- parent::reportQueryError( $error, $errno, $sql, $fname, $tempIgnore );
-
- return;
- }
- }
- /* Transaction stays in the ERROR state until rolled back */
- if ( $this->trxLevel ) {
- // Throw away the transaction state, then raise the error as normal.
- // Note that if this connection is managed by LBFactory, it's already expected
- // that the other transactions LBFactory manages will be rolled back.
- $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
- }
- parent::reportQueryError( $error, $errno, $sql, $fname, false );
- }
-
public function freeResult( $res ) {
if ( $res instanceof ResultWrapper ) {
$res = $res->result;
return in_array( $errno, $codes, true );
}
+ protected function wasKnownStatementRollbackError() {
+ return false; // transaction has to be rolled-back from error state
+ }
+
public function duplicateTableStructure(
$oldName, $newName, $temporary = false, $fname = __METHOD__
) {
return $errno == 17; // SQLITE_SCHEMA;
}
+ protected function wasKnownStatementRollbackError() {
+ // ON CONFLICT ROLLBACK clauses make it so that SQLITE_CONSTRAINT error is
+ // ambiguous with regard to whether it implies a ROLLBACK or an ABORT happened.
+ // https://sqlite.org/lang_createtable.html#uniqueconst
+ // https://sqlite.org/lang_conflict.html
+ return false;
+ }
+
/**
* @return string Wikitext of a link to the server software's web site
*/
* Construct a database error
* @param IDatabase $db Object which threw the error
* @param string $error A simple error message to be used for debugging
+ * @param \Exception|\Throwable|null $prev Previous exception
*/
- public function __construct( IDatabase $db = null, $error ) {
+ public function __construct( IDatabase $db = null, $error, $prev = null ) {
+ parent::__construct( $error, 0, $prev );
$this->db = $db;
- parent::__construct( $error );
}
}
/** @var string[] Message parameters */
protected $params;
- public function __construct( IDatabase $db = null, $error, array $params = [] ) {
- parent::__construct( $db, $error );
+ /**
+ * @param IDatabase|null $db
+ * @param string $error
+ * @param array $params
+ * @param \Exception|\Throwable|null $prev
+ */
+ public function __construct(
+ IDatabase $db = null, $error, array $params = [], $prev = null
+ ) {
+ parent::__construct( $db, $error, $prev );
$this->params = $params;
}
--- /dev/null
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Database
+ */
+
+namespace Wikimedia\Rdbms;
+
+/**
+ * @ingroup Database
+ */
+class DBTransactionStateError extends DBTransactionError {
+}
"apisandbox-dynamic-error-exists": "A parameter named \"$1\" already exists.",
"apisandbox-deprecated-parameters": "Deprecated parameters",
"apisandbox-fetch-token": "Auto-fill the token",
+ "apisandbox-add-multi": "Add",
"apisandbox-submit-invalid-fields-title": "Some fields are invalid",
"apisandbox-submit-invalid-fields-message": "Please correct the marked fields and try again.",
"apisandbox-results": "Results",
"apisandbox-dynamic-error-exists": "Displayed as an error message from JavaScript when trying to add a new arbitrary parameter with a name that already exists. Parameters:\n* $1 - Parameter name that failed.",
"apisandbox-deprecated-parameters": "JavaScript button label and fieldset legend for separating deprecated parameters in the UI.",
"apisandbox-fetch-token": "Label for the button that fetches a CSRF token.",
+ "apisandbox-add-multi": "Label for the button to add another value to a field that accepts multiple values",
"apisandbox-submit-invalid-fields-title": "Title for a JavaScript error message when fields are invalid.",
"apisandbox-submit-invalid-fields-message": "Content for a JavaScript error message when fields are invalid.",
"apisandbox-results": "JavaScript tab label for the tab displaying the API query results.\n{{Identical|Result}}",
'apisandbox-loading',
'apisandbox-load-error',
'apisandbox-fetch-token',
+ 'apisandbox-add-multi',
'apisandbox-helpurls',
'apisandbox-examples',
'apisandbox-dynamic-parameters',
overflow: visible;
}
+/* Display contents of the popup on a single line */
+.mw-apisandbox-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body {
+ display: table;
+}
+
+.mw-apisandbox-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body > * {
+ display: table-cell;
+}
+
+.mw-apisandbox-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body > .oo-ui-buttonWidget {
+ padding-left: 0.5em;
+ width: 1%;
+}
+
.mw-apisandbox-fullscreen #mw-apisandbox-ui {
position: fixed;
top: 0;
* @return {OO.ui.Widget}
*/
createWidgetForParameter: function ( pi, opts ) {
- var widget, innerWidget, finalWidget, items, $button, $content, func,
- multiMode = 'none';
+ var widget, innerWidget, finalWidget, items, $content, func,
+ multiModeButton = null,
+ multiModeInput = null,
+ multiModeAllowed = false;
opts = opts || {};
$.extend( widget, WidgetMethods.textInputWidget );
$.extend( widget, WidgetMethods.passwordWidget );
widget.setValidation( Validators.generic );
- multiMode = 'enter';
+ multiModeAllowed = true;
+ multiModeInput = widget;
break;
case 'integer':
if ( Util.apiBool( pi.enforcerange ) ) {
widget.setRange( pi.min || -Infinity, pi.max || Infinity );
}
- multiMode = 'enter';
+ multiModeAllowed = true;
+ multiModeInput = widget;
break;
case 'limit':
pi.apiSandboxMax = mw.config.get( 'apihighlimits' ) ? pi.highmax : pi.max;
widget.paramInfo = pi;
$.extend( widget, WidgetMethods.textInputWidget );
- multiMode = 'enter';
+ multiModeAllowed = true;
+ multiModeInput = widget;
break;
case 'timestamp':
widget.paramInfo = pi;
$.extend( widget, WidgetMethods.textInputWidget );
$.extend( widget, WidgetMethods.dateTimeInputWidget );
- multiMode = 'indicator';
+ multiModeAllowed = true;
break;
case 'upload':
break;
}
- if ( Util.apiBool( pi.multi ) && multiMode !== 'none' ) {
+ if ( Util.apiBool( pi.multi ) && multiModeAllowed ) {
innerWidget = widget;
- switch ( multiMode ) {
- case 'enter':
- $content = innerWidget.$element;
- break;
-
- case 'indicator':
- $button = innerWidget.$indicator;
- $button.css( 'cursor', 'pointer' );
- $button.attr( 'tabindex', 0 );
- $button.parent().append( $button );
- innerWidget.setIndicator( 'next' );
- $content = innerWidget.$element;
- break;
-
- default:
- throw new Error( 'Unknown multiMode "' + multiMode + '"' );
- }
+
+ multiModeButton = new OO.ui.ButtonWidget( {
+ label: mw.message( 'apisandbox-add-multi' ).text()
+ } );
+ $content = innerWidget.$element.add( multiModeButton.$element );
widget = new OO.ui.PopupTagMultiselectWidget( {
allowArbitrary: true,
return false;
}
};
- switch ( multiMode ) {
- case 'enter':
- innerWidget.connect( null, { enter: func } );
- break;
-
- case 'indicator':
- $button.on( {
- click: func,
- keypress: function ( e ) {
- if ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) {
- func();
- }
- }
- } );
- break;
+
+ if ( multiModeInput ) {
+ multiModeInput.on( 'enter', func );
}
+ multiModeButton.on( 'click', func );
}
if ( Util.apiBool( pi.required ) || opts.nooptional ) {
}
.mw-widget-sizeFilterWidget .oo-ui-textInputWidget {
- max-width: 29.5em;
+ max-width: 10em;
}
/* PHP widget */
* @param bool $appendModule
* @param User|null $user
*
+ * @throws ApiUsageException
* @return array
*/
protected function doApiRequest( array $params, array $session = null,
wfWarn( get_class( $e ) . ": {$e->getMessage()}" );
};
$this->currentDomain = DatabaseDomain::newUnspecified();
+ $this->open( 'localhost', 'testuser', 'password', 'testdb' );
}
/**
}
protected function checkFunctionName( $fname ) {
+ if ( $fname === 'Wikimedia\\Rdbms\\Database::close' ) {
+ return; // no $fname parameter
+ }
+
if ( substr( $fname, 0, strlen( $this->testName ) ) !== $this->testName ) {
throw new MWException( 'function name does not start with test class. ' .
$fname . ' vs. ' . $this->testName . '. ' .
}
function open( $server, $user, $password, $dbName ) {
- return false;
+ $this->conn = (object)[ 'test' ];
+
+ return true;
}
function fetchObject( $res ) {
}
function isOpen() {
- return true;
+ return $this->conn ? true : false;
}
function ping( &$rtt = null ) {
}
protected function closeConnection() {
- return false;
+ return true;
}
protected function doQuery( $sql ) {
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\LikeMatch;
use Wikimedia\Rdbms\Database;
+use Wikimedia\TestingAccessWrapper;
+use Wikimedia\Rdbms\DBUnexpectedError;
/**
* Test the parts of the Database abstract class that deal
}
}
+ /**
+ * @expectedException \Wikimedia\Rdbms\DBTransactionStateError
+ */
+ public function testTransactionErrorState1() {
+ $wrapper = TestingAccessWrapper::newFromObject( $this->database );
+
+ $this->database->begin( __METHOD__ );
+ $wrapper->trxStatus = Database::STATUS_TRX_ERROR;
+ $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
+ $this->database->commit( __METHOD__ );
+ }
+
+ /**
+ * @covers \Wikimedia\Rdbms\Database::query
+ */
+ public function testTransactionErrorState2() {
+ $wrapper = TestingAccessWrapper::newFromObject( $this->database );
+
+ $this->database->startAtomic( __METHOD__ );
+ $wrapper->trxStatus = Database::STATUS_TRX_ERROR;
+ $this->database->rollback( __METHOD__ );
+ $this->assertEquals( 0, $this->database->trxLevel() );
+ $this->assertEquals( Database::STATUS_TRX_NONE, $wrapper->trxStatus() );
+ $this->assertLastSql( 'BEGIN; ROLLBACK' );
+
+ $this->database->startAtomic( __METHOD__ );
+ $this->assertEquals( Database::STATUS_TRX_OK, $wrapper->trxStatus() );
+ $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
+ $this->database->endAtomic( __METHOD__ );
+ $this->assertEquals( Database::STATUS_TRX_NONE, $wrapper->trxStatus() );
+ $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'1\'; COMMIT' );
+ $this->assertEquals( 0, $this->database->trxLevel(), 'Use after rollback()' );
+
+ $this->database->begin( __METHOD__ );
+ $this->database->startAtomic( __METHOD__, Database::ATOMIC_CANCELABLE );
+ $this->database->update( 'y', [ 'a' => 1 ], [ 'field' => 1 ], __METHOD__ );
+ $wrapper->trxStatus = Database::STATUS_TRX_ERROR;
+ $this->database->cancelAtomic( __METHOD__ );
+ $this->assertEquals( Database::STATUS_TRX_OK, $wrapper->trxStatus() );
+ $this->database->startAtomic( __METHOD__ );
+ $this->database->delete( 'y', [ 'field' => 1 ], __METHOD__ );
+ $this->database->endAtomic( __METHOD__ );
+ $this->database->commit( __METHOD__ );
+ // phpcs:ignore Generic.Files.LineLength
+ $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; UPDATE y SET a = \'1\' WHERE field = \'1\'; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; DELETE FROM y WHERE field = \'1\'; COMMIT' );
+ $this->assertEquals( 0, $this->database->trxLevel(), 'Use after rollback()' );
+
+ // Next transaction
+ $this->database->startAtomic( __METHOD__ );
+ $this->assertEquals( Database::STATUS_TRX_OK, $wrapper->trxStatus() );
+ $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
+ $this->database->endAtomic( __METHOD__ );
+ $this->assertEquals( Database::STATUS_TRX_NONE, $wrapper->trxStatus() );
+ $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; COMMIT' );
+ $this->assertEquals( 0, $this->database->trxLevel() );
+ }
+
+ /**
+ * @covers \Wikimedia\Rdbms\Database::close
+ */
+ public function testPrematureClose1() {
+ $fname = __METHOD__;
+ $this->database->begin( __METHOD__ );
+ $this->database->onTransactionIdle( function () use ( $fname ) {
+ $this->database->query( 'SELECT 1', $fname );
+ } );
+ $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
+ $this->database->close();
+
+ $this->assertFalse( $this->database->isOpen() );
+ $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; COMMIT; SELECT 1' );
+ $this->assertEquals( 0, $this->database->trxLevel() );
+ }
+
+ /**
+ * @covers \Wikimedia\Rdbms\Database::close
+ */
+ public function testPrematureClose2() {
+ try {
+ $fname = __METHOD__;
+ $this->database->startAtomic( __METHOD__ );
+ $this->database->onTransactionIdle( function () use ( $fname ) {
+ $this->database->query( 'SELECT 1', $fname );
+ } );
+ $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
+ $this->database->close();
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( DBUnexpectedError $ex ) {
+ $this->assertSame(
+ 'Wikimedia\Rdbms\Database::close: atomic sections ' .
+ 'DatabaseSQLTest::testPrematureClose2 are still open.',
+ $ex->getMessage()
+ );
+ }
+
+ $this->assertFalse( $this->database->isOpen() );
+ $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; ROLLBACK' );
+ $this->assertEquals( 0, $this->database->trxLevel() );
+ }
}