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