6adbc758ad082242735239a5e9221ea7860e41cd
[lhc/web/wiklou.git] / tests / phpunit / includes / libs / rdbms / database / DatabaseTest.php
1 <?php
2
3 use Wikimedia\Rdbms\IDatabase;
4 use Wikimedia\Rdbms\Database;
5 use Wikimedia\Rdbms\DatabaseMysqli;
6 use Wikimedia\Rdbms\LBFactorySingle;
7 use Wikimedia\Rdbms\TransactionProfiler;
8 use Wikimedia\TestingAccessWrapper;
9
10 class DatabaseTest extends PHPUnit\Framework\TestCase {
11
12 use MediaWikiCoversValidator;
13
14 protected function setUp() {
15 $this->db = new DatabaseTestHelper( __CLASS__ . '::' . $this->getName() );
16 }
17
18 public static function provideAddQuotes() {
19 return [
20 [ null, 'NULL' ],
21 [ 1234, "'1234'" ],
22 [ 1234.5678, "'1234.5678'" ],
23 [ 'string', "'string'" ],
24 [ 'string\'s cause trouble', "'string\'s cause trouble'" ],
25 ];
26 }
27
28 /**
29 * @dataProvider provideAddQuotes
30 * @covers Wikimedia\Rdbms\Database::addQuotes
31 */
32 public function testAddQuotes( $input, $expected ) {
33 $this->assertEquals( $expected, $this->db->addQuotes( $input ) );
34 }
35
36 public static function provideTableName() {
37 // Formatting is mostly ignored since addIdentifierQuotes is abstract.
38 // For testing of addIdentifierQuotes, see actual Database subclas tests.
39 return [
40 'local' => [
41 'tablename',
42 'tablename',
43 'quoted',
44 ],
45 'local-raw' => [
46 'tablename',
47 'tablename',
48 'raw',
49 ],
50 'shared' => [
51 'sharedb.tablename',
52 'tablename',
53 'quoted',
54 [ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => '' ],
55 ],
56 'shared-raw' => [
57 'sharedb.tablename',
58 'tablename',
59 'raw',
60 [ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => '' ],
61 ],
62 'shared-prefix' => [
63 'sharedb.sh_tablename',
64 'tablename',
65 'quoted',
66 [ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => 'sh_' ],
67 ],
68 'shared-prefix-raw' => [
69 'sharedb.sh_tablename',
70 'tablename',
71 'raw',
72 [ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => 'sh_' ],
73 ],
74 'foreign' => [
75 'databasename.tablename',
76 'databasename.tablename',
77 'quoted',
78 ],
79 'foreign-raw' => [
80 'databasename.tablename',
81 'databasename.tablename',
82 'raw',
83 ],
84 ];
85 }
86
87 /**
88 * @dataProvider provideTableName
89 * @covers Wikimedia\Rdbms\Database::tableName
90 */
91 public function testTableName( $expected, $table, $format, array $alias = null ) {
92 if ( $alias ) {
93 $this->db->setTableAliases( [ $table => $alias ] );
94 }
95 $this->assertEquals(
96 $expected,
97 $this->db->tableName( $table, $format ?: 'quoted' )
98 );
99 }
100
101 public function provideTableNamesWithIndexClauseOrJOIN() {
102 return [
103 'one-element array' => [
104 [ 'table' ], [], 'table '
105 ],
106 'comma join' => [
107 [ 'table1', 'table2' ], [], 'table1,table2 '
108 ],
109 'real join' => [
110 [ 'table1', 'table2' ],
111 [ 'table2' => [ 'LEFT JOIN', 't1_id = t2_id' ] ],
112 'table1 LEFT JOIN table2 ON ((t1_id = t2_id))'
113 ],
114 'real join with multiple conditionals' => [
115 [ 'table1', 'table2' ],
116 [ 'table2' => [ 'LEFT JOIN', [ 't1_id = t2_id', 't2_x = \'X\'' ] ] ],
117 'table1 LEFT JOIN table2 ON ((t1_id = t2_id) AND (t2_x = \'X\'))'
118 ],
119 'join with parenthesized group' => [
120 [ 'table1', 'n' => [ 'table2', 'table3' ] ],
121 [
122 'table3' => [ 'JOIN', 't2_id = t3_id' ],
123 'n' => [ 'LEFT JOIN', 't1_id = t2_id' ],
124 ],
125 'table1 LEFT JOIN (table2 JOIN table3 ON ((t2_id = t3_id))) ON ((t1_id = t2_id))'
126 ],
127 'join with degenerate parenthesized group' => [
128 [ 'table1', 'n' => [ 't2' => 'table2' ] ],
129 [
130 'n' => [ 'LEFT JOIN', 't1_id = t2_id' ],
131 ],
132 'table1 LEFT JOIN table2 t2 ON ((t1_id = t2_id))'
133 ],
134 ];
135 }
136
137 /**
138 * @dataProvider provideTableNamesWithIndexClauseOrJOIN
139 * @covers Wikimedia\Rdbms\Database::tableNamesWithIndexClauseOrJOIN
140 */
141 public function testTableNamesWithIndexClauseOrJOIN( $tables, $join_conds, $expect ) {
142 $clause = TestingAccessWrapper::newFromObject( $this->db )
143 ->tableNamesWithIndexClauseOrJOIN( $tables, [], [], $join_conds );
144 $this->assertSame( $expect, $clause );
145 }
146
147 /**
148 * @covers Wikimedia\Rdbms\Database::onTransactionIdle
149 * @covers Wikimedia\Rdbms\Database::runOnTransactionIdleCallbacks
150 */
151 public function testTransactionIdle() {
152 $db = $this->db;
153
154 $db->setFlag( DBO_TRX );
155 $called = false;
156 $flagSet = null;
157 $db->onTransactionIdle(
158 function () use ( $db, &$flagSet, &$called ) {
159 $called = true;
160 $flagSet = $db->getFlag( DBO_TRX );
161 },
162 __METHOD__
163 );
164 $this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
165 $this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
166 $this->assertTrue( $called, 'Callback reached' );
167
168 $db->clearFlag( DBO_TRX );
169 $flagSet = null;
170 $db->onTransactionIdle(
171 function () use ( $db, &$flagSet ) {
172 $flagSet = $db->getFlag( DBO_TRX );
173 },
174 __METHOD__
175 );
176 $this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
177 $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
178
179 $db->clearFlag( DBO_TRX );
180 $db->onTransactionIdle(
181 function () use ( $db ) {
182 $db->setFlag( DBO_TRX );
183 },
184 __METHOD__
185 );
186 $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
187 }
188
189 /**
190 * @covers Wikimedia\Rdbms\Database::onTransactionPreCommitOrIdle
191 * @covers Wikimedia\Rdbms\Database::runOnTransactionPreCommitCallbacks
192 */
193 public function testTransactionPreCommitOrIdle() {
194 $db = $this->getMockDB( [ 'isOpen' ] );
195 $db->method( 'isOpen' )->willReturn( true );
196 $db->clearFlag( DBO_TRX );
197
198 $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX is not set' );
199
200 $called = false;
201 $db->onTransactionPreCommitOrIdle(
202 function () use ( &$called ) {
203 $called = true;
204 },
205 __METHOD__
206 );
207 $this->assertTrue( $called, 'Called when idle' );
208
209 $db->begin( __METHOD__ );
210 $called = false;
211 $db->onTransactionPreCommitOrIdle(
212 function () use ( &$called ) {
213 $called = true;
214 },
215 __METHOD__
216 );
217 $this->assertFalse( $called, 'Not called when transaction is active' );
218 $db->commit( __METHOD__ );
219 $this->assertTrue( $called, 'Called when transaction is committed' );
220 }
221
222 /**
223 * @covers Wikimedia\Rdbms\Database::onTransactionPreCommitOrIdle
224 * @covers Wikimedia\Rdbms\Database::runOnTransactionPreCommitCallbacks
225 */
226 public function testTransactionPreCommitOrIdle_TRX() {
227 $db = $this->getMockDB( [ 'isOpen' ] );
228 $db->method( 'isOpen' )->willReturn( true );
229 $db->setFlag( DBO_TRX );
230
231 $lbFactory = LBFactorySingle::newFromConnection( $db );
232 // Ask for the connectin so that LB sets internal state
233 // about this connection being the master connection
234 $lb = $lbFactory->getMainLB();
235 $conn = $lb->openConnection( $lb->getWriterIndex() );
236 $this->assertSame( $db, $conn, 'Same DB instance' );
237 $this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX is set' );
238
239 $called = false;
240 $db->onTransactionPreCommitOrIdle(
241 function () use ( &$called ) {
242 $called = true;
243 }
244 );
245 $this->assertFalse( $called, 'Not called when idle if DBO_TRX is set' );
246
247 $lbFactory->beginMasterChanges( __METHOD__ );
248 $this->assertFalse( $called, 'Not called when lb-transaction is active' );
249
250 $lbFactory->commitMasterChanges( __METHOD__ );
251 $this->assertTrue( $called, 'Called when lb-transaction is committed' );
252 }
253
254 /**
255 * @covers Wikimedia\Rdbms\Database::onTransactionResolution
256 * @covers Wikimedia\Rdbms\Database::runOnTransactionIdleCallbacks
257 */
258 public function testTransactionResolution() {
259 $db = $this->db;
260
261 $db->clearFlag( DBO_TRX );
262 $db->begin( __METHOD__ );
263 $called = false;
264 $db->onTransactionResolution( function () use ( $db, &$called ) {
265 $called = true;
266 $db->setFlag( DBO_TRX );
267 } );
268 $db->commit( __METHOD__ );
269 $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
270 $this->assertTrue( $called, 'Callback reached' );
271
272 $db->clearFlag( DBO_TRX );
273 $db->begin( __METHOD__ );
274 $called = false;
275 $db->onTransactionResolution( function () use ( $db, &$called ) {
276 $called = true;
277 $db->setFlag( DBO_TRX );
278 } );
279 $db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
280 $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
281 $this->assertTrue( $called, 'Callback reached' );
282 }
283
284 /**
285 * @covers Wikimedia\Rdbms\Database::setTransactionListener
286 */
287 public function testTransactionListener() {
288 $db = $this->db;
289
290 $db->setTransactionListener( 'ping', function () use ( $db, &$called ) {
291 $called = true;
292 } );
293
294 $called = false;
295 $db->begin( __METHOD__ );
296 $db->commit( __METHOD__ );
297 $this->assertTrue( $called, 'Callback reached' );
298
299 $called = false;
300 $db->begin( __METHOD__ );
301 $db->commit( __METHOD__ );
302 $this->assertTrue( $called, 'Callback still reached' );
303
304 $called = false;
305 $db->begin( __METHOD__ );
306 $db->rollback( __METHOD__ );
307 $this->assertTrue( $called, 'Callback reached' );
308
309 $db->setTransactionListener( 'ping', null );
310 $called = false;
311 $db->begin( __METHOD__ );
312 $db->commit( __METHOD__ );
313 $this->assertFalse( $called, 'Callback not reached' );
314 }
315
316 /**
317 * Use this mock instead of DatabaseTestHelper for cases where
318 * DatabaseTestHelper is too inflexibile due to mocking too much
319 * or being too restrictive about fname matching (e.g. for tests
320 * that assert behaviour when the name is a mismatch, we need to
321 * catch the error here instead of there).
322 *
323 * @return Database
324 */
325 private function getMockDB( $methods = [] ) {
326 static $abstractMethods = [
327 'fetchAffectedRowCount',
328 'closeConnection',
329 'dataSeek',
330 'doQuery',
331 'fetchObject', 'fetchRow',
332 'fieldInfo', 'fieldName',
333 'getSoftwareLink', 'getServerVersion',
334 'getType',
335 'indexInfo',
336 'insertId',
337 'lastError', 'lastErrno',
338 'numFields', 'numRows',
339 'open',
340 'strencode',
341 ];
342 $db = $this->getMockBuilder( Database::class )
343 ->disableOriginalConstructor()
344 ->setMethods( array_values( array_unique( array_merge(
345 $abstractMethods,
346 $methods
347 ) ) ) )
348 ->getMock();
349 $wdb = TestingAccessWrapper::newFromObject( $db );
350 $wdb->trxProfiler = new TransactionProfiler();
351 $wdb->connLogger = new \Psr\Log\NullLogger();
352 $wdb->queryLogger = new \Psr\Log\NullLogger();
353 return $db;
354 }
355
356 /**
357 * @covers Wikimedia\Rdbms\Database::flushSnapshot
358 */
359 public function testFlushSnapshot() {
360 $db = $this->getMockDB( [ 'isOpen' ] );
361 $db->method( 'isOpen' )->willReturn( true );
362
363 $db->flushSnapshot( __METHOD__ ); // ok
364 $db->flushSnapshot( __METHOD__ ); // ok
365
366 $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
367 $db->query( 'SELECT 1', __METHOD__ );
368 $this->assertTrue( (bool)$db->trxLevel(), "Transaction started." );
369 $db->flushSnapshot( __METHOD__ ); // ok
370 $db->restoreFlags( $db::RESTORE_PRIOR );
371
372 $this->assertFalse( (bool)$db->trxLevel(), "Transaction cleared." );
373 }
374
375 /**
376 * @covers Wikimedia\Rdbms\Database::getScopedLockAndFlush
377 * @covers Wikimedia\Rdbms\Database::lock
378 * @covers Wikimedia\Rdbms\Database::unlock
379 * @covers Wikimedia\Rdbms\Database::lockIsFree
380 */
381 public function testGetScopedLock() {
382 $db = $this->getMockDB( [ 'isOpen' ] );
383 $db->method( 'isOpen' )->willReturn( true );
384
385 $this->assertEquals( 0, $db->trxLevel() );
386 $this->assertEquals( true, $db->lockIsFree( 'x', __METHOD__ ) );
387 $this->assertEquals( true, $db->lock( 'x', __METHOD__ ) );
388 $this->assertEquals( false, $db->lockIsFree( 'x', __METHOD__ ) );
389 $this->assertEquals( true, $db->unlock( 'x', __METHOD__ ) );
390 $this->assertEquals( true, $db->lockIsFree( 'x', __METHOD__ ) );
391 $this->assertEquals( 0, $db->trxLevel() );
392
393 $db->setFlag( DBO_TRX );
394 $this->assertEquals( true, $db->lockIsFree( 'x', __METHOD__ ) );
395 $this->assertEquals( true, $db->lock( 'x', __METHOD__ ) );
396 $this->assertEquals( false, $db->lockIsFree( 'x', __METHOD__ ) );
397 $this->assertEquals( true, $db->unlock( 'x', __METHOD__ ) );
398 $this->assertEquals( true, $db->lockIsFree( 'x', __METHOD__ ) );
399 $db->clearFlag( DBO_TRX );
400
401 $this->assertEquals( 0, $db->trxLevel() );
402
403 $db->setFlag( DBO_TRX );
404 try {
405 $this->badLockingMethodImplicit( $db );
406 } catch ( RunTimeException $e ) {
407 $this->assertTrue( $db->trxLevel() > 0, "Transaction not committed." );
408 }
409 $db->clearFlag( DBO_TRX );
410 $db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
411 $this->assertTrue( $db->lockIsFree( 'meow', __METHOD__ ) );
412
413 try {
414 $this->badLockingMethodExplicit( $db );
415 } catch ( RunTimeException $e ) {
416 $this->assertTrue( $db->trxLevel() > 0, "Transaction not committed." );
417 }
418 $db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
419 $this->assertTrue( $db->lockIsFree( 'meow', __METHOD__ ) );
420 }
421
422 private function badLockingMethodImplicit( IDatabase $db ) {
423 $lock = $db->getScopedLockAndFlush( 'meow', __METHOD__, 1 );
424 $db->query( "SELECT 1" ); // trigger DBO_TRX
425 throw new RunTimeException( "Uh oh!" );
426 }
427
428 private function badLockingMethodExplicit( IDatabase $db ) {
429 $lock = $db->getScopedLockAndFlush( 'meow', __METHOD__, 1 );
430 $db->begin( __METHOD__ );
431 throw new RunTimeException( "Uh oh!" );
432 }
433
434 /**
435 * @covers Wikimedia\Rdbms\Database::getFlag
436 * @covers Wikimedia\Rdbms\Database::setFlag
437 * @covers Wikimedia\Rdbms\Database::restoreFlags
438 */
439 public function testFlagSetting() {
440 $db = $this->db;
441 $origTrx = $db->getFlag( DBO_TRX );
442 $origSsl = $db->getFlag( DBO_SSL );
443
444 $origTrx
445 ? $db->clearFlag( DBO_TRX, $db::REMEMBER_PRIOR )
446 : $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
447 $this->assertEquals( !$origTrx, $db->getFlag( DBO_TRX ) );
448
449 $origSsl
450 ? $db->clearFlag( DBO_SSL, $db::REMEMBER_PRIOR )
451 : $db->setFlag( DBO_SSL, $db::REMEMBER_PRIOR );
452 $this->assertEquals( !$origSsl, $db->getFlag( DBO_SSL ) );
453
454 $db->restoreFlags( $db::RESTORE_INITIAL );
455 $this->assertEquals( $origTrx, $db->getFlag( DBO_TRX ) );
456 $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
457
458 $origTrx
459 ? $db->clearFlag( DBO_TRX, $db::REMEMBER_PRIOR )
460 : $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
461 $origSsl
462 ? $db->clearFlag( DBO_SSL, $db::REMEMBER_PRIOR )
463 : $db->setFlag( DBO_SSL, $db::REMEMBER_PRIOR );
464
465 $db->restoreFlags();
466 $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
467 $this->assertEquals( !$origTrx, $db->getFlag( DBO_TRX ) );
468
469 $db->restoreFlags();
470 $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
471 $this->assertEquals( $origTrx, $db->getFlag( DBO_TRX ) );
472 }
473
474 /**
475 * @expectedException UnexpectedValueException
476 * @covers Wikimedia\Rdbms\Database::setFlag
477 */
478 public function testDBOIgnoreSet() {
479 $db = $this->getMockBuilder( DatabaseMysqli::class )
480 ->disableOriginalConstructor()
481 ->setMethods( null )
482 ->getMock();
483
484 $db->setFlag( Database::DBO_IGNORE );
485 }
486
487 /**
488 * @expectedException UnexpectedValueException
489 * @covers Wikimedia\Rdbms\Database::clearFlag
490 */
491 public function testDBOIgnoreClear() {
492 $db = $this->getMockBuilder( DatabaseMysqli::class )
493 ->disableOriginalConstructor()
494 ->setMethods( null )
495 ->getMock();
496
497 $db->clearFlag( Database::DBO_IGNORE );
498 }
499
500 /**
501 * @covers Wikimedia\Rdbms\Database::tablePrefix
502 * @covers Wikimedia\Rdbms\Database::dbSchema
503 */
504 public function testMutators() {
505 $old = $this->db->tablePrefix();
506 $this->assertInternalType( 'string', $old, 'Prefix is string' );
507 $this->assertEquals( $old, $this->db->tablePrefix(), "Prefix unchanged" );
508 $this->assertEquals( $old, $this->db->tablePrefix( 'xxx' ) );
509 $this->assertEquals( 'xxx', $this->db->tablePrefix(), "Prefix set" );
510 $this->db->tablePrefix( $old );
511 $this->assertNotEquals( 'xxx', $this->db->tablePrefix() );
512
513 $old = $this->db->dbSchema();
514 $this->assertInternalType( 'string', $old, 'Schema is string' );
515 $this->assertEquals( $old, $this->db->dbSchema(), "Schema unchanged" );
516 $this->assertEquals( $old, $this->db->dbSchema( 'xxx' ) );
517 $this->assertEquals( 'xxx', $this->db->dbSchema(), "Schema set" );
518 $this->db->dbSchema( $old );
519 $this->assertNotEquals( 'xxx', $this->db->dbSchema() );
520 }
521 }