Handle all errors in query() that might have caused rollback by putting the Database handle into an error state that can only be resolved by cancelAtomic() or rollback(). Other queries will be rejected until then. This results in more immediate exceptions in some cases where atomic section mismatch errors would have been thrown, such as a an error bubbling up from a child atomic section. Most cases were a try/catch block assumes that only the statement was rolled back now result in an error and rollback. Callers using try/catch to handle key conflicts should instead use SELECT FOR UPDATE to find conflicts beforehand, or use IGNORE, or the upsert()/replace() methods. The try/catch pattern is unsafe and no longer allowed, except for some common errors known to just rollback the statement. Even then, such statements can come from child atomic sections, so committing would be unsafe. Luckily, in such cases, there will be a mismatch detected on endAtomic() or a dangling section detected in close(), resulting in rollback. Remove caching from DatabaseMyslBase::getServerVariableSettings in case some SET query changes the values. Bug: T189999 Change-Id: I532bc5201681a915d0c8aa7a3b1c143b040b142e
228 lines
5 KiB
PHP
228 lines
5 KiB
PHP
<?php
|
|
|
|
use Wikimedia\Rdbms\TransactionProfiler;
|
|
use Wikimedia\Rdbms\DatabaseDomain;
|
|
use Wikimedia\Rdbms\Database;
|
|
|
|
/**
|
|
* Helper for testing the methods from the Database class
|
|
* @since 1.22
|
|
*/
|
|
class DatabaseTestHelper extends Database {
|
|
|
|
/**
|
|
* __CLASS__ of the test suite,
|
|
* used to determine, if the function name is passed every time to query()
|
|
*/
|
|
protected $testName = [];
|
|
|
|
/**
|
|
* Array of lastSqls passed to query(),
|
|
* This is an array since some methods in Database can do more than one
|
|
* query. Cleared when calling getLastSqls().
|
|
*/
|
|
protected $lastSqls = [];
|
|
|
|
/** @var array List of row arrays */
|
|
protected $nextResult = [];
|
|
|
|
/**
|
|
* Array of tables to be considered as existing by tableExist()
|
|
* Use setExistingTables() to alter.
|
|
*/
|
|
protected $tablesExists;
|
|
|
|
/**
|
|
* Value to return from unionSupportsOrderAndLimit()
|
|
*/
|
|
protected $unionSupportsOrderAndLimit = true;
|
|
|
|
public function __construct( $testName, array $opts = [] ) {
|
|
$this->testName = $testName;
|
|
|
|
$this->profiler = new ProfilerStub( [] );
|
|
$this->trxProfiler = new TransactionProfiler();
|
|
$this->cliMode = isset( $opts['cliMode'] ) ? $opts['cliMode'] : true;
|
|
$this->connLogger = new \Psr\Log\NullLogger();
|
|
$this->queryLogger = new \Psr\Log\NullLogger();
|
|
$this->errorLogger = function ( Exception $e ) {
|
|
wfWarn( get_class( $e ) . ": {$e->getMessage()}" );
|
|
};
|
|
$this->currentDomain = DatabaseDomain::newUnspecified();
|
|
$this->open( 'localhost', 'testuser', 'password', 'testdb' );
|
|
}
|
|
|
|
/**
|
|
* Returns SQL queries grouped by '; '
|
|
* Clear the list of queries that have been done so far.
|
|
* @return string
|
|
*/
|
|
public function getLastSqls() {
|
|
$lastSqls = implode( '; ', $this->lastSqls );
|
|
$this->lastSqls = [];
|
|
|
|
return $lastSqls;
|
|
}
|
|
|
|
public function setExistingTables( $tablesExists ) {
|
|
$this->tablesExists = (array)$tablesExists;
|
|
}
|
|
|
|
/**
|
|
* @param mixed $res Use an array of row arrays to set row result
|
|
*/
|
|
public function forceNextResult( $res ) {
|
|
$this->nextResult = $res;
|
|
}
|
|
|
|
protected function addSql( $sql ) {
|
|
// clean up spaces before and after some words and the whole string
|
|
$this->lastSqls[] = trim( preg_replace(
|
|
'/\s{2,}(?=FROM|WHERE|GROUP BY|ORDER BY|LIMIT)|(?<=SELECT|INSERT|UPDATE)\s{2,}/',
|
|
' ', $sql
|
|
) );
|
|
}
|
|
|
|
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 . '. ' .
|
|
'Please provide __METHOD__ to database methods.' );
|
|
}
|
|
}
|
|
|
|
function strencode( $s ) {
|
|
// Choose apos to avoid handling of escaping double quotes in quoted text
|
|
return str_replace( "'", "\'", $s );
|
|
}
|
|
|
|
public function addIdentifierQuotes( $s ) {
|
|
// no escaping to avoid handling of double quotes in quoted text
|
|
return $s;
|
|
}
|
|
|
|
public function query( $sql, $fname = '', $tempIgnore = false ) {
|
|
$this->checkFunctionName( $fname );
|
|
$this->addSql( $sql );
|
|
|
|
return parent::query( $sql, $fname, $tempIgnore );
|
|
}
|
|
|
|
public function tableExists( $table, $fname = __METHOD__ ) {
|
|
$tableRaw = $this->tableName( $table, 'raw' );
|
|
if ( isset( $this->sessionTempTables[$tableRaw] ) ) {
|
|
return true; // already known to exist
|
|
}
|
|
|
|
$this->checkFunctionName( $fname );
|
|
|
|
return in_array( $table, (array)$this->tablesExists );
|
|
}
|
|
|
|
// Redeclare parent method to make it public
|
|
public function nativeReplace( $table, $rows, $fname ) {
|
|
return parent::nativeReplace( $table, $rows, $fname );
|
|
}
|
|
|
|
function getType() {
|
|
return 'test';
|
|
}
|
|
|
|
function open( $server, $user, $password, $dbName ) {
|
|
$this->conn = (object)[ 'test' ];
|
|
|
|
return true;
|
|
}
|
|
|
|
function fetchObject( $res ) {
|
|
return false;
|
|
}
|
|
|
|
function fetchRow( $res ) {
|
|
return false;
|
|
}
|
|
|
|
function numRows( $res ) {
|
|
return -1;
|
|
}
|
|
|
|
function numFields( $res ) {
|
|
return -1;
|
|
}
|
|
|
|
function fieldName( $res, $n ) {
|
|
return 'test';
|
|
}
|
|
|
|
function insertId() {
|
|
return -1;
|
|
}
|
|
|
|
function dataSeek( $res, $row ) {
|
|
/* nop */
|
|
}
|
|
|
|
function lastErrno() {
|
|
return -1;
|
|
}
|
|
|
|
function lastError() {
|
|
return 'test';
|
|
}
|
|
|
|
function fieldInfo( $table, $field ) {
|
|
return false;
|
|
}
|
|
|
|
function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) {
|
|
return false;
|
|
}
|
|
|
|
function fetchAffectedRowCount() {
|
|
return -1;
|
|
}
|
|
|
|
function getSoftwareLink() {
|
|
return 'test';
|
|
}
|
|
|
|
function getServerVersion() {
|
|
return 'test';
|
|
}
|
|
|
|
function getServerInfo() {
|
|
return 'test';
|
|
}
|
|
|
|
function isOpen() {
|
|
return $this->conn ? true : false;
|
|
}
|
|
|
|
function ping( &$rtt = null ) {
|
|
$rtt = 0.0;
|
|
return true;
|
|
}
|
|
|
|
protected function closeConnection() {
|
|
return true;
|
|
}
|
|
|
|
protected function doQuery( $sql ) {
|
|
$res = $this->nextResult;
|
|
$this->nextResult = [];
|
|
|
|
return new FakeResultWrapper( $res );
|
|
}
|
|
|
|
public function unionSupportsOrderAndLimit() {
|
|
return $this->unionSupportsOrderAndLimit;
|
|
}
|
|
|
|
public function setUnionSupportsOrderAndLimit( $v ) {
|
|
$this->unionSupportsOrderAndLimit = (bool)$v;
|
|
}
|
|
}
|