wiki.techinc.nl/tests/phpunit/includes/db/DatabaseTestHelper.php
Aaron Schulz 3975e04cf4 rdbms: make Database query error handling more strict
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
2018-04-04 21:26:11 -07:00

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;
}
}