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