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