3 use MediaWiki\Tests\Unit\Libs\Rdbms\AddQuoterMock
;
4 use MediaWiki\Tests\Unit\Libs\Rdbms\SQLPlatformTestHelper
;
5 use Psr\Log\NullLogger
;
6 use Wikimedia\ObjectCache\HashBagOStuff
;
7 use Wikimedia\Rdbms\Database
;
8 use Wikimedia\Rdbms\Database\DatabaseFlags
;
9 use Wikimedia\Rdbms\DatabaseDomain
;
10 use Wikimedia\Rdbms\FakeResultWrapper
;
11 use Wikimedia\Rdbms\QueryStatus
;
12 use Wikimedia\Rdbms\Replication\ReplicationReporter
;
13 use Wikimedia\Rdbms\TransactionProfiler
;
14 use Wikimedia\RequestTimeout\RequestTimeout
;
17 * Helper for testing the methods from the Database class
20 class DatabaseTestHelper
extends Database
{
23 * @var string __CLASS__ of the test suite,
24 * used to determine, if the function name is passed every time to query()
26 protected string $testName;
29 * @var string[] Array of lastSqls passed to query(),
30 * This is an array since some methods in Database can do more than one
31 * query. Cleared when calling getLastSqls().
33 protected $lastSqls = [];
35 /** @var array Stack of result maps */
36 protected $nextResMapQueue = [];
38 /** @var array|null */
39 protected $lastResMap = null;
42 * @var string[] Array of tables to be considered as existing by tableExist()
43 * Use setExistingTables() to alter.
45 protected $tablesExists;
48 protected $forcedAffectedCountQueue = [];
50 public function __construct( string $testName, array $opts = [] ) {
62 'topologyRole' => null,
63 'srvCache' => new HashBagOStuff(),
65 'trxProfiler' => new TransactionProfiler(),
66 'logger' => new NullLogger(),
67 'errorLogger' => static function ( Exception
$e ) {
68 wfWarn( get_class( $e ) . ': ' . $e->getMessage() );
70 'deprecationLogger' => static function ( $msg ) {
73 'criticalSectionProvider' =>
74 RequestTimeout
::singleton()->createCriticalSectionProvider( 120 )
76 parent
::__construct( $params );
78 $this->testName
= $testName;
79 $this->platform
= new SQLPlatformTestHelper( new AddQuoterMock() );
80 $this->flagsHolder
= new DatabaseFlags( 0 );
81 $this->replicationReporter
= new ReplicationReporter(
82 $params['topologyRole'],
87 $this->currentDomain
= DatabaseDomain
::newUnspecified();
88 $this->open( 'localhost', 'testuser', 'password', 'testdb', null, '' );
92 * Returns SQL queries grouped by '; '
93 * Clear the list of queries that have been done so far.
96 public function getLastSqls() {
97 $lastSqls = implode( '; ', $this->lastSqls
);
103 public function setExistingTables( $tablesExists ) {
104 $this->tablesExists
= (array)$tablesExists;
108 * @param mixed $res Use an array of row arrays to set row result
109 * @param int $errno Error number
110 * @param string $error Error text
111 * @param array $options
112 * - isKnownStatementRollbackError: Return value for isKnownStatementRollbackError()
114 public function forceNextResult( $res, $errno = 0, $error = '', $options = [] ) {
115 $this->nextResMapQueue
[] = [
122 protected function addSql( $sql ) {
123 // clean up spaces before and after some words and the whole string
124 $this->lastSqls
[] = trim( preg_replace(
125 '/\s{2,}(?=FROM|WHERE|GROUP BY|ORDER BY|LIMIT)|(?<=SELECT|INSERT|UPDATE)\s{2,}/',
130 protected function checkFunctionName( $fname ) {
131 if ( $fname === 'Wikimedia\\Rdbms\\Database::close' ) {
132 return; // no $fname parameter
135 // Handle some internal calls from the Database class
138 '/^Wikimedia\\\\Rdbms\\\\Database::(?:query|beginIfImplied) \((.+)\)$/',
145 if ( !str_starts_with( $check, $this->testName
) ) {
146 throw new LogicException( 'function name does not start with test class. ' .
147 $fname . ' vs. ' . $this->testName
. '. ' .
148 'Please provide __METHOD__ to database methods.' );
152 public function strencode( $s ) {
153 // Choose apos to avoid handling of escaping double quotes in quoted text
154 return str_replace( "'", "\'", $s );
157 public function query( $sql, $fname = '', $flags = 0 ) {
158 $this->checkFunctionName( $fname );
160 return parent
::query( $sql, $fname, $flags );
163 public function tableExists( $table, $fname = __METHOD__
) {
164 [ $db, $pt ] = $this->platform
->getDatabaseAndTableIdentifier( $table );
165 if ( isset( $this->sessionTempTables
[$db][$pt] ) ) {
166 return true; // already known to exist
169 $this->checkFunctionName( $fname );
171 return in_array( $table, (array)$this->tablesExists
);
174 public function getType() {
178 public function open( $server, $user, $password, $db, $schema, $tablePrefix ) {
179 $this->conn
= (object)[ 'test' ];
184 protected function lastInsertId() {
188 public function lastErrno() {
189 return $this->lastResMap ?
$this->lastResMap
['errno'] : -1;
192 public function lastError() {
193 return $this->lastResMap ?
$this->lastResMap
['error'] : 'test';
196 protected function isKnownStatementRollbackError( $errno ) {
197 return ( $this->lastResMap
['errno'] ??
0 ) === $errno
198 ?
( $this->lastResMap
['isKnownStatementRollbackError'] ??
false )
202 public function fieldInfo( $table, $field ) {
206 public function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) {
210 public function getSoftwareLink() {
214 public function getServerVersion() {
218 public function getServerInfo() {
222 public function ping( &$rtt = null ) {
227 protected function closeConnection() {
231 public function setNextQueryAffectedRowCounts( array $counts ) {
232 $this->forcedAffectedCountQueue
= $counts;
235 protected function doSingleStatementQuery( string $sql ): QueryStatus
{
236 $sql = preg_replace( '< /\* .+? \*/>', '', $sql );
237 $this->addSql( $sql );
239 if ( $this->nextResMapQueue
) {
240 $this->lastResMap
= array_shift( $this->nextResMapQueue
);
241 if ( !$this->lastResMap
['errno'] && $this->forcedAffectedCountQueue
) {
242 $count = array_shift( $this->forcedAffectedCountQueue
);
243 $this->lastQueryAffectedRows
= $count;
246 $this->lastResMap
= [ 'res' => [], 'errno' => 0, 'error' => '' ];
248 $res = $this->lastResMap
['res'];
250 return new QueryStatus(
251 is_bool( $res ) ?
$res : new FakeResultWrapper( $res ),
252 $this->affectedRows(),