rdbms: rename onTransactionIdle() to onTransactionCommitOrIdle()
[lhc/web/wiklou.git] / tests / phpunit / includes / libs / rdbms / database / DatabaseSQLTest.php
1 <?php
2
3 use Wikimedia\Rdbms\IDatabase;
4 use Wikimedia\Rdbms\LikeMatch;
5 use Wikimedia\Rdbms\Database;
6 use Wikimedia\TestingAccessWrapper;
7 use Wikimedia\Rdbms\DBTransactionStateError;
8 use Wikimedia\Rdbms\DBUnexpectedError;
9 use Wikimedia\Rdbms\DBTransactionError;
10
11 /**
12 * Test the parts of the Database abstract class that deal
13 * with creating SQL text.
14 */
15 class DatabaseSQLTest extends PHPUnit\Framework\TestCase {
16
17 use MediaWikiCoversValidator;
18 use PHPUnit4And6Compat;
19
20 /** @var DatabaseTestHelper|Database */
21 private $database;
22
23 protected function setUp() {
24 parent::setUp();
25 $this->database = new DatabaseTestHelper( __CLASS__, [ 'cliMode' => true ] );
26 }
27
28 protected function assertLastSql( $sqlText ) {
29 $this->assertEquals(
30 $sqlText,
31 $this->database->getLastSqls()
32 );
33 }
34
35 protected function assertLastSqlDb( $sqlText, DatabaseTestHelper $db ) {
36 $this->assertEquals( $sqlText, $db->getLastSqls() );
37 }
38
39 /**
40 * @dataProvider provideSelect
41 * @covers Wikimedia\Rdbms\Database::select
42 * @covers Wikimedia\Rdbms\Database::selectSQLText
43 * @covers Wikimedia\Rdbms\Database::tableNamesWithIndexClauseOrJOIN
44 * @covers Wikimedia\Rdbms\Database::useIndexClause
45 * @covers Wikimedia\Rdbms\Database::ignoreIndexClause
46 * @covers Wikimedia\Rdbms\Database::makeSelectOptions
47 * @covers Wikimedia\Rdbms\Database::makeOrderBy
48 * @covers Wikimedia\Rdbms\Database::makeGroupByWithHaving
49 * @covers Wikimedia\Rdbms\Database::selectFieldsOrOptionsAggregate
50 * @covers Wikimedia\Rdbms\Database::selectOptionsIncludeLocking
51 */
52 public function testSelect( $sql, $sqlText ) {
53 $this->database->select(
54 $sql['tables'],
55 $sql['fields'],
56 isset( $sql['conds'] ) ? $sql['conds'] : [],
57 __METHOD__,
58 isset( $sql['options'] ) ? $sql['options'] : [],
59 isset( $sql['join_conds'] ) ? $sql['join_conds'] : []
60 );
61 $this->assertLastSql( $sqlText );
62 }
63
64 public static function provideSelect() {
65 return [
66 [
67 [
68 'tables' => 'table',
69 'fields' => [ 'field', 'alias' => 'field2' ],
70 'conds' => [ 'alias' => 'text' ],
71 ],
72 "SELECT field,field2 AS alias " .
73 "FROM table " .
74 "WHERE alias = 'text'"
75 ],
76 [
77 [
78 'tables' => 'table',
79 'fields' => [ 'field', 'alias' => 'field2' ],
80 'conds' => 'alias = \'text\'',
81 ],
82 "SELECT field,field2 AS alias " .
83 "FROM table " .
84 "WHERE alias = 'text'"
85 ],
86 [
87 [
88 'tables' => 'table',
89 'fields' => [ 'field', 'alias' => 'field2' ],
90 'conds' => [],
91 ],
92 "SELECT field,field2 AS alias " .
93 "FROM table"
94 ],
95 [
96 [
97 'tables' => 'table',
98 'fields' => [ 'field', 'alias' => 'field2' ],
99 'conds' => '',
100 ],
101 "SELECT field,field2 AS alias " .
102 "FROM table"
103 ],
104 [
105 [
106 'tables' => 'table',
107 'fields' => [ 'field', 'alias' => 'field2' ],
108 'conds' => '0', // T188314
109 ],
110 "SELECT field,field2 AS alias " .
111 "FROM table " .
112 "WHERE 0"
113 ],
114 [
115 [
116 // 'tables' with space prepended indicates pre-escaped table name
117 'tables' => ' table LEFT JOIN table2',
118 'fields' => [ 'field' ],
119 'conds' => [ 'field' => 'text' ],
120 ],
121 "SELECT field FROM table LEFT JOIN table2 WHERE field = 'text'"
122 ],
123 [
124 [
125 // Empty 'tables' is allowed
126 'tables' => '',
127 'fields' => [ 'SPECIAL_QUERY()' ],
128 ],
129 "SELECT SPECIAL_QUERY()"
130 ],
131 [
132 [
133 'tables' => 'table',
134 'fields' => [ 'field', 'alias' => 'field2' ],
135 'conds' => [ 'alias' => 'text' ],
136 'options' => [ 'LIMIT' => 1, 'ORDER BY' => 'field' ],
137 ],
138 "SELECT field,field2 AS alias " .
139 "FROM table " .
140 "WHERE alias = 'text' " .
141 "ORDER BY field " .
142 "LIMIT 1"
143 ],
144 [
145 [
146 'tables' => [ 'table', 't2' => 'table2' ],
147 'fields' => [ 'tid', 'field', 'alias' => 'field2', 't2.id' ],
148 'conds' => [ 'alias' => 'text' ],
149 'options' => [ 'LIMIT' => 1, 'ORDER BY' => 'field' ],
150 'join_conds' => [ 't2' => [
151 'LEFT JOIN', 'tid = t2.id'
152 ] ],
153 ],
154 "SELECT tid,field,field2 AS alias,t2.id " .
155 "FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " .
156 "WHERE alias = 'text' " .
157 "ORDER BY field " .
158 "LIMIT 1"
159 ],
160 [
161 [
162 'tables' => [ 'table', 't2' => 'table2' ],
163 'fields' => [ 'tid', 'field', 'alias' => 'field2', 't2.id' ],
164 'conds' => [ 'alias' => 'text' ],
165 'options' => [ 'LIMIT' => 1, 'GROUP BY' => 'field', 'HAVING' => 'COUNT(*) > 1' ],
166 'join_conds' => [ 't2' => [
167 'LEFT JOIN', 'tid = t2.id'
168 ] ],
169 ],
170 "SELECT tid,field,field2 AS alias,t2.id " .
171 "FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " .
172 "WHERE alias = 'text' " .
173 "GROUP BY field HAVING COUNT(*) > 1 " .
174 "LIMIT 1"
175 ],
176 [
177 [
178 'tables' => [ 'table', 't2' => 'table2' ],
179 'fields' => [ 'tid', 'field', 'alias' => 'field2', 't2.id' ],
180 'conds' => [ 'alias' => 'text' ],
181 'options' => [
182 'LIMIT' => 1,
183 'GROUP BY' => [ 'field', 'field2' ],
184 'HAVING' => [ 'COUNT(*) > 1', 'field' => 1 ]
185 ],
186 'join_conds' => [ 't2' => [
187 'LEFT JOIN', 'tid = t2.id'
188 ] ],
189 ],
190 "SELECT tid,field,field2 AS alias,t2.id " .
191 "FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " .
192 "WHERE alias = 'text' " .
193 "GROUP BY field,field2 HAVING (COUNT(*) > 1) AND field = '1' " .
194 "LIMIT 1"
195 ],
196 [
197 [
198 'tables' => [ 'table' ],
199 'fields' => [ 'alias' => 'field' ],
200 'conds' => [ 'alias' => [ 1, 2, 3, 4 ] ],
201 ],
202 "SELECT field AS alias " .
203 "FROM table " .
204 "WHERE alias IN ('1','2','3','4')"
205 ],
206 [
207 [
208 'tables' => 'table',
209 'fields' => [ 'field' ],
210 'options' => [ 'USE INDEX' => [ 'table' => 'X' ] ],
211 ],
212 // No-op by default
213 "SELECT field FROM table"
214 ],
215 [
216 [
217 'tables' => 'table',
218 'fields' => [ 'field' ],
219 'options' => [ 'IGNORE INDEX' => [ 'table' => 'X' ] ],
220 ],
221 // No-op by default
222 "SELECT field FROM table"
223 ],
224 [
225 [
226 'tables' => 'table',
227 'fields' => [ 'field' ],
228 'options' => [ 'DISTINCT' ],
229 ],
230 "SELECT DISTINCT field FROM table"
231 ],
232 [
233 [
234 'tables' => 'table',
235 'fields' => [ 'field' ],
236 'options' => [ 'LOCK IN SHARE MODE' ],
237 ],
238 "SELECT field FROM table LOCK IN SHARE MODE"
239 ],
240 [
241 [
242 'tables' => 'table',
243 'fields' => [ 'field' ],
244 'options' => [ 'EXPLAIN' => true ],
245 ],
246 'EXPLAIN SELECT field FROM table'
247 ],
248 [
249 [
250 'tables' => 'table',
251 'fields' => [ 'field' ],
252 'options' => [ 'FOR UPDATE' ],
253 ],
254 "SELECT field FROM table FOR UPDATE"
255 ],
256 ];
257 }
258
259 /**
260 * @covers Wikimedia\Rdbms\Subquery
261 * @dataProvider provideSelectRowCount
262 * @param $sql
263 * @param $sqlText
264 */
265 public function testSelectRowCount( $sql, $sqlText ) {
266 $this->database->selectRowCount(
267 $sql['tables'],
268 $sql['field'],
269 isset( $sql['conds'] ) ? $sql['conds'] : [],
270 __METHOD__,
271 isset( $sql['options'] ) ? $sql['options'] : [],
272 isset( $sql['join_conds'] ) ? $sql['join_conds'] : []
273 );
274 $this->assertLastSql( $sqlText );
275 }
276
277 public static function provideSelectRowCount() {
278 return [
279 [
280 [
281 'tables' => 'table',
282 'field' => [ '*' ],
283 'conds' => [ 'field' => 'text' ],
284 ],
285 "SELECT COUNT(*) AS rowcount FROM " .
286 "(SELECT 1 FROM table WHERE field = 'text' ) tmp_count"
287 ],
288 [
289 [
290 'tables' => 'table',
291 'field' => [ 'column' ],
292 'conds' => [ 'field' => 'text' ],
293 ],
294 "SELECT COUNT(*) AS rowcount FROM " .
295 "(SELECT 1 FROM table WHERE field = 'text' AND (column IS NOT NULL) ) tmp_count"
296 ],
297 [
298 [
299 'tables' => 'table',
300 'field' => [ 'alias' => 'column' ],
301 'conds' => [ 'field' => 'text' ],
302 ],
303 "SELECT COUNT(*) AS rowcount FROM " .
304 "(SELECT 1 FROM table WHERE field = 'text' AND (column IS NOT NULL) ) tmp_count"
305 ],
306 [
307 [
308 'tables' => 'table',
309 'field' => [ 'alias' => 'column' ],
310 'conds' => '',
311 ],
312 "SELECT COUNT(*) AS rowcount FROM " .
313 "(SELECT 1 FROM table WHERE (column IS NOT NULL) ) tmp_count"
314 ],
315 [
316 [
317 'tables' => 'table',
318 'field' => [ 'alias' => 'column' ],
319 'conds' => false,
320 ],
321 "SELECT COUNT(*) AS rowcount FROM " .
322 "(SELECT 1 FROM table WHERE (column IS NOT NULL) ) tmp_count"
323 ],
324 [
325 [
326 'tables' => 'table',
327 'field' => [ 'alias' => 'column' ],
328 'conds' => null,
329 ],
330 "SELECT COUNT(*) AS rowcount FROM " .
331 "(SELECT 1 FROM table WHERE (column IS NOT NULL) ) tmp_count"
332 ],
333 [
334 [
335 'tables' => 'table',
336 'field' => [ 'alias' => 'column' ],
337 'conds' => '1',
338 ],
339 "SELECT COUNT(*) AS rowcount FROM " .
340 "(SELECT 1 FROM table WHERE (1) AND (column IS NOT NULL) ) tmp_count"
341 ],
342 [
343 [
344 'tables' => 'table',
345 'field' => [ 'alias' => 'column' ],
346 'conds' => '0',
347 ],
348 "SELECT COUNT(*) AS rowcount FROM " .
349 "(SELECT 1 FROM table WHERE (0) AND (column IS NOT NULL) ) tmp_count"
350 ],
351 ];
352 }
353
354 /**
355 * @dataProvider provideUpdate
356 * @covers Wikimedia\Rdbms\Database::update
357 * @covers Wikimedia\Rdbms\Database::makeUpdateOptions
358 * @covers Wikimedia\Rdbms\Database::makeUpdateOptionsArray
359 */
360 public function testUpdate( $sql, $sqlText ) {
361 $this->database->update(
362 $sql['table'],
363 $sql['values'],
364 $sql['conds'],
365 __METHOD__,
366 isset( $sql['options'] ) ? $sql['options'] : []
367 );
368 $this->assertLastSql( $sqlText );
369 }
370
371 public static function provideUpdate() {
372 return [
373 [
374 [
375 'table' => 'table',
376 'values' => [ 'field' => 'text', 'field2' => 'text2' ],
377 'conds' => [ 'alias' => 'text' ],
378 ],
379 "UPDATE table " .
380 "SET field = 'text'" .
381 ",field2 = 'text2' " .
382 "WHERE alias = 'text'"
383 ],
384 [
385 [
386 'table' => 'table',
387 'values' => [ 'field = other', 'field2' => 'text2' ],
388 'conds' => [ 'id' => '1' ],
389 ],
390 "UPDATE table " .
391 "SET field = other" .
392 ",field2 = 'text2' " .
393 "WHERE id = '1'"
394 ],
395 [
396 [
397 'table' => 'table',
398 'values' => [ 'field = other', 'field2' => 'text2' ],
399 'conds' => '*',
400 ],
401 "UPDATE table " .
402 "SET field = other" .
403 ",field2 = 'text2'"
404 ],
405 ];
406 }
407
408 /**
409 * @dataProvider provideDelete
410 * @covers Wikimedia\Rdbms\Database::delete
411 */
412 public function testDelete( $sql, $sqlText ) {
413 $this->database->delete(
414 $sql['table'],
415 $sql['conds'],
416 __METHOD__
417 );
418 $this->assertLastSql( $sqlText );
419 }
420
421 public static function provideDelete() {
422 return [
423 [
424 [
425 'table' => 'table',
426 'conds' => [ 'alias' => 'text' ],
427 ],
428 "DELETE FROM table " .
429 "WHERE alias = 'text'"
430 ],
431 [
432 [
433 'table' => 'table',
434 'conds' => '*',
435 ],
436 "DELETE FROM table"
437 ],
438 ];
439 }
440
441 /**
442 * @dataProvider provideUpsert
443 * @covers Wikimedia\Rdbms\Database::upsert
444 */
445 public function testUpsert( $sql, $sqlText ) {
446 $this->database->upsert(
447 $sql['table'],
448 $sql['rows'],
449 $sql['uniqueIndexes'],
450 $sql['set'],
451 __METHOD__
452 );
453 $this->assertLastSql( $sqlText );
454 }
455
456 public static function provideUpsert() {
457 return [
458 [
459 [
460 'table' => 'upsert_table',
461 'rows' => [ 'field' => 'text', 'field2' => 'text2' ],
462 'uniqueIndexes' => [ 'field' ],
463 'set' => [ 'field' => 'set' ],
464 ],
465 "BEGIN; " .
466 "UPDATE upsert_table " .
467 "SET field = 'set' " .
468 "WHERE ((field = 'text')); " .
469 "INSERT IGNORE INTO upsert_table " .
470 "(field,field2) " .
471 "VALUES ('text','text2'); " .
472 "COMMIT"
473 ],
474 ];
475 }
476
477 /**
478 * @dataProvider provideDeleteJoin
479 * @covers Wikimedia\Rdbms\Database::deleteJoin
480 */
481 public function testDeleteJoin( $sql, $sqlText ) {
482 $this->database->deleteJoin(
483 $sql['delTable'],
484 $sql['joinTable'],
485 $sql['delVar'],
486 $sql['joinVar'],
487 $sql['conds'],
488 __METHOD__
489 );
490 $this->assertLastSql( $sqlText );
491 }
492
493 public static function provideDeleteJoin() {
494 return [
495 [
496 [
497 'delTable' => 'table',
498 'joinTable' => 'table_join',
499 'delVar' => 'field',
500 'joinVar' => 'field_join',
501 'conds' => [ 'alias' => 'text' ],
502 ],
503 "DELETE FROM table " .
504 "WHERE field IN (" .
505 "SELECT field_join FROM table_join WHERE alias = 'text'" .
506 ")"
507 ],
508 [
509 [
510 'delTable' => 'table',
511 'joinTable' => 'table_join',
512 'delVar' => 'field',
513 'joinVar' => 'field_join',
514 'conds' => '*',
515 ],
516 "DELETE FROM table " .
517 "WHERE field IN (" .
518 "SELECT field_join FROM table_join " .
519 ")"
520 ],
521 ];
522 }
523
524 /**
525 * @dataProvider provideInsert
526 * @covers Wikimedia\Rdbms\Database::insert
527 * @covers Wikimedia\Rdbms\Database::makeInsertOptions
528 */
529 public function testInsert( $sql, $sqlText ) {
530 $this->database->insert(
531 $sql['table'],
532 $sql['rows'],
533 __METHOD__,
534 isset( $sql['options'] ) ? $sql['options'] : []
535 );
536 $this->assertLastSql( $sqlText );
537 }
538
539 public static function provideInsert() {
540 return [
541 [
542 [
543 'table' => 'table',
544 'rows' => [ 'field' => 'text', 'field2' => 2 ],
545 ],
546 "INSERT INTO table " .
547 "(field,field2) " .
548 "VALUES ('text','2')"
549 ],
550 [
551 [
552 'table' => 'table',
553 'rows' => [ 'field' => 'text', 'field2' => 2 ],
554 'options' => 'IGNORE',
555 ],
556 "INSERT IGNORE INTO table " .
557 "(field,field2) " .
558 "VALUES ('text','2')"
559 ],
560 [
561 [
562 'table' => 'table',
563 'rows' => [
564 [ 'field' => 'text', 'field2' => 2 ],
565 [ 'field' => 'multi', 'field2' => 3 ],
566 ],
567 'options' => 'IGNORE',
568 ],
569 "INSERT IGNORE INTO table " .
570 "(field,field2) " .
571 "VALUES " .
572 "('text','2')," .
573 "('multi','3')"
574 ],
575 ];
576 }
577
578 /**
579 * @dataProvider provideInsertSelect
580 * @covers Wikimedia\Rdbms\Database::insertSelect
581 * @covers Wikimedia\Rdbms\Database::nativeInsertSelect
582 */
583 public function testInsertSelect( $sql, $sqlTextNative, $sqlSelect, $sqlInsert ) {
584 $this->database->insertSelect(
585 $sql['destTable'],
586 $sql['srcTable'],
587 $sql['varMap'],
588 $sql['conds'],
589 __METHOD__,
590 isset( $sql['insertOptions'] ) ? $sql['insertOptions'] : [],
591 isset( $sql['selectOptions'] ) ? $sql['selectOptions'] : [],
592 isset( $sql['selectJoinConds'] ) ? $sql['selectJoinConds'] : []
593 );
594 $this->assertLastSql( $sqlTextNative );
595
596 $dbWeb = new DatabaseTestHelper( __CLASS__, [ 'cliMode' => false ] );
597 $dbWeb->forceNextResult( [
598 array_flip( array_keys( $sql['varMap'] ) )
599 ] );
600 $dbWeb->insertSelect(
601 $sql['destTable'],
602 $sql['srcTable'],
603 $sql['varMap'],
604 $sql['conds'],
605 __METHOD__,
606 isset( $sql['insertOptions'] ) ? $sql['insertOptions'] : [],
607 isset( $sql['selectOptions'] ) ? $sql['selectOptions'] : [],
608 isset( $sql['selectJoinConds'] ) ? $sql['selectJoinConds'] : []
609 );
610 $this->assertLastSqlDb( implode( '; ', [ $sqlSelect, 'BEGIN', $sqlInsert, 'COMMIT' ] ), $dbWeb );
611 }
612
613 public static function provideInsertSelect() {
614 return [
615 [
616 [
617 'destTable' => 'insert_table',
618 'srcTable' => 'select_table',
619 'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
620 'conds' => '*',
621 ],
622 "INSERT INTO insert_table " .
623 "(field_insert,field) " .
624 "SELECT field_select,field2 " .
625 "FROM select_table WHERE *",
626 "SELECT field_select AS field_insert,field2 AS field " .
627 "FROM select_table WHERE * FOR UPDATE",
628 "INSERT INTO insert_table (field_insert,field) VALUES ('0','1')"
629 ],
630 [
631 [
632 'destTable' => 'insert_table',
633 'srcTable' => 'select_table',
634 'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
635 'conds' => [ 'field' => 2 ],
636 ],
637 "INSERT INTO insert_table " .
638 "(field_insert,field) " .
639 "SELECT field_select,field2 " .
640 "FROM select_table " .
641 "WHERE field = '2'",
642 "SELECT field_select AS field_insert,field2 AS field FROM " .
643 "select_table WHERE field = '2' FOR UPDATE",
644 "INSERT INTO insert_table (field_insert,field) VALUES ('0','1')"
645 ],
646 [
647 [
648 'destTable' => 'insert_table',
649 'srcTable' => 'select_table',
650 'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
651 'conds' => [ 'field' => 2 ],
652 'insertOptions' => 'IGNORE',
653 'selectOptions' => [ 'ORDER BY' => 'field' ],
654 ],
655 "INSERT IGNORE INTO insert_table " .
656 "(field_insert,field) " .
657 "SELECT field_select,field2 " .
658 "FROM select_table " .
659 "WHERE field = '2' " .
660 "ORDER BY field",
661 "SELECT field_select AS field_insert,field2 AS field " .
662 "FROM select_table WHERE field = '2' ORDER BY field FOR UPDATE",
663 "INSERT IGNORE INTO insert_table (field_insert,field) VALUES ('0','1')"
664 ],
665 [
666 [
667 'destTable' => 'insert_table',
668 'srcTable' => [ 'select_table1', 'select_table2' ],
669 'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
670 'conds' => [ 'field' => 2 ],
671 'insertOptions' => [ 'NO_AUTO_COLUMNS' ],
672 'selectOptions' => [ 'ORDER BY' => 'field', 'FORCE INDEX' => [ 'select_table1' => 'index1' ] ],
673 'selectJoinConds' => [
674 'select_table2' => [ 'LEFT JOIN', [ 'select_table1.foo = select_table2.bar' ] ],
675 ],
676 ],
677 "INSERT INTO insert_table " .
678 "(field_insert,field) " .
679 "SELECT field_select,field2 " .
680 "FROM select_table1 LEFT JOIN select_table2 ON ((select_table1.foo = select_table2.bar)) " .
681 "WHERE field = '2' " .
682 "ORDER BY field",
683 "SELECT field_select AS field_insert,field2 AS field " .
684 "FROM select_table1 LEFT JOIN select_table2 ON ((select_table1.foo = select_table2.bar)) " .
685 "WHERE field = '2' ORDER BY field FOR UPDATE",
686 "INSERT INTO insert_table (field_insert,field) VALUES ('0','1')"
687 ],
688 ];
689 }
690
691 public function testInsertSelectBatching() {
692 $dbWeb = new DatabaseTestHelper( __CLASS__, [ 'cliMode' => false ] );
693 $rows = [];
694 for ( $i = 0; $i <= 25000; $i++ ) {
695 $rows[] = [ 'field' => $i ];
696 }
697 $dbWeb->forceNextResult( $rows );
698 $dbWeb->insertSelect(
699 'insert_table',
700 'select_table',
701 [ 'field' => 'field2' ],
702 '*',
703 __METHOD__
704 );
705 $this->assertLastSqlDb( implode( '; ', [
706 'SELECT field2 AS field FROM select_table WHERE * FOR UPDATE',
707 'BEGIN',
708 "INSERT INTO insert_table (field) VALUES ('" . implode( "'),('", range( 0, 9999 ) ) . "')",
709 "INSERT INTO insert_table (field) VALUES ('" . implode( "'),('", range( 10000, 19999 ) ) . "')",
710 "INSERT INTO insert_table (field) VALUES ('" . implode( "'),('", range( 20000, 25000 ) ) . "')",
711 'COMMIT'
712 ] ), $dbWeb );
713 }
714
715 /**
716 * @dataProvider provideReplace
717 * @covers Wikimedia\Rdbms\Database::replace
718 */
719 public function testReplace( $sql, $sqlText ) {
720 $this->database->replace(
721 $sql['table'],
722 $sql['uniqueIndexes'],
723 $sql['rows'],
724 __METHOD__
725 );
726 $this->assertLastSql( $sqlText );
727 }
728
729 public static function provideReplace() {
730 return [
731 [
732 [
733 'table' => 'replace_table',
734 'uniqueIndexes' => [ 'field' ],
735 'rows' => [ 'field' => 'text', 'field2' => 'text2' ],
736 ],
737 "BEGIN; DELETE FROM replace_table " .
738 "WHERE (field = 'text'); " .
739 "INSERT INTO replace_table " .
740 "(field,field2) " .
741 "VALUES ('text','text2'); COMMIT"
742 ],
743 [
744 [
745 'table' => 'module_deps',
746 'uniqueIndexes' => [ [ 'md_module', 'md_skin' ] ],
747 'rows' => [
748 'md_module' => 'module',
749 'md_skin' => 'skin',
750 'md_deps' => 'deps',
751 ],
752 ],
753 "BEGIN; DELETE FROM module_deps " .
754 "WHERE (md_module = 'module' AND md_skin = 'skin'); " .
755 "INSERT INTO module_deps " .
756 "(md_module,md_skin,md_deps) " .
757 "VALUES ('module','skin','deps'); COMMIT"
758 ],
759 [
760 [
761 'table' => 'module_deps',
762 'uniqueIndexes' => [ [ 'md_module', 'md_skin' ] ],
763 'rows' => [
764 [
765 'md_module' => 'module',
766 'md_skin' => 'skin',
767 'md_deps' => 'deps',
768 ], [
769 'md_module' => 'module2',
770 'md_skin' => 'skin2',
771 'md_deps' => 'deps2',
772 ],
773 ],
774 ],
775 "BEGIN; DELETE FROM module_deps " .
776 "WHERE (md_module = 'module' AND md_skin = 'skin'); " .
777 "INSERT INTO module_deps " .
778 "(md_module,md_skin,md_deps) " .
779 "VALUES ('module','skin','deps'); " .
780 "DELETE FROM module_deps " .
781 "WHERE (md_module = 'module2' AND md_skin = 'skin2'); " .
782 "INSERT INTO module_deps " .
783 "(md_module,md_skin,md_deps) " .
784 "VALUES ('module2','skin2','deps2'); COMMIT"
785 ],
786 [
787 [
788 'table' => 'module_deps',
789 'uniqueIndexes' => [ 'md_module', 'md_skin' ],
790 'rows' => [
791 [
792 'md_module' => 'module',
793 'md_skin' => 'skin',
794 'md_deps' => 'deps',
795 ], [
796 'md_module' => 'module2',
797 'md_skin' => 'skin2',
798 'md_deps' => 'deps2',
799 ],
800 ],
801 ],
802 "BEGIN; DELETE FROM module_deps " .
803 "WHERE (md_module = 'module') OR (md_skin = 'skin'); " .
804 "INSERT INTO module_deps " .
805 "(md_module,md_skin,md_deps) " .
806 "VALUES ('module','skin','deps'); " .
807 "DELETE FROM module_deps " .
808 "WHERE (md_module = 'module2') OR (md_skin = 'skin2'); " .
809 "INSERT INTO module_deps " .
810 "(md_module,md_skin,md_deps) " .
811 "VALUES ('module2','skin2','deps2'); COMMIT"
812 ],
813 [
814 [
815 'table' => 'module_deps',
816 'uniqueIndexes' => [],
817 'rows' => [
818 'md_module' => 'module',
819 'md_skin' => 'skin',
820 'md_deps' => 'deps',
821 ],
822 ],
823 "BEGIN; INSERT INTO module_deps " .
824 "(md_module,md_skin,md_deps) " .
825 "VALUES ('module','skin','deps'); COMMIT"
826 ],
827 ];
828 }
829
830 /**
831 * @dataProvider provideNativeReplace
832 * @covers Wikimedia\Rdbms\Database::nativeReplace
833 */
834 public function testNativeReplace( $sql, $sqlText ) {
835 $this->database->nativeReplace(
836 $sql['table'],
837 $sql['rows'],
838 __METHOD__
839 );
840 $this->assertLastSql( $sqlText );
841 }
842
843 public static function provideNativeReplace() {
844 return [
845 [
846 [
847 'table' => 'replace_table',
848 'rows' => [ 'field' => 'text', 'field2' => 'text2' ],
849 ],
850 "REPLACE INTO replace_table " .
851 "(field,field2) " .
852 "VALUES ('text','text2')"
853 ],
854 ];
855 }
856
857 /**
858 * @dataProvider provideConditional
859 * @covers Wikimedia\Rdbms\Database::conditional
860 */
861 public function testConditional( $sql, $sqlText ) {
862 $this->assertEquals( trim( $this->database->conditional(
863 $sql['conds'],
864 $sql['true'],
865 $sql['false']
866 ) ), $sqlText );
867 }
868
869 public static function provideConditional() {
870 return [
871 [
872 [
873 'conds' => [ 'field' => 'text' ],
874 'true' => 1,
875 'false' => 'NULL',
876 ],
877 "(CASE WHEN field = 'text' THEN 1 ELSE NULL END)"
878 ],
879 [
880 [
881 'conds' => [ 'field' => 'text', 'field2' => 'anothertext' ],
882 'true' => 1,
883 'false' => 'NULL',
884 ],
885 "(CASE WHEN field = 'text' AND field2 = 'anothertext' THEN 1 ELSE NULL END)"
886 ],
887 [
888 [
889 'conds' => 'field=1',
890 'true' => 1,
891 'false' => 'NULL',
892 ],
893 "(CASE WHEN field=1 THEN 1 ELSE NULL END)"
894 ],
895 ];
896 }
897
898 /**
899 * @dataProvider provideBuildConcat
900 * @covers Wikimedia\Rdbms\Database::buildConcat
901 */
902 public function testBuildConcat( $stringList, $sqlText ) {
903 $this->assertEquals( trim( $this->database->buildConcat(
904 $stringList
905 ) ), $sqlText );
906 }
907
908 public static function provideBuildConcat() {
909 return [
910 [
911 [ 'field', 'field2' ],
912 "CONCAT(field,field2)"
913 ],
914 [
915 [ "'test'", 'field2' ],
916 "CONCAT('test',field2)"
917 ],
918 ];
919 }
920
921 /**
922 * @dataProvider provideBuildLike
923 * @covers Wikimedia\Rdbms\Database::buildLike
924 * @covers Wikimedia\Rdbms\Database::escapeLikeInternal
925 */
926 public function testBuildLike( $array, $sqlText ) {
927 $this->assertEquals( trim( $this->database->buildLike(
928 $array
929 ) ), $sqlText );
930 }
931
932 public static function provideBuildLike() {
933 return [
934 [
935 'text',
936 "LIKE 'text' ESCAPE '`'"
937 ],
938 [
939 [ 'text', new LikeMatch( '%' ) ],
940 "LIKE 'text%' ESCAPE '`'"
941 ],
942 [
943 [ 'text', new LikeMatch( '%' ), 'text2' ],
944 "LIKE 'text%text2' ESCAPE '`'"
945 ],
946 [
947 [ 'text', new LikeMatch( '_' ) ],
948 "LIKE 'text_' ESCAPE '`'"
949 ],
950 [
951 'more_text',
952 "LIKE 'more`_text' ESCAPE '`'"
953 ],
954 [
955 [ 'C:\\Windows\\', new LikeMatch( '%' ) ],
956 "LIKE 'C:\\Windows\\%' ESCAPE '`'"
957 ],
958 [
959 [ 'accent`_test`', new LikeMatch( '%' ) ],
960 "LIKE 'accent```_test``%' ESCAPE '`'"
961 ],
962 ];
963 }
964
965 /**
966 * @dataProvider provideUnionQueries
967 * @covers Wikimedia\Rdbms\Database::unionQueries
968 */
969 public function testUnionQueries( $sql, $sqlText ) {
970 $this->assertEquals( trim( $this->database->unionQueries(
971 $sql['sqls'],
972 $sql['all']
973 ) ), $sqlText );
974 }
975
976 public static function provideUnionQueries() {
977 return [
978 [
979 [
980 'sqls' => [ 'RAW SQL', 'RAW2SQL' ],
981 'all' => true,
982 ],
983 "(RAW SQL) UNION ALL (RAW2SQL)"
984 ],
985 [
986 [
987 'sqls' => [ 'RAW SQL', 'RAW2SQL' ],
988 'all' => false,
989 ],
990 "(RAW SQL) UNION (RAW2SQL)"
991 ],
992 [
993 [
994 'sqls' => [ 'RAW SQL', 'RAW2SQL', 'RAW3SQL' ],
995 'all' => false,
996 ],
997 "(RAW SQL) UNION (RAW2SQL) UNION (RAW3SQL)"
998 ],
999 ];
1000 }
1001
1002 /**
1003 * @dataProvider provideUnionConditionPermutations
1004 * @covers Wikimedia\Rdbms\Database::unionConditionPermutations
1005 */
1006 public function testUnionConditionPermutations( $params, $expect ) {
1007 if ( isset( $params['unionSupportsOrderAndLimit'] ) ) {
1008 $this->database->setUnionSupportsOrderAndLimit( $params['unionSupportsOrderAndLimit'] );
1009 }
1010
1011 $sql = trim( $this->database->unionConditionPermutations(
1012 $params['table'],
1013 $params['vars'],
1014 $params['permute_conds'],
1015 isset( $params['extra_conds'] ) ? $params['extra_conds'] : '',
1016 'FNAME',
1017 isset( $params['options'] ) ? $params['options'] : [],
1018 isset( $params['join_conds'] ) ? $params['join_conds'] : []
1019 ) );
1020 $this->assertEquals( $expect, $sql );
1021 }
1022
1023 public static function provideUnionConditionPermutations() {
1024 // phpcs:disable Generic.Files.LineLength
1025 return [
1026 [
1027 [
1028 'table' => [ 'table1', 'table2' ],
1029 'vars' => [ 'field1', 'alias' => 'field2' ],
1030 'permute_conds' => [
1031 'field3' => [ 1, 2, 3 ],
1032 'duplicates' => [ 4, 5, 4 ],
1033 'empty' => [],
1034 'single' => [ 0 ],
1035 ],
1036 'extra_conds' => 'table2.bar > 23',
1037 'options' => [
1038 'ORDER BY' => [ 'field1', 'alias' ],
1039 'INNER ORDER BY' => [ 'field1', 'field2' ],
1040 'LIMIT' => 100,
1041 ],
1042 'join_conds' => [
1043 'table2' => [ 'JOIN', 'table1.foo_id = table2.foo_id' ],
1044 ],
1045 ],
1046 "(SELECT field1,field2 AS alias FROM table1 JOIN table2 ON ((table1.foo_id = table2.foo_id)) WHERE field3 = '1' AND duplicates = '4' AND single = '0' AND (table2.bar > 23) ORDER BY field1,field2 LIMIT 100 ) UNION ALL " .
1047 "(SELECT field1,field2 AS alias FROM table1 JOIN table2 ON ((table1.foo_id = table2.foo_id)) WHERE field3 = '1' AND duplicates = '5' AND single = '0' AND (table2.bar > 23) ORDER BY field1,field2 LIMIT 100 ) UNION ALL " .
1048 "(SELECT field1,field2 AS alias FROM table1 JOIN table2 ON ((table1.foo_id = table2.foo_id)) WHERE field3 = '2' AND duplicates = '4' AND single = '0' AND (table2.bar > 23) ORDER BY field1,field2 LIMIT 100 ) UNION ALL " .
1049 "(SELECT field1,field2 AS alias FROM table1 JOIN table2 ON ((table1.foo_id = table2.foo_id)) WHERE field3 = '2' AND duplicates = '5' AND single = '0' AND (table2.bar > 23) ORDER BY field1,field2 LIMIT 100 ) UNION ALL " .
1050 "(SELECT field1,field2 AS alias FROM table1 JOIN table2 ON ((table1.foo_id = table2.foo_id)) WHERE field3 = '3' AND duplicates = '4' AND single = '0' AND (table2.bar > 23) ORDER BY field1,field2 LIMIT 100 ) UNION ALL " .
1051 "(SELECT field1,field2 AS alias FROM table1 JOIN table2 ON ((table1.foo_id = table2.foo_id)) WHERE field3 = '3' AND duplicates = '5' AND single = '0' AND (table2.bar > 23) ORDER BY field1,field2 LIMIT 100 ) " .
1052 "ORDER BY field1,alias LIMIT 100"
1053 ],
1054 [
1055 [
1056 'table' => 'foo',
1057 'vars' => [ 'foo_id' ],
1058 'permute_conds' => [
1059 'bar' => [ 1, 2, 3 ],
1060 ],
1061 'extra_conds' => [ 'baz' => null ],
1062 'options' => [
1063 'NOTALL',
1064 'ORDER BY' => [ 'foo_id' ],
1065 'LIMIT' => 25,
1066 ],
1067 ],
1068 "(SELECT foo_id FROM foo WHERE bar = '1' AND baz IS NULL ORDER BY foo_id LIMIT 25 ) UNION " .
1069 "(SELECT foo_id FROM foo WHERE bar = '2' AND baz IS NULL ORDER BY foo_id LIMIT 25 ) UNION " .
1070 "(SELECT foo_id FROM foo WHERE bar = '3' AND baz IS NULL ORDER BY foo_id LIMIT 25 ) " .
1071 "ORDER BY foo_id LIMIT 25"
1072 ],
1073 [
1074 [
1075 'table' => 'foo',
1076 'vars' => [ 'foo_id' ],
1077 'permute_conds' => [
1078 'bar' => [ 1, 2, 3 ],
1079 ],
1080 'extra_conds' => [ 'baz' => null ],
1081 'options' => [
1082 'NOTALL' => true,
1083 'ORDER BY' => [ 'foo_id' ],
1084 'LIMIT' => 25,
1085 ],
1086 'unionSupportsOrderAndLimit' => false,
1087 ],
1088 "(SELECT foo_id FROM foo WHERE bar = '1' AND baz IS NULL ) UNION " .
1089 "(SELECT foo_id FROM foo WHERE bar = '2' AND baz IS NULL ) UNION " .
1090 "(SELECT foo_id FROM foo WHERE bar = '3' AND baz IS NULL ) " .
1091 "ORDER BY foo_id LIMIT 25"
1092 ],
1093 [
1094 [
1095 'table' => 'foo',
1096 'vars' => [ 'foo_id' ],
1097 'permute_conds' => [],
1098 'extra_conds' => [ 'baz' => null ],
1099 'options' => [
1100 'ORDER BY' => [ 'foo_id' ],
1101 'LIMIT' => 25,
1102 ],
1103 ],
1104 "SELECT foo_id FROM foo WHERE baz IS NULL ORDER BY foo_id LIMIT 25"
1105 ],
1106 [
1107 [
1108 'table' => 'foo',
1109 'vars' => [ 'foo_id' ],
1110 'permute_conds' => [
1111 'bar' => [],
1112 ],
1113 'extra_conds' => [ 'baz' => null ],
1114 'options' => [
1115 'ORDER BY' => [ 'foo_id' ],
1116 'LIMIT' => 25,
1117 ],
1118 ],
1119 "SELECT foo_id FROM foo WHERE baz IS NULL ORDER BY foo_id LIMIT 25"
1120 ],
1121 [
1122 [
1123 'table' => 'foo',
1124 'vars' => [ 'foo_id' ],
1125 'permute_conds' => [
1126 'bar' => [ 1 ],
1127 ],
1128 'options' => [
1129 'ORDER BY' => [ 'foo_id' ],
1130 'LIMIT' => 25,
1131 'OFFSET' => 150,
1132 ],
1133 ],
1134 "SELECT foo_id FROM foo WHERE bar = '1' ORDER BY foo_id LIMIT 150,25"
1135 ],
1136 [
1137 [
1138 'table' => 'foo',
1139 'vars' => [ 'foo_id' ],
1140 'permute_conds' => [],
1141 'extra_conds' => [ 'baz' => null ],
1142 'options' => [
1143 'ORDER BY' => [ 'foo_id' ],
1144 'LIMIT' => 25,
1145 'OFFSET' => 150,
1146 'INNER ORDER BY' => [ 'bar_id' ],
1147 ],
1148 ],
1149 "(SELECT foo_id FROM foo WHERE baz IS NULL ORDER BY bar_id LIMIT 175 ) ORDER BY foo_id LIMIT 150,25"
1150 ],
1151 [
1152 [
1153 'table' => 'foo',
1154 'vars' => [ 'foo_id' ],
1155 'permute_conds' => [],
1156 'extra_conds' => [ 'baz' => null ],
1157 'options' => [
1158 'ORDER BY' => [ 'foo_id' ],
1159 'LIMIT' => 25,
1160 'OFFSET' => 150,
1161 'INNER ORDER BY' => [ 'bar_id' ],
1162 ],
1163 'unionSupportsOrderAndLimit' => false,
1164 ],
1165 "SELECT foo_id FROM foo WHERE baz IS NULL ORDER BY foo_id LIMIT 150,25"
1166 ],
1167 ];
1168 // phpcs:enable
1169 }
1170
1171 /**
1172 * @covers Wikimedia\Rdbms\Database::commit
1173 * @covers Wikimedia\Rdbms\Database::doCommit
1174 */
1175 public function testTransactionCommit() {
1176 $this->database->begin( __METHOD__ );
1177 $this->database->commit( __METHOD__ );
1178 $this->assertLastSql( 'BEGIN; COMMIT' );
1179 }
1180
1181 /**
1182 * @covers Wikimedia\Rdbms\Database::rollback
1183 * @covers Wikimedia\Rdbms\Database::doRollback
1184 */
1185 public function testTransactionRollback() {
1186 $this->database->begin( __METHOD__ );
1187 $this->database->rollback( __METHOD__ );
1188 $this->assertLastSql( 'BEGIN; ROLLBACK' );
1189 }
1190
1191 /**
1192 * @covers Wikimedia\Rdbms\Database::dropTable
1193 */
1194 public function testDropTable() {
1195 $this->database->setExistingTables( [ 'table' ] );
1196 $this->database->dropTable( 'table', __METHOD__ );
1197 $this->assertLastSql( 'DROP TABLE table CASCADE' );
1198 }
1199
1200 /**
1201 * @covers Wikimedia\Rdbms\Database::dropTable
1202 */
1203 public function testDropNonExistingTable() {
1204 $this->assertFalse(
1205 $this->database->dropTable( 'non_existing', __METHOD__ )
1206 );
1207 }
1208
1209 /**
1210 * @dataProvider provideMakeList
1211 * @covers Wikimedia\Rdbms\Database::makeList
1212 */
1213 public function testMakeList( $list, $mode, $sqlText ) {
1214 $this->assertEquals( trim( $this->database->makeList(
1215 $list, $mode
1216 ) ), $sqlText );
1217 }
1218
1219 public static function provideMakeList() {
1220 return [
1221 [
1222 [ 'value', 'value2' ],
1223 LIST_COMMA,
1224 "'value','value2'"
1225 ],
1226 [
1227 [ 'field', 'field2' ],
1228 LIST_NAMES,
1229 "field,field2"
1230 ],
1231 [
1232 [ 'field' => 'value', 'field2' => 'value2' ],
1233 LIST_AND,
1234 "field = 'value' AND field2 = 'value2'"
1235 ],
1236 [
1237 [ 'field' => null, "field2 != 'value2'" ],
1238 LIST_AND,
1239 "field IS NULL AND (field2 != 'value2')"
1240 ],
1241 [
1242 [ 'field' => [ 'value', null, 'value2' ], 'field2' => 'value2' ],
1243 LIST_AND,
1244 "(field IN ('value','value2') OR field IS NULL) AND field2 = 'value2'"
1245 ],
1246 [
1247 [ 'field' => [ null ], 'field2' => null ],
1248 LIST_AND,
1249 "field IS NULL AND field2 IS NULL"
1250 ],
1251 [
1252 [ 'field' => 'value', 'field2' => 'value2' ],
1253 LIST_OR,
1254 "field = 'value' OR field2 = 'value2'"
1255 ],
1256 [
1257 [ 'field' => 'value', 'field2' => null ],
1258 LIST_OR,
1259 "field = 'value' OR field2 IS NULL"
1260 ],
1261 [
1262 [ 'field' => [ 'value', 'value2' ], 'field2' => [ 'value' ] ],
1263 LIST_OR,
1264 "field IN ('value','value2') OR field2 = 'value'"
1265 ],
1266 [
1267 [ 'field' => [ null, 'value', null, 'value2' ], "field2 != 'value2'" ],
1268 LIST_OR,
1269 "(field IN ('value','value2') OR field IS NULL) OR (field2 != 'value2')"
1270 ],
1271 [
1272 [ 'field' => 'value', 'field2' => 'value2' ],
1273 LIST_SET,
1274 "field = 'value',field2 = 'value2'"
1275 ],
1276 [
1277 [ 'field' => 'value', 'field2' => null ],
1278 LIST_SET,
1279 "field = 'value',field2 = NULL"
1280 ],
1281 [
1282 [ 'field' => 'value', "field2 != 'value2'" ],
1283 LIST_SET,
1284 "field = 'value',field2 != 'value2'"
1285 ],
1286 ];
1287 }
1288
1289 /**
1290 * @covers Wikimedia\Rdbms\Database::registerTempTableOperation
1291 */
1292 public function testSessionTempTables() {
1293 $temp1 = $this->database->tableName( 'tmp_table_1' );
1294 $temp2 = $this->database->tableName( 'tmp_table_2' );
1295 $temp3 = $this->database->tableName( 'tmp_table_3' );
1296
1297 $this->database->query( "CREATE TEMPORARY TABLE $temp1 LIKE orig_tbl", __METHOD__ );
1298 $this->database->query( "CREATE TEMPORARY TABLE $temp2 LIKE orig_tbl", __METHOD__ );
1299 $this->database->query( "CREATE TEMPORARY TABLE $temp3 LIKE orig_tbl", __METHOD__ );
1300
1301 $this->assertTrue( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
1302 $this->assertTrue( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
1303 $this->assertTrue( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
1304
1305 $this->database->dropTable( 'tmp_table_1', __METHOD__ );
1306 $this->database->dropTable( 'tmp_table_2', __METHOD__ );
1307 $this->database->dropTable( 'tmp_table_3', __METHOD__ );
1308
1309 $this->assertFalse( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
1310 $this->assertFalse( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
1311 $this->assertFalse( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
1312
1313 $this->database->query( "CREATE TEMPORARY TABLE tmp_table_1 LIKE orig_tbl", __METHOD__ );
1314 $this->database->query( "CREATE TEMPORARY TABLE 'tmp_table_2' LIKE orig_tbl", __METHOD__ );
1315 $this->database->query( "CREATE TEMPORARY TABLE `tmp_table_3` LIKE orig_tbl", __METHOD__ );
1316
1317 $this->assertTrue( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
1318 $this->assertTrue( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
1319 $this->assertTrue( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
1320
1321 $this->database->query( "DROP TEMPORARY TABLE tmp_table_1 LIKE orig_tbl", __METHOD__ );
1322 $this->database->query( "DROP TEMPORARY TABLE 'tmp_table_2' LIKE orig_tbl", __METHOD__ );
1323 $this->database->query( "DROP TABLE `tmp_table_3` LIKE orig_tbl", __METHOD__ );
1324
1325 $this->assertFalse( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
1326 $this->assertFalse( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
1327 $this->assertFalse( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
1328 }
1329
1330 public function provideBuildSubstring() {
1331 yield [ 'someField', 1, 2, 'SUBSTRING(someField FROM 1 FOR 2)' ];
1332 yield [ 'someField', 1, null, 'SUBSTRING(someField FROM 1)' ];
1333 }
1334
1335 /**
1336 * @covers Wikimedia\Rdbms\Database::buildSubstring
1337 * @dataProvider provideBuildSubstring
1338 */
1339 public function testBuildSubstring( $input, $start, $length, $expected ) {
1340 $output = $this->database->buildSubstring( $input, $start, $length );
1341 $this->assertSame( $expected, $output );
1342 }
1343
1344 public function provideBuildSubstring_invalidParams() {
1345 yield [ -1, 1 ];
1346 yield [ 1, -1 ];
1347 yield [ 1, 'foo' ];
1348 yield [ 'foo', 1 ];
1349 yield [ null, 1 ];
1350 yield [ 0, 1 ];
1351 }
1352
1353 /**
1354 * @covers Wikimedia\Rdbms\Database::buildSubstring
1355 * @covers Wikimedia\Rdbms\Database::assertBuildSubstringParams
1356 * @dataProvider provideBuildSubstring_invalidParams
1357 */
1358 public function testBuildSubstring_invalidParams( $start, $length ) {
1359 $this->setExpectedException( InvalidArgumentException::class );
1360 $this->database->buildSubstring( 'foo', $start, $length );
1361 }
1362
1363 /**
1364 * @covers \Wikimedia\Rdbms\Database::buildIntegerCast
1365 */
1366 public function testBuildIntegerCast() {
1367 $output = $this->database->buildIntegerCast( 'fieldName' );
1368 $this->assertSame( 'CAST( fieldName AS INTEGER )', $output );
1369 }
1370
1371 /**
1372 * @covers \Wikimedia\Rdbms\Database::doSavepoint
1373 * @covers \Wikimedia\Rdbms\Database::doReleaseSavepoint
1374 * @covers \Wikimedia\Rdbms\Database::doRollbackToSavepoint
1375 * @covers \Wikimedia\Rdbms\Database::startAtomic
1376 * @covers \Wikimedia\Rdbms\Database::endAtomic
1377 * @covers \Wikimedia\Rdbms\Database::cancelAtomic
1378 * @covers \Wikimedia\Rdbms\Database::doAtomicSection
1379 */
1380 public function testAtomicSections() {
1381 $this->database->startAtomic( __METHOD__ );
1382 $this->database->endAtomic( __METHOD__ );
1383 $this->assertLastSql( 'BEGIN; COMMIT' );
1384
1385 $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
1386 $this->database->cancelAtomic( __METHOD__ );
1387 $this->assertLastSql( 'BEGIN; ROLLBACK' );
1388
1389 $this->database->begin( __METHOD__ );
1390 $this->database->startAtomic( __METHOD__ );
1391 $this->database->endAtomic( __METHOD__ );
1392 $this->database->commit( __METHOD__ );
1393 $this->assertLastSql( 'BEGIN; COMMIT' );
1394
1395 $this->database->begin( __METHOD__ );
1396 $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
1397 $this->database->endAtomic( __METHOD__ );
1398 $this->database->commit( __METHOD__ );
1399 // phpcs:ignore Generic.Files.LineLength
1400 $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; RELEASE SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
1401
1402 $this->database->begin( __METHOD__ );
1403 $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
1404 $this->database->cancelAtomic( __METHOD__ );
1405 $this->database->commit( __METHOD__ );
1406 // phpcs:ignore Generic.Files.LineLength
1407 $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
1408
1409 $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
1410 $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
1411 $this->database->cancelAtomic( __METHOD__ );
1412 $this->database->endAtomic( __METHOD__ );
1413 // phpcs:ignore Generic.Files.LineLength
1414 $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
1415
1416 $noOpCallack = function () {
1417 };
1418
1419 $this->database->doAtomicSection( __METHOD__, $noOpCallack, IDatabase::ATOMIC_CANCELABLE );
1420 $this->assertLastSql( 'BEGIN; COMMIT' );
1421
1422 $this->database->doAtomicSection( __METHOD__, $noOpCallack );
1423 $this->assertLastSql( 'BEGIN; COMMIT' );
1424
1425 $this->database->begin( __METHOD__ );
1426 $this->database->doAtomicSection( __METHOD__, $noOpCallack, IDatabase::ATOMIC_CANCELABLE );
1427 $this->database->rollback( __METHOD__ );
1428 // phpcs:ignore Generic.Files.LineLength
1429 $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; RELEASE SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK' );
1430
1431 $fname = __METHOD__;
1432 $triggerMap = [
1433 '-' => '-',
1434 IDatabase::TRIGGER_COMMIT => 'tCommit',
1435 IDatabase::TRIGGER_ROLLBACK => 'tRollback'
1436 ];
1437 $pcCallback = function ( IDatabase $db ) use ( $fname ) {
1438 $this->database->query( "SELECT 0", $fname );
1439 };
1440 $callback1 = function ( $trigger = '-' ) use ( $fname, $triggerMap ) {
1441 $this->database->query( "SELECT 1, {$triggerMap[$trigger]} AS t", $fname );
1442 };
1443 $callback2 = function ( $trigger = '-' ) use ( $fname, $triggerMap ) {
1444 $this->database->query( "SELECT 2, {$triggerMap[$trigger]} AS t", $fname );
1445 };
1446 $callback3 = function ( $trigger = '-' ) use ( $fname, $triggerMap ) {
1447 $this->database->query( "SELECT 3, {$triggerMap[$trigger]} AS t", $fname );
1448 };
1449
1450 $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
1451 $this->database->onTransactionPreCommitOrIdle( $pcCallback, __METHOD__ );
1452 $this->database->cancelAtomic( __METHOD__ );
1453 $this->assertLastSql( 'BEGIN; ROLLBACK' );
1454
1455 $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
1456 $this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
1457 $this->database->cancelAtomic( __METHOD__ );
1458 $this->assertLastSql( 'BEGIN; ROLLBACK' );
1459
1460 $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
1461 $this->database->onTransactionResolution( $callback1, __METHOD__ );
1462 $this->database->cancelAtomic( __METHOD__ );
1463 $this->assertLastSql( 'BEGIN; ROLLBACK; SELECT 1, tRollback AS t' );
1464
1465 $this->database->startAtomic( __METHOD__ . '_outer' );
1466 $this->database->onTransactionPreCommitOrIdle( $pcCallback, __METHOD__ );
1467 $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
1468 $this->database->onTransactionPreCommitOrIdle( $pcCallback, __METHOD__ );
1469 $this->database->cancelAtomic( __METHOD__ );
1470 $this->database->onTransactionPreCommitOrIdle( $pcCallback, __METHOD__ );
1471 $this->database->endAtomic( __METHOD__ . '_outer' );
1472 $this->assertLastSql( implode( "; ", [
1473 'BEGIN',
1474 'SAVEPOINT wikimedia_rdbms_atomic1',
1475 'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
1476 'SELECT 0',
1477 'SELECT 0',
1478 'COMMIT'
1479 ] ) );
1480
1481 $this->database->startAtomic( __METHOD__ . '_outer' );
1482 $this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
1483 $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
1484 $this->database->onTransactionCommitOrIdle( $callback2, __METHOD__ );
1485 $this->database->cancelAtomic( __METHOD__ );
1486 $this->database->onTransactionCommitOrIdle( $callback3, __METHOD__ );
1487 $this->database->endAtomic( __METHOD__ . '_outer' );
1488 $this->assertLastSql( implode( "; ", [
1489 'BEGIN',
1490 'SAVEPOINT wikimedia_rdbms_atomic1',
1491 'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
1492 'COMMIT',
1493 'SELECT 1, tCommit AS t',
1494 'SELECT 3, tCommit AS t'
1495 ] ) );
1496
1497 $this->database->startAtomic( __METHOD__ . '_outer' );
1498 $this->database->onTransactionResolution( $callback1, __METHOD__ );
1499 $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
1500 $this->database->onTransactionResolution( $callback2, __METHOD__ );
1501 $this->database->cancelAtomic( __METHOD__ );
1502 $this->database->onTransactionResolution( $callback3, __METHOD__ );
1503 $this->database->endAtomic( __METHOD__ . '_outer' );
1504 $this->assertLastSql( implode( "; ", [
1505 'BEGIN',
1506 'SAVEPOINT wikimedia_rdbms_atomic1',
1507 'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
1508 'COMMIT',
1509 'SELECT 1, tCommit AS t',
1510 'SELECT 2, tRollback AS t',
1511 'SELECT 3, tCommit AS t'
1512 ] ) );
1513
1514 $makeCallback = function ( $id ) use ( $fname, $triggerMap ) {
1515 return function ( $trigger = '-' ) use ( $id, $fname, $triggerMap ) {
1516 $this->database->query( "SELECT $id, {$triggerMap[$trigger]} AS t", $fname );
1517 };
1518 };
1519
1520 $this->database->startAtomic( __METHOD__ . '_outer' );
1521 $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
1522 $this->database->onTransactionResolution( $makeCallback( 1 ), __METHOD__ );
1523 $this->database->cancelAtomic( __METHOD__ );
1524 $this->database->endAtomic( __METHOD__ . '_outer' );
1525 $this->assertLastSql( implode( "; ", [
1526 'BEGIN',
1527 'SAVEPOINT wikimedia_rdbms_atomic1',
1528 'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
1529 'COMMIT',
1530 'SELECT 1, tRollback AS t'
1531 ] ) );
1532
1533 $this->database->startAtomic( __METHOD__ . '_level1', IDatabase::ATOMIC_CANCELABLE );
1534 $this->database->onTransactionResolution( $makeCallback( 1 ), __METHOD__ );
1535 $this->database->startAtomic( __METHOD__ . '_level2' );
1536 $this->database->startAtomic( __METHOD__ . '_level3', IDatabase::ATOMIC_CANCELABLE );
1537 $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
1538 $this->database->onTransactionResolution( $makeCallback( 2 ), __METHOD__ );
1539 $this->database->endAtomic( __METHOD__ );
1540 $this->database->onTransactionResolution( $makeCallback( 3 ), __METHOD__ );
1541 $this->database->cancelAtomic( __METHOD__ . '_level3' );
1542 $this->database->endAtomic( __METHOD__ . '_level2' );
1543 $this->database->onTransactionResolution( $makeCallback( 4 ), __METHOD__ );
1544 $this->database->endAtomic( __METHOD__ . '_level1' );
1545 $this->assertLastSql( implode( "; ", [
1546 'BEGIN',
1547 'SAVEPOINT wikimedia_rdbms_atomic1',
1548 'SAVEPOINT wikimedia_rdbms_atomic2',
1549 'RELEASE SAVEPOINT wikimedia_rdbms_atomic2',
1550 'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
1551 'COMMIT; SELECT 1, tCommit AS t',
1552 'SELECT 2, tRollback AS t',
1553 'SELECT 3, tRollback AS t',
1554 'SELECT 4, tCommit AS t'
1555 ] ) );
1556 }
1557
1558 /**
1559 * @covers \Wikimedia\Rdbms\Database::doSavepoint
1560 * @covers \Wikimedia\Rdbms\Database::doReleaseSavepoint
1561 * @covers \Wikimedia\Rdbms\Database::doRollbackToSavepoint
1562 * @covers \Wikimedia\Rdbms\Database::startAtomic
1563 * @covers \Wikimedia\Rdbms\Database::endAtomic
1564 * @covers \Wikimedia\Rdbms\Database::cancelAtomic
1565 * @covers \Wikimedia\Rdbms\Database::doAtomicSection
1566 */
1567 public function testAtomicSectionsRecovery() {
1568 $this->database->begin( __METHOD__ );
1569 try {
1570 $this->database->doAtomicSection(
1571 __METHOD__,
1572 function () {
1573 $this->database->startAtomic( 'inner_func1' );
1574 $this->database->startAtomic( 'inner_func2' );
1575
1576 throw new RuntimeException( 'Test exception' );
1577 },
1578 IDatabase::ATOMIC_CANCELABLE
1579 );
1580 $this->fail( 'Expected exception not thrown' );
1581 } catch ( RuntimeException $ex ) {
1582 $this->assertSame( 'Test exception', $ex->getMessage() );
1583 }
1584 $this->database->commit( __METHOD__ );
1585 // phpcs:ignore Generic.Files.LineLength
1586 $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
1587
1588 $this->database->begin( __METHOD__ );
1589 try {
1590 $this->database->doAtomicSection(
1591 __METHOD__,
1592 function () {
1593 throw new RuntimeException( 'Test exception' );
1594 }
1595 );
1596 $this->fail( 'Test exception not thrown' );
1597 } catch ( RuntimeException $ex ) {
1598 $this->assertSame( 'Test exception', $ex->getMessage() );
1599 }
1600 try {
1601 $this->database->commit( __METHOD__ );
1602 $this->fail( 'Test exception not thrown' );
1603 } catch ( DBTransactionError $ex ) {
1604 $this->assertSame(
1605 'Cannot execute query from ' . __METHOD__ . ' while transaction status is ERROR.',
1606 $ex->getMessage()
1607 );
1608 }
1609 $this->database->rollback( __METHOD__ );
1610 $this->assertLastSql( 'BEGIN; ROLLBACK' );
1611 }
1612
1613 /**
1614 * @covers \Wikimedia\Rdbms\Database::doSavepoint
1615 * @covers \Wikimedia\Rdbms\Database::doReleaseSavepoint
1616 * @covers \Wikimedia\Rdbms\Database::doRollbackToSavepoint
1617 * @covers \Wikimedia\Rdbms\Database::startAtomic
1618 * @covers \Wikimedia\Rdbms\Database::endAtomic
1619 * @covers \Wikimedia\Rdbms\Database::cancelAtomic
1620 * @covers \Wikimedia\Rdbms\Database::doAtomicSection
1621 */
1622 public function testAtomicSectionsCallbackCancellation() {
1623 $fname = __METHOD__;
1624 $callback1Called = null;
1625 $callback1 = function ( $trigger = '-' ) use ( $fname, &$callback1Called ) {
1626 $callback1Called = $trigger;
1627 $this->database->query( "SELECT 1", $fname );
1628 };
1629 $callback2Called = null;
1630 $callback2 = function ( $trigger = '-' ) use ( $fname, &$callback2Called ) {
1631 $callback2Called = $trigger;
1632 $this->database->query( "SELECT 2", $fname );
1633 };
1634 $callback3Called = null;
1635 $callback3 = function ( $trigger = '-' ) use ( $fname, &$callback3Called ) {
1636 $callback3Called = $trigger;
1637 $this->database->query( "SELECT 3", $fname );
1638 };
1639
1640 $this->database->startAtomic( __METHOD__ . '_outer' );
1641 $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
1642 $this->database->startAtomic( __METHOD__ . '_inner' );
1643 $this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
1644 $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
1645 $this->database->onTransactionResolution( $callback3, __METHOD__ );
1646 $this->database->endAtomic( __METHOD__ . '_inner' );
1647 $this->database->cancelAtomic( __METHOD__ );
1648 $this->database->endAtomic( __METHOD__ . '_outer' );
1649 $this->assertNull( $callback1Called );
1650 $this->assertNull( $callback2Called );
1651 $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
1652 // phpcs:ignore Generic.Files.LineLength
1653 $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT; SELECT 3' );
1654
1655 $callback1Called = null;
1656 $callback2Called = null;
1657 $callback3Called = null;
1658 $this->database->startAtomic( __METHOD__ . '_outer' );
1659 $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
1660 $this->database->startAtomic( __METHOD__ . '_inner', IDatabase::ATOMIC_CANCELABLE );
1661 $this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
1662 $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
1663 $this->database->onTransactionResolution( $callback3, __METHOD__ );
1664 $this->database->endAtomic( __METHOD__ . '_inner' );
1665 $this->database->cancelAtomic( __METHOD__ );
1666 $this->database->endAtomic( __METHOD__ . '_outer' );
1667 $this->assertNull( $callback1Called );
1668 $this->assertNull( $callback2Called );
1669 $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
1670 // phpcs:ignore Generic.Files.LineLength
1671 $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; SAVEPOINT wikimedia_rdbms_atomic2; RELEASE SAVEPOINT wikimedia_rdbms_atomic2; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT; SELECT 3' );
1672
1673 $callback1Called = null;
1674 $callback2Called = null;
1675 $callback3Called = null;
1676 $this->database->startAtomic( __METHOD__ . '_outer' );
1677 $atomicId = $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
1678 $this->database->startAtomic( __METHOD__ . '_inner' );
1679 $this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
1680 $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
1681 $this->database->onTransactionResolution( $callback3, __METHOD__ );
1682 $this->database->cancelAtomic( __METHOD__, $atomicId );
1683 $this->database->endAtomic( __METHOD__ . '_outer' );
1684 $this->assertNull( $callback1Called );
1685 $this->assertNull( $callback2Called );
1686 $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
1687
1688 $callback1Called = null;
1689 $callback2Called = null;
1690 $callback3Called = null;
1691 $this->database->startAtomic( __METHOD__ . '_outer' );
1692 $atomicId = $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
1693 $this->database->startAtomic( __METHOD__ . '_inner' );
1694 $this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
1695 $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
1696 $this->database->onTransactionResolution( $callback3, __METHOD__ );
1697 try {
1698 $this->database->cancelAtomic( __METHOD__ . '_X', $atomicId );
1699 } catch ( DBUnexpectedError $e ) {
1700 $m = __METHOD__;
1701 $this->assertSame(
1702 "Invalid atomic section ended (got {$m}_X but expected {$m}).",
1703 $e->getMessage()
1704 );
1705 }
1706 $this->database->cancelAtomic( __METHOD__ );
1707 $this->database->endAtomic( __METHOD__ . '_outer' );
1708 $this->assertNull( $callback1Called );
1709 $this->assertNull( $callback2Called );
1710 $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
1711
1712 $this->database->startAtomic( __METHOD__ . '_outer' );
1713 $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
1714 $this->database->startAtomic( __METHOD__ . '_inner' );
1715 $this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
1716 $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
1717 $this->database->onTransactionResolution( $callback3, __METHOD__ );
1718 $this->database->cancelAtomic( __METHOD__ . '_inner' );
1719 $this->database->cancelAtomic( __METHOD__ );
1720 $this->database->endAtomic( __METHOD__ . '_outer' );
1721 $this->assertNull( $callback1Called );
1722 $this->assertNull( $callback2Called );
1723 $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
1724
1725 $wrapper = TestingAccessWrapper::newFromObject( $this->database );
1726 $callback1Called = null;
1727 $callback2Called = null;
1728 $callback3Called = null;
1729 $this->database->startAtomic( __METHOD__ . '_outer' );
1730 $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
1731 $this->database->startAtomic( __METHOD__ . '_inner' );
1732 $this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
1733 $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
1734 $this->database->onTransactionResolution( $callback3, __METHOD__ );
1735 $wrapper->trxStatus = Database::STATUS_TRX_ERROR;
1736 $this->database->cancelAtomic( __METHOD__ . '_inner' );
1737 $this->database->cancelAtomic( __METHOD__ );
1738 $this->database->endAtomic( __METHOD__ . '_outer' );
1739 $this->assertNull( $callback1Called );
1740 $this->assertNull( $callback2Called );
1741 $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
1742 }
1743
1744 /**
1745 * @covers \Wikimedia\Rdbms\Database::doSavepoint
1746 * @covers \Wikimedia\Rdbms\Database::doReleaseSavepoint
1747 * @covers \Wikimedia\Rdbms\Database::doRollbackToSavepoint
1748 * @covers \Wikimedia\Rdbms\Database::startAtomic
1749 * @covers \Wikimedia\Rdbms\Database::endAtomic
1750 * @covers \Wikimedia\Rdbms\Database::cancelAtomic
1751 * @covers \Wikimedia\Rdbms\Database::doAtomicSection
1752 */
1753 public function testAtomicSectionsTrxRound() {
1754 $this->database->setFlag( IDatabase::DBO_TRX );
1755 $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
1756 $this->database->query( 'SELECT 1', __METHOD__ );
1757 $this->database->endAtomic( __METHOD__ );
1758 $this->database->commit( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
1759 // phpcs:ignore Generic.Files.LineLength
1760 $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; SELECT 1; RELEASE SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
1761 }
1762
1763 public static function provideAtomicSectionMethodsForErrors() {
1764 return [
1765 [ 'endAtomic' ],
1766 [ 'cancelAtomic' ],
1767 ];
1768 }
1769
1770 /**
1771 * @dataProvider provideAtomicSectionMethodsForErrors
1772 * @covers \Wikimedia\Rdbms\Database::endAtomic
1773 * @covers \Wikimedia\Rdbms\Database::cancelAtomic
1774 */
1775 public function testNoAtomicSection( $method ) {
1776 try {
1777 $this->database->$method( __METHOD__ );
1778 $this->fail( 'Expected exception not thrown' );
1779 } catch ( DBUnexpectedError $ex ) {
1780 $this->assertSame(
1781 'No atomic section is open (got ' . __METHOD__ . ').',
1782 $ex->getMessage()
1783 );
1784 }
1785 }
1786
1787 /**
1788 * @dataProvider provideAtomicSectionMethodsForErrors
1789 * @covers \Wikimedia\Rdbms\Database::endAtomic
1790 * @covers \Wikimedia\Rdbms\Database::cancelAtomic
1791 */
1792 public function testInvalidAtomicSectionEnded( $method ) {
1793 $this->database->startAtomic( __METHOD__ . 'X' );
1794 try {
1795 $this->database->$method( __METHOD__ );
1796 $this->fail( 'Expected exception not thrown' );
1797 } catch ( DBUnexpectedError $ex ) {
1798 $this->assertSame(
1799 'Invalid atomic section ended (got ' . __METHOD__ . ' but expected ' .
1800 __METHOD__ . 'X' . ').',
1801 $ex->getMessage()
1802 );
1803 }
1804 }
1805
1806 /**
1807 * @covers \Wikimedia\Rdbms\Database::cancelAtomic
1808 */
1809 public function testUncancellableAtomicSection() {
1810 $this->database->startAtomic( __METHOD__ );
1811 try {
1812 $this->database->cancelAtomic( __METHOD__ );
1813 $this->database->select( 'test', '1', [], __METHOD__ );
1814 $this->fail( 'Expected exception not thrown' );
1815 } catch ( DBTransactionError $ex ) {
1816 $this->assertSame(
1817 'Cannot execute query from ' . __METHOD__ . ' while transaction status is ERROR.',
1818 $ex->getMessage()
1819 );
1820 }
1821 }
1822
1823 /**
1824 * @expectedException \Wikimedia\Rdbms\DBTransactionStateError
1825 */
1826 public function testTransactionErrorState1() {
1827 $wrapper = TestingAccessWrapper::newFromObject( $this->database );
1828
1829 $this->database->begin( __METHOD__ );
1830 $wrapper->trxStatus = Database::STATUS_TRX_ERROR;
1831 $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
1832 $this->database->commit( __METHOD__ );
1833 }
1834
1835 /**
1836 * @covers \Wikimedia\Rdbms\Database::query
1837 */
1838 public function testTransactionErrorState2() {
1839 $wrapper = TestingAccessWrapper::newFromObject( $this->database );
1840
1841 $this->database->startAtomic( __METHOD__ );
1842 $wrapper->trxStatus = Database::STATUS_TRX_ERROR;
1843 $this->database->rollback( __METHOD__ );
1844 $this->assertEquals( 0, $this->database->trxLevel() );
1845 $this->assertEquals( Database::STATUS_TRX_NONE, $wrapper->trxStatus() );
1846 $this->assertLastSql( 'BEGIN; ROLLBACK' );
1847
1848 $this->database->startAtomic( __METHOD__ );
1849 $this->assertEquals( Database::STATUS_TRX_OK, $wrapper->trxStatus() );
1850 $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
1851 $this->database->endAtomic( __METHOD__ );
1852 $this->assertEquals( Database::STATUS_TRX_NONE, $wrapper->trxStatus() );
1853 $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'1\'; COMMIT' );
1854 $this->assertEquals( 0, $this->database->trxLevel(), 'Use after rollback()' );
1855
1856 $this->database->begin( __METHOD__ );
1857 $this->database->startAtomic( __METHOD__, Database::ATOMIC_CANCELABLE );
1858 $this->database->update( 'y', [ 'a' => 1 ], [ 'field' => 1 ], __METHOD__ );
1859 $wrapper->trxStatus = Database::STATUS_TRX_ERROR;
1860 $this->database->cancelAtomic( __METHOD__ );
1861 $this->assertEquals( Database::STATUS_TRX_OK, $wrapper->trxStatus() );
1862 $this->database->startAtomic( __METHOD__ );
1863 $this->database->delete( 'y', [ 'field' => 1 ], __METHOD__ );
1864 $this->database->endAtomic( __METHOD__ );
1865 $this->database->commit( __METHOD__ );
1866 // phpcs:ignore Generic.Files.LineLength
1867 $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; UPDATE y SET a = \'1\' WHERE field = \'1\'; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; DELETE FROM y WHERE field = \'1\'; COMMIT' );
1868 $this->assertEquals( 0, $this->database->trxLevel(), 'Use after rollback()' );
1869
1870 // Next transaction
1871 $this->database->startAtomic( __METHOD__ );
1872 $this->assertEquals( Database::STATUS_TRX_OK, $wrapper->trxStatus() );
1873 $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
1874 $this->database->endAtomic( __METHOD__ );
1875 $this->assertEquals( Database::STATUS_TRX_NONE, $wrapper->trxStatus() );
1876 $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; COMMIT' );
1877 $this->assertEquals( 0, $this->database->trxLevel() );
1878 }
1879
1880 /**
1881 * @covers \Wikimedia\Rdbms\Database::query
1882 */
1883 public function testImplicitTransactionRollback() {
1884 $doError = function () {
1885 $this->database->forceNextQueryError( 666, 'Evilness' );
1886 try {
1887 $this->database->delete( 'error', '1', __CLASS__ . '::SomeCaller' );
1888 $this->fail( 'Expected exception not thrown' );
1889 } catch ( DBError $e ) {
1890 $this->assertSame( 666, $e->errno );
1891 }
1892 };
1893
1894 $this->database->setFlag( Database::DBO_TRX );
1895
1896 // Implicit transaction gets silently rolled back
1897 $this->database->begin( __METHOD__, Database::TRANSACTION_INTERNAL );
1898 call_user_func( $doError );
1899 $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
1900 $this->database->commit( __METHOD__, Database::FLUSHING_INTERNAL );
1901 // phpcs:ignore
1902 $this->assertLastSql( 'BEGIN; DELETE FROM error WHERE 1; ROLLBACK; BEGIN; DELETE FROM x WHERE field = \'1\'; COMMIT' );
1903
1904 // ... unless there were prior writes
1905 $this->database->begin( __METHOD__, Database::TRANSACTION_INTERNAL );
1906 $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
1907 call_user_func( $doError );
1908 try {
1909 $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
1910 $this->fail( 'Expected exception not thrown' );
1911 } catch ( DBTransactionStateError $e ) {
1912 }
1913 $this->database->rollback( __METHOD__, Database::FLUSHING_INTERNAL );
1914 // phpcs:ignore
1915 $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'1\'; DELETE FROM error WHERE 1; ROLLBACK' );
1916 }
1917
1918 /**
1919 * @covers \Wikimedia\Rdbms\Database::query
1920 */
1921 public function testTransactionStatementRollbackIgnoring() {
1922 $wrapper = TestingAccessWrapper::newFromObject( $this->database );
1923 $warning = [];
1924 $wrapper->deprecationLogger = function ( $msg ) use ( &$warning ) {
1925 $warning[] = $msg;
1926 };
1927
1928 $doError = function () {
1929 $this->database->forceNextQueryError( 666, 'Evilness', [
1930 'wasKnownStatementRollbackError' => true,
1931 ] );
1932 try {
1933 $this->database->delete( 'error', '1', __CLASS__ . '::SomeCaller' );
1934 $this->fail( 'Expected exception not thrown' );
1935 } catch ( DBError $e ) {
1936 $this->assertSame( 666, $e->errno );
1937 }
1938 };
1939 $expectWarning = 'Caller from ' . __METHOD__ .
1940 ' ignored an error originally raised from ' . __CLASS__ . '::SomeCaller: [666] Evilness';
1941
1942 // Rollback doesn't raise a warning
1943 $warning = [];
1944 $this->database->startAtomic( __METHOD__ );
1945 call_user_func( $doError );
1946 $this->database->rollback( __METHOD__ );
1947 $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
1948 $this->assertSame( [], $warning );
1949 // phpcs:ignore
1950 $this->assertLastSql( 'BEGIN; DELETE FROM error WHERE 1; ROLLBACK; DELETE FROM x WHERE field = \'1\'' );
1951
1952 // cancelAtomic() doesn't raise a warning
1953 $warning = [];
1954 $this->database->begin( __METHOD__ );
1955 $this->database->startAtomic( __METHOD__, Database::ATOMIC_CANCELABLE );
1956 call_user_func( $doError );
1957 $this->database->cancelAtomic( __METHOD__ );
1958 $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
1959 $this->database->commit( __METHOD__ );
1960 $this->assertSame( [], $warning );
1961 // phpcs:ignore
1962 $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; DELETE FROM error WHERE 1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; DELETE FROM x WHERE field = \'1\'; COMMIT' );
1963
1964 // Commit does raise a warning
1965 $warning = [];
1966 $this->database->begin( __METHOD__ );
1967 call_user_func( $doError );
1968 $this->database->commit( __METHOD__ );
1969 $this->assertSame( [ $expectWarning ], $warning );
1970 $this->assertLastSql( 'BEGIN; DELETE FROM error WHERE 1; COMMIT' );
1971
1972 // Deprecation only gets raised once
1973 $warning = [];
1974 $this->database->begin( __METHOD__ );
1975 call_user_func( $doError );
1976 $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
1977 $this->database->commit( __METHOD__ );
1978 $this->assertSame( [ $expectWarning ], $warning );
1979 // phpcs:ignore
1980 $this->assertLastSql( 'BEGIN; DELETE FROM error WHERE 1; DELETE FROM x WHERE field = \'1\'; COMMIT' );
1981 }
1982
1983 /**
1984 * @covers \Wikimedia\Rdbms\Database::close
1985 */
1986 public function testPrematureClose1() {
1987 $fname = __METHOD__;
1988 $this->database->begin( __METHOD__ );
1989 $this->database->onTransactionCommitOrIdle( function () use ( $fname ) {
1990 $this->database->query( 'SELECT 1', $fname );
1991 } );
1992 $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
1993 $this->database->close();
1994
1995 $this->assertFalse( $this->database->isOpen() );
1996 $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; COMMIT; SELECT 1' );
1997 $this->assertEquals( 0, $this->database->trxLevel() );
1998 }
1999
2000 /**
2001 * @covers \Wikimedia\Rdbms\Database::close
2002 */
2003 public function testPrematureClose2() {
2004 try {
2005 $fname = __METHOD__;
2006 $this->database->startAtomic( __METHOD__ );
2007 $this->database->onTransactionCommitOrIdle( function () use ( $fname ) {
2008 $this->database->query( 'SELECT 1', $fname );
2009 } );
2010 $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
2011 $this->database->close();
2012 $this->fail( 'Expected exception not thrown' );
2013 } catch ( DBUnexpectedError $ex ) {
2014 $this->assertSame(
2015 'Wikimedia\Rdbms\Database::close: atomic sections ' .
2016 'DatabaseSQLTest::testPrematureClose2 are still open.',
2017 $ex->getMessage()
2018 );
2019 }
2020
2021 $this->assertFalse( $this->database->isOpen() );
2022 $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; ROLLBACK' );
2023 $this->assertEquals( 0, $this->database->trxLevel() );
2024 }
2025
2026 /**
2027 * @covers \Wikimedia\Rdbms\Database::close
2028 */
2029 public function testPrematureClose3() {
2030 try {
2031 $this->database->setFlag( IDatabase::DBO_TRX );
2032 $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
2033 $this->assertEquals( 1, $this->database->trxLevel() );
2034 $this->database->close();
2035 $this->fail( 'Expected exception not thrown' );
2036 } catch ( DBUnexpectedError $ex ) {
2037 $this->assertSame(
2038 'Wikimedia\Rdbms\Database::close: ' .
2039 'mass commit/rollback of peer transaction required (DBO_TRX set).',
2040 $ex->getMessage()
2041 );
2042 }
2043
2044 $this->assertFalse( $this->database->isOpen() );
2045 $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; ROLLBACK' );
2046 $this->assertEquals( 0, $this->database->trxLevel() );
2047 }
2048
2049 /**
2050 * @covers \Wikimedia\Rdbms\Database::close
2051 */
2052 public function testPrematureClose4() {
2053 $this->database->setFlag( IDatabase::DBO_TRX );
2054 $this->database->query( 'SELECT 1', __METHOD__ );
2055 $this->assertEquals( 1, $this->database->trxLevel() );
2056 $this->database->close();
2057 $this->database->clearFlag( IDatabase::DBO_TRX );
2058
2059 $this->assertFalse( $this->database->isOpen() );
2060 $this->assertLastSql( 'BEGIN; SELECT 1; COMMIT' );
2061 $this->assertEquals( 0, $this->database->trxLevel() );
2062 }
2063 }