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