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
;
11 * Test the parts of the Database abstract class that deal
12 * with creating SQL text.
14 class DatabaseSQLTest
extends PHPUnit\Framework\TestCase
{
16 use MediaWikiCoversValidator
;
18 /** @var DatabaseTestHelper|Database */
21 protected function setUp() {
23 $this->database
= new DatabaseTestHelper( __CLASS__
, [ 'cliMode' => true ] );
26 protected function assertLastSql( $sqlText ) {
29 $this->database
->getLastSqls()
33 protected function assertLastSqlDb( $sqlText, DatabaseTestHelper
$db ) {
34 $this->assertEquals( $sqlText, $db->getLastSqls() );
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
48 public function testSelect( $sql, $sqlText ) {
49 $this->database
->select(
52 isset( $sql['conds'] ) ?
$sql['conds'] : [],
54 isset( $sql['options'] ) ?
$sql['options'] : [],
55 isset( $sql['join_conds'] ) ?
$sql['join_conds'] : []
57 $this->assertLastSql( $sqlText );
60 public static function provideSelect() {
65 'fields' => [ 'field', 'alias' => 'field2' ],
66 'conds' => [ 'alias' => 'text' ],
68 "SELECT field,field2 AS alias " .
70 "WHERE alias = 'text'"
75 'fields' => [ 'field', 'alias' => 'field2' ],
76 'conds' => 'alias = \'text\'',
78 "SELECT field,field2 AS alias " .
80 "WHERE alias = 'text'"
85 'fields' => [ 'field', 'alias' => 'field2' ],
88 "SELECT field,field2 AS alias " .
94 'fields' => [ 'field', 'alias' => 'field2' ],
97 "SELECT field,field2 AS alias " .
103 'fields' => [ 'field', 'alias' => 'field2' ],
104 'conds' => '0', // T188314
106 "SELECT field,field2 AS alias " .
112 // 'tables' with space prepended indicates pre-escaped table name
113 'tables' => ' table LEFT JOIN table2',
114 'fields' => [ 'field' ],
115 'conds' => [ 'field' => 'text' ],
117 "SELECT field FROM table LEFT JOIN table2 WHERE field = 'text'"
121 // Empty 'tables' is allowed
123 'fields' => [ 'SPECIAL_QUERY()' ],
125 "SELECT SPECIAL_QUERY()"
130 'fields' => [ 'field', 'alias' => 'field2' ],
131 'conds' => [ 'alias' => 'text' ],
132 'options' => [ 'LIMIT' => 1, 'ORDER BY' => 'field' ],
134 "SELECT field,field2 AS alias " .
136 "WHERE alias = 'text' " .
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'
150 "SELECT tid,field,field2 AS alias,t2.id " .
151 "FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " .
152 "WHERE alias = 'text' " .
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'
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 " .
174 'tables' => [ 'table', 't2' => 'table2' ],
175 'fields' => [ 'tid', 'field', 'alias' => 'field2', 't2.id' ],
176 'conds' => [ 'alias' => 'text' ],
179 'GROUP BY' => [ 'field', 'field2' ],
180 'HAVING' => [ 'COUNT(*) > 1', 'field' => 1 ]
182 'join_conds' => [ 't2' => [
183 'LEFT JOIN', 'tid = t2.id'
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' " .
194 'tables' => [ 'table' ],
195 'fields' => [ 'alias' => 'field' ],
196 'conds' => [ 'alias' => [ 1, 2, 3, 4 ] ],
198 "SELECT field AS alias " .
200 "WHERE alias IN ('1','2','3','4')"
205 'fields' => [ 'field' ],
206 'options' => [ 'USE INDEX' => [ 'table' => 'X' ] ],
209 "SELECT field FROM table"
214 'fields' => [ 'field' ],
215 'options' => [ 'IGNORE INDEX' => [ 'table' => 'X' ] ],
218 "SELECT field FROM table"
223 'fields' => [ 'field' ],
224 'options' => [ 'DISTINCT', 'LOCK IN SHARE MODE' ],
226 "SELECT DISTINCT field FROM table LOCK IN SHARE MODE"
231 'fields' => [ 'field' ],
232 'options' => [ 'EXPLAIN' => true ],
234 'EXPLAIN SELECT field FROM table'
239 'fields' => [ 'field' ],
240 'options' => [ 'FOR UPDATE' ],
242 "SELECT field FROM table FOR UPDATE"
248 * @covers Wikimedia\Rdbms\Subquery
249 * @dataProvider provideSelectRowCount
253 public function testSelectRowCount( $sql, $sqlText ) {
254 $this->database
->selectRowCount(
257 isset( $sql['conds'] ) ?
$sql['conds'] : [],
259 isset( $sql['options'] ) ?
$sql['options'] : [],
260 isset( $sql['join_conds'] ) ?
$sql['join_conds'] : []
262 $this->assertLastSql( $sqlText );
265 public static function provideSelectRowCount() {
271 'conds' => [ 'field' => 'text' ],
273 "SELECT COUNT(*) AS rowcount FROM " .
274 "(SELECT 1 FROM table WHERE field = 'text' ) tmp_count"
279 'field' => [ 'column' ],
280 'conds' => [ 'field' => 'text' ],
282 "SELECT COUNT(*) AS rowcount FROM " .
283 "(SELECT 1 FROM table WHERE field = 'text' AND (column IS NOT NULL) ) tmp_count"
288 'field' => [ 'alias' => 'column' ],
289 'conds' => [ 'field' => 'text' ],
291 "SELECT COUNT(*) AS rowcount FROM " .
292 "(SELECT 1 FROM table WHERE field = 'text' AND (column IS NOT NULL) ) tmp_count"
297 'field' => [ 'alias' => 'column' ],
300 "SELECT COUNT(*) AS rowcount FROM " .
301 "(SELECT 1 FROM table WHERE (column IS NOT NULL) ) tmp_count"
306 'field' => [ 'alias' => 'column' ],
309 "SELECT COUNT(*) AS rowcount FROM " .
310 "(SELECT 1 FROM table WHERE (column IS NOT NULL) ) tmp_count"
315 'field' => [ 'alias' => 'column' ],
318 "SELECT COUNT(*) AS rowcount FROM " .
319 "(SELECT 1 FROM table WHERE (column IS NOT NULL) ) tmp_count"
324 'field' => [ 'alias' => 'column' ],
327 "SELECT COUNT(*) AS rowcount FROM " .
328 "(SELECT 1 FROM table WHERE (1) AND (column IS NOT NULL) ) tmp_count"
333 'field' => [ 'alias' => 'column' ],
336 "SELECT COUNT(*) AS rowcount FROM " .
337 "(SELECT 1 FROM table WHERE (0) AND (column IS NOT NULL) ) tmp_count"
343 * @dataProvider provideUpdate
344 * @covers Wikimedia\Rdbms\Database::update
345 * @covers Wikimedia\Rdbms\Database::makeUpdateOptions
346 * @covers Wikimedia\Rdbms\Database::makeUpdateOptionsArray
348 public function testUpdate( $sql, $sqlText ) {
349 $this->database
->update(
354 isset( $sql['options'] ) ?
$sql['options'] : []
356 $this->assertLastSql( $sqlText );
359 public static function provideUpdate() {
364 'values' => [ 'field' => 'text', 'field2' => 'text2' ],
365 'conds' => [ 'alias' => 'text' ],
368 "SET field = 'text'" .
369 ",field2 = 'text2' " .
370 "WHERE alias = 'text'"
375 'values' => [ 'field = other', 'field2' => 'text2' ],
376 'conds' => [ 'id' => '1' ],
379 "SET field = other" .
380 ",field2 = 'text2' " .
386 'values' => [ 'field = other', 'field2' => 'text2' ],
390 "SET field = other" .
397 * @dataProvider provideDelete
398 * @covers Wikimedia\Rdbms\Database::delete
400 public function testDelete( $sql, $sqlText ) {
401 $this->database
->delete(
406 $this->assertLastSql( $sqlText );
409 public static function provideDelete() {
414 'conds' => [ 'alias' => 'text' ],
416 "DELETE FROM table " .
417 "WHERE alias = 'text'"
430 * @dataProvider provideUpsert
431 * @covers Wikimedia\Rdbms\Database::upsert
433 public function testUpsert( $sql, $sqlText ) {
434 $this->database
->upsert(
437 $sql['uniqueIndexes'],
441 $this->assertLastSql( $sqlText );
444 public static function provideUpsert() {
448 'table' => 'upsert_table',
449 'rows' => [ 'field' => 'text', 'field2' => 'text2' ],
450 'uniqueIndexes' => [ 'field' ],
451 'set' => [ 'field' => 'set' ],
454 "UPDATE upsert_table " .
455 "SET field = 'set' " .
456 "WHERE ((field = 'text')); " .
457 "INSERT IGNORE INTO upsert_table " .
459 "VALUES ('text','text2'); " .
466 * @dataProvider provideDeleteJoin
467 * @covers Wikimedia\Rdbms\Database::deleteJoin
469 public function testDeleteJoin( $sql, $sqlText ) {
470 $this->database
->deleteJoin(
478 $this->assertLastSql( $sqlText );
481 public static function provideDeleteJoin() {
485 'delTable' => 'table',
486 'joinTable' => 'table_join',
488 'joinVar' => 'field_join',
489 'conds' => [ 'alias' => 'text' ],
491 "DELETE FROM table " .
493 "SELECT field_join FROM table_join WHERE alias = 'text'" .
498 'delTable' => 'table',
499 'joinTable' => 'table_join',
501 'joinVar' => 'field_join',
504 "DELETE FROM table " .
506 "SELECT field_join FROM table_join " .
513 * @dataProvider provideInsert
514 * @covers Wikimedia\Rdbms\Database::insert
515 * @covers Wikimedia\Rdbms\Database::makeInsertOptions
517 public function testInsert( $sql, $sqlText ) {
518 $this->database
->insert(
522 isset( $sql['options'] ) ?
$sql['options'] : []
524 $this->assertLastSql( $sqlText );
527 public static function provideInsert() {
532 'rows' => [ 'field' => 'text', 'field2' => 2 ],
534 "INSERT INTO table " .
536 "VALUES ('text','2')"
541 'rows' => [ 'field' => 'text', 'field2' => 2 ],
542 'options' => 'IGNORE',
544 "INSERT IGNORE INTO table " .
546 "VALUES ('text','2')"
552 [ 'field' => 'text', 'field2' => 2 ],
553 [ 'field' => 'multi', 'field2' => 3 ],
555 'options' => 'IGNORE',
557 "INSERT IGNORE INTO table " .
567 * @dataProvider provideInsertSelect
568 * @covers Wikimedia\Rdbms\Database::insertSelect
569 * @covers Wikimedia\Rdbms\Database::nativeInsertSelect
571 public function testInsertSelect( $sql, $sqlTextNative, $sqlSelect, $sqlInsert ) {
572 $this->database
->insertSelect(
578 isset( $sql['insertOptions'] ) ?
$sql['insertOptions'] : [],
579 isset( $sql['selectOptions'] ) ?
$sql['selectOptions'] : [],
580 isset( $sql['selectJoinConds'] ) ?
$sql['selectJoinConds'] : []
582 $this->assertLastSql( $sqlTextNative );
584 $dbWeb = new DatabaseTestHelper( __CLASS__
, [ 'cliMode' => false ] );
585 $dbWeb->forceNextResult( [
586 array_flip( array_keys( $sql['varMap'] ) )
588 $dbWeb->insertSelect(
594 isset( $sql['insertOptions'] ) ?
$sql['insertOptions'] : [],
595 isset( $sql['selectOptions'] ) ?
$sql['selectOptions'] : [],
596 isset( $sql['selectJoinConds'] ) ?
$sql['selectJoinConds'] : []
598 $this->assertLastSqlDb( implode( '; ', [ $sqlSelect, 'BEGIN', $sqlInsert, 'COMMIT' ] ), $dbWeb );
601 public static function provideInsertSelect() {
605 'destTable' => 'insert_table',
606 'srcTable' => 'select_table',
607 'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
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')"
620 'destTable' => 'insert_table',
621 'srcTable' => 'select_table',
622 'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
623 'conds' => [ 'field' => 2 ],
625 "INSERT INTO insert_table " .
626 "(field_insert,field) " .
627 "SELECT field_select,field2 " .
628 "FROM select_table " .
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')"
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' ],
643 "INSERT IGNORE INTO insert_table " .
644 "(field_insert,field) " .
645 "SELECT field_select,field2 " .
646 "FROM select_table " .
647 "WHERE field = '2' " .
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')"
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' ] ],
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' " .
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')"
679 public function testInsertSelectBatching() {
680 $dbWeb = new DatabaseTestHelper( __CLASS__
, [ 'cliMode' => false ] );
682 for ( $i = 0; $i <= 25000; $i++
) {
683 $rows[] = [ 'field' => $i ];
685 $dbWeb->forceNextResult( $rows );
686 $dbWeb->insertSelect(
689 [ 'field' => 'field2' ],
693 $this->assertLastSqlDb( implode( '; ', [
694 'SELECT field2 AS field FROM select_table WHERE * FOR UPDATE',
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 ) ) . "')",
704 * @dataProvider provideReplace
705 * @covers Wikimedia\Rdbms\Database::replace
707 public function testReplace( $sql, $sqlText ) {
708 $this->database
->replace(
710 $sql['uniqueIndexes'],
714 $this->assertLastSql( $sqlText );
717 public static function provideReplace() {
721 'table' => 'replace_table',
722 'uniqueIndexes' => [ 'field' ],
723 'rows' => [ 'field' => 'text', 'field2' => 'text2' ],
725 "BEGIN; DELETE FROM replace_table " .
726 "WHERE (field = 'text'); " .
727 "INSERT INTO replace_table " .
729 "VALUES ('text','text2'); COMMIT"
733 'table' => 'module_deps',
734 'uniqueIndexes' => [ [ 'md_module', 'md_skin' ] ],
736 'md_module' => 'module',
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"
749 'table' => 'module_deps',
750 'uniqueIndexes' => [ [ 'md_module', 'md_skin' ] ],
753 'md_module' => 'module',
757 'md_module' => 'module2',
758 'md_skin' => 'skin2',
759 'md_deps' => 'deps2',
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"
776 'table' => 'module_deps',
777 'uniqueIndexes' => [ 'md_module', 'md_skin' ],
780 'md_module' => 'module',
784 'md_module' => 'module2',
785 'md_skin' => 'skin2',
786 'md_deps' => 'deps2',
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"
803 'table' => 'module_deps',
804 'uniqueIndexes' => [],
806 'md_module' => 'module',
811 "BEGIN; INSERT INTO module_deps " .
812 "(md_module,md_skin,md_deps) " .
813 "VALUES ('module','skin','deps'); COMMIT"
819 * @dataProvider provideNativeReplace
820 * @covers Wikimedia\Rdbms\Database::nativeReplace
822 public function testNativeReplace( $sql, $sqlText ) {
823 $this->database
->nativeReplace(
828 $this->assertLastSql( $sqlText );
831 public static function provideNativeReplace() {
835 'table' => 'replace_table',
836 'rows' => [ 'field' => 'text', 'field2' => 'text2' ],
838 "REPLACE INTO replace_table " .
840 "VALUES ('text','text2')"
846 * @dataProvider provideConditional
847 * @covers Wikimedia\Rdbms\Database::conditional
849 public function testConditional( $sql, $sqlText ) {
850 $this->assertEquals( trim( $this->database
->conditional(
857 public static function provideConditional() {
861 'conds' => [ 'field' => 'text' ],
865 "(CASE WHEN field = 'text' THEN 1 ELSE NULL END)"
869 'conds' => [ 'field' => 'text', 'field2' => 'anothertext' ],
873 "(CASE WHEN field = 'text' AND field2 = 'anothertext' THEN 1 ELSE NULL END)"
877 'conds' => 'field=1',
881 "(CASE WHEN field=1 THEN 1 ELSE NULL END)"
887 * @dataProvider provideBuildConcat
888 * @covers Wikimedia\Rdbms\Database::buildConcat
890 public function testBuildConcat( $stringList, $sqlText ) {
891 $this->assertEquals( trim( $this->database
->buildConcat(
896 public static function provideBuildConcat() {
899 [ 'field', 'field2' ],
900 "CONCAT(field,field2)"
903 [ "'test'", 'field2' ],
904 "CONCAT('test',field2)"
910 * @dataProvider provideBuildLike
911 * @covers Wikimedia\Rdbms\Database::buildLike
912 * @covers Wikimedia\Rdbms\Database::escapeLikeInternal
914 public function testBuildLike( $array, $sqlText ) {
915 $this->assertEquals( trim( $this->database
->buildLike(
920 public static function provideBuildLike() {
924 "LIKE 'text' ESCAPE '`'"
927 [ 'text', new LikeMatch( '%' ) ],
928 "LIKE 'text%' ESCAPE '`'"
931 [ 'text', new LikeMatch( '%' ), 'text2' ],
932 "LIKE 'text%text2' ESCAPE '`'"
935 [ 'text', new LikeMatch( '_' ) ],
936 "LIKE 'text_' ESCAPE '`'"
940 "LIKE 'more`_text' ESCAPE '`'"
943 [ 'C:\\Windows\\', new LikeMatch( '%' ) ],
944 "LIKE 'C:\\Windows\\%' ESCAPE '`'"
947 [ 'accent`_test`', new LikeMatch( '%' ) ],
948 "LIKE 'accent```_test``%' ESCAPE '`'"
954 * @dataProvider provideUnionQueries
955 * @covers Wikimedia\Rdbms\Database::unionQueries
957 public function testUnionQueries( $sql, $sqlText ) {
958 $this->assertEquals( trim( $this->database
->unionQueries(
964 public static function provideUnionQueries() {
968 'sqls' => [ 'RAW SQL', 'RAW2SQL' ],
971 "(RAW SQL) UNION ALL (RAW2SQL)"
975 'sqls' => [ 'RAW SQL', 'RAW2SQL' ],
978 "(RAW SQL) UNION (RAW2SQL)"
982 'sqls' => [ 'RAW SQL', 'RAW2SQL', 'RAW3SQL' ],
985 "(RAW SQL) UNION (RAW2SQL) UNION (RAW3SQL)"
991 * @dataProvider provideUnionConditionPermutations
992 * @covers Wikimedia\Rdbms\Database::unionConditionPermutations
994 public function testUnionConditionPermutations( $params, $expect ) {
995 if ( isset( $params['unionSupportsOrderAndLimit'] ) ) {
996 $this->database
->setUnionSupportsOrderAndLimit( $params['unionSupportsOrderAndLimit'] );
999 $sql = trim( $this->database
->unionConditionPermutations(
1002 $params['permute_conds'],
1003 isset( $params['extra_conds'] ) ?
$params['extra_conds'] : '',
1005 isset( $params['options'] ) ?
$params['options'] : [],
1006 isset( $params['join_conds'] ) ?
$params['join_conds'] : []
1008 $this->assertEquals( $expect, $sql );
1011 public static function provideUnionConditionPermutations() {
1012 // phpcs:disable Generic.Files.LineLength
1016 'table' => [ 'table1', 'table2' ],
1017 'vars' => [ 'field1', 'alias' => 'field2' ],
1018 'permute_conds' => [
1019 'field3' => [ 1, 2, 3 ],
1020 'duplicates' => [ 4, 5, 4 ],
1024 'extra_conds' => 'table2.bar > 23',
1026 'ORDER BY' => [ 'field1', 'alias' ],
1027 'INNER ORDER BY' => [ 'field1', 'field2' ],
1031 'table2' => [ 'JOIN', 'table1.foo_id = table2.foo_id' ],
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"
1045 'vars' => [ 'foo_id' ],
1046 'permute_conds' => [
1047 'bar' => [ 1, 2, 3 ],
1049 'extra_conds' => [ 'baz' => null ],
1052 'ORDER BY' => [ 'foo_id' ],
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"
1064 'vars' => [ 'foo_id' ],
1065 'permute_conds' => [
1066 'bar' => [ 1, 2, 3 ],
1068 'extra_conds' => [ 'baz' => null ],
1071 'ORDER BY' => [ 'foo_id' ],
1074 'unionSupportsOrderAndLimit' => false,
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"
1084 'vars' => [ 'foo_id' ],
1085 'permute_conds' => [],
1086 'extra_conds' => [ 'baz' => null ],
1088 'ORDER BY' => [ 'foo_id' ],
1092 "SELECT foo_id FROM foo WHERE baz IS NULL ORDER BY foo_id LIMIT 25"
1097 'vars' => [ 'foo_id' ],
1098 'permute_conds' => [
1101 'extra_conds' => [ 'baz' => null ],
1103 'ORDER BY' => [ 'foo_id' ],
1107 "SELECT foo_id FROM foo WHERE baz IS NULL ORDER BY foo_id LIMIT 25"
1112 'vars' => [ 'foo_id' ],
1113 'permute_conds' => [
1117 'ORDER BY' => [ 'foo_id' ],
1122 "SELECT foo_id FROM foo WHERE bar = '1' ORDER BY foo_id LIMIT 150,25"
1127 'vars' => [ 'foo_id' ],
1128 'permute_conds' => [],
1129 'extra_conds' => [ 'baz' => null ],
1131 'ORDER BY' => [ 'foo_id' ],
1134 'INNER ORDER BY' => [ 'bar_id' ],
1137 "(SELECT foo_id FROM foo WHERE baz IS NULL ORDER BY bar_id LIMIT 175 ) ORDER BY foo_id LIMIT 150,25"
1142 'vars' => [ 'foo_id' ],
1143 'permute_conds' => [],
1144 'extra_conds' => [ 'baz' => null ],
1146 'ORDER BY' => [ 'foo_id' ],
1149 'INNER ORDER BY' => [ 'bar_id' ],
1151 'unionSupportsOrderAndLimit' => false,
1153 "SELECT foo_id FROM foo WHERE baz IS NULL ORDER BY foo_id LIMIT 150,25"
1160 * @covers Wikimedia\Rdbms\Database::commit
1161 * @covers Wikimedia\Rdbms\Database::doCommit
1163 public function testTransactionCommit() {
1164 $this->database
->begin( __METHOD__
);
1165 $this->database
->commit( __METHOD__
);
1166 $this->assertLastSql( 'BEGIN; COMMIT' );
1170 * @covers Wikimedia\Rdbms\Database::rollback
1171 * @covers Wikimedia\Rdbms\Database::doRollback
1173 public function testTransactionRollback() {
1174 $this->database
->begin( __METHOD__
);
1175 $this->database
->rollback( __METHOD__
);
1176 $this->assertLastSql( 'BEGIN; ROLLBACK' );
1180 * @covers Wikimedia\Rdbms\Database::dropTable
1182 public function testDropTable() {
1183 $this->database
->setExistingTables( [ 'table' ] );
1184 $this->database
->dropTable( 'table', __METHOD__
);
1185 $this->assertLastSql( 'DROP TABLE table CASCADE' );
1189 * @covers Wikimedia\Rdbms\Database::dropTable
1191 public function testDropNonExistingTable() {
1193 $this->database
->dropTable( 'non_existing', __METHOD__
)
1198 * @dataProvider provideMakeList
1199 * @covers Wikimedia\Rdbms\Database::makeList
1201 public function testMakeList( $list, $mode, $sqlText ) {
1202 $this->assertEquals( trim( $this->database
->makeList(
1207 public static function provideMakeList() {
1210 [ 'value', 'value2' ],
1215 [ 'field', 'field2' ],
1220 [ 'field' => 'value', 'field2' => 'value2' ],
1222 "field = 'value' AND field2 = 'value2'"
1225 [ 'field' => null, "field2 != 'value2'" ],
1227 "field IS NULL AND (field2 != 'value2')"
1230 [ 'field' => [ 'value', null, 'value2' ], 'field2' => 'value2' ],
1232 "(field IN ('value','value2') OR field IS NULL) AND field2 = 'value2'"
1235 [ 'field' => [ null ], 'field2' => null ],
1237 "field IS NULL AND field2 IS NULL"
1240 [ 'field' => 'value', 'field2' => 'value2' ],
1242 "field = 'value' OR field2 = 'value2'"
1245 [ 'field' => 'value', 'field2' => null ],
1247 "field = 'value' OR field2 IS NULL"
1250 [ 'field' => [ 'value', 'value2' ], 'field2' => [ 'value' ] ],
1252 "field IN ('value','value2') OR field2 = 'value'"
1255 [ 'field' => [ null, 'value', null, 'value2' ], "field2 != 'value2'" ],
1257 "(field IN ('value','value2') OR field IS NULL) OR (field2 != 'value2')"
1260 [ 'field' => 'value', 'field2' => 'value2' ],
1262 "field = 'value',field2 = 'value2'"
1265 [ 'field' => 'value', 'field2' => null ],
1267 "field = 'value',field2 = NULL"
1270 [ 'field' => 'value', "field2 != 'value2'" ],
1272 "field = 'value',field2 != 'value2'"
1278 * @covers Wikimedia\Rdbms\Database::registerTempTableOperation
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' );
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__
);
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__
) );
1293 $this->database
->dropTable( 'tmp_table_1', __METHOD__
);
1294 $this->database
->dropTable( 'tmp_table_2', __METHOD__
);
1295 $this->database
->dropTable( 'tmp_table_3', __METHOD__
);
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__
) );
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__
);
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__
) );
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__
);
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__
) );
1318 public function provideBuildSubstring() {
1319 yield
[ 'someField', 1, 2, 'SUBSTRING(someField FROM 1 FOR 2)' ];
1320 yield
[ 'someField', 1, null, 'SUBSTRING(someField FROM 1)' ];
1324 * @covers Wikimedia\Rdbms\Database::buildSubstring
1325 * @dataProvider provideBuildSubstring
1327 public function testBuildSubstring( $input, $start, $length, $expected ) {
1328 $output = $this->database
->buildSubstring( $input, $start, $length );
1329 $this->assertSame( $expected, $output );
1332 public function provideBuildSubstring_invalidParams() {
1342 * @covers Wikimedia\Rdbms\Database::buildSubstring
1343 * @covers Wikimedia\Rdbms\Database::assertBuildSubstringParams
1344 * @dataProvider provideBuildSubstring_invalidParams
1346 public function testBuildSubstring_invalidParams( $start, $length ) {
1347 $this->setExpectedException( InvalidArgumentException
::class );
1348 $this->database
->buildSubstring( 'foo', $start, $length );
1352 * @covers \Wikimedia\Rdbms\Database::buildIntegerCast
1354 public function testBuildIntegerCast() {
1355 $output = $this->database
->buildIntegerCast( 'fieldName' );
1356 $this->assertSame( 'CAST( fieldName AS INTEGER )', $output );
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
1368 public function testAtomicSections() {
1369 $this->database
->startAtomic( __METHOD__
);
1370 $this->database
->endAtomic( __METHOD__
);
1371 $this->assertLastSql( 'BEGIN; COMMIT' );
1373 $this->database
->startAtomic( __METHOD__
, IDatabase
::ATOMIC_CANCELABLE
);
1374 $this->database
->cancelAtomic( __METHOD__
);
1375 $this->assertLastSql( 'BEGIN; ROLLBACK' );
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' );
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' );
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' );
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' );
1404 $this->database
->doAtomicSection( __METHOD__
, function () {
1406 $this->assertLastSql( 'BEGIN; COMMIT' );
1408 $this->database
->begin( __METHOD__
);
1409 $this->database
->doAtomicSection( __METHOD__
, function () {
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' );
1415 $this->database
->begin( __METHOD__
);
1417 $this->database
->doAtomicSection( __METHOD__
, function () {
1418 throw new RuntimeException( 'Test exception' );
1420 $this->fail( 'Expected exception not thrown' );
1421 } catch ( RuntimeException
$ex ) {
1422 $this->assertSame( 'Test exception', $ex->getMessage() );
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' );
1429 public static function provideAtomicSectionMethodsForErrors() {
1437 * @dataProvider provideAtomicSectionMethodsForErrors
1438 * @covers \Wikimedia\Rdbms\Database::endAtomic
1439 * @covers \Wikimedia\Rdbms\Database::cancelAtomic
1441 public function testNoAtomicSection( $method ) {
1443 $this->database
->$method( __METHOD__
);
1444 $this->fail( 'Expected exception not thrown' );
1445 } catch ( DBUnexpectedError
$ex ) {
1447 'No atomic transaction is open (got ' . __METHOD__
. ').',
1454 * @dataProvider provideAtomicSectionMethodsForErrors
1455 * @covers \Wikimedia\Rdbms\Database::endAtomic
1456 * @covers \Wikimedia\Rdbms\Database::cancelAtomic
1458 public function testInvalidAtomicSectionEnded( $method ) {
1459 $this->database
->startAtomic( __METHOD__
. 'X' );
1461 $this->database
->$method( __METHOD__
);
1462 $this->fail( 'Expected exception not thrown' );
1463 } catch ( DBUnexpectedError
$ex ) {
1465 'Invalid atomic section ended (got ' . __METHOD__
. ').',
1472 * @covers \Wikimedia\Rdbms\Database::cancelAtomic
1474 public function testUncancellableAtomicSection() {
1475 $this->database
->startAtomic( __METHOD__
);
1477 $this->database
->cancelAtomic( __METHOD__
);
1478 $this->fail( 'Expected exception not thrown' );
1479 } catch ( DBUnexpectedError
$ex ) {
1481 'Uncancelable atomic section canceled (got ' . __METHOD__
. ').',
1488 * @expectedException \Wikimedia\Rdbms\DBTransactionStateError
1490 public function testTransactionErrorState1() {
1491 $wrapper = TestingAccessWrapper
::newFromObject( $this->database
);
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__
);
1500 * @covers \Wikimedia\Rdbms\Database::query
1502 public function testTransactionErrorState2() {
1503 $wrapper = TestingAccessWrapper
::newFromObject( $this->database
);
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' );
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()' );
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()' );
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() );
1545 * @covers \Wikimedia\Rdbms\Database::query
1547 public function testImplicitTransactionRollback() {
1548 $doError = function ( $wasKnown = true ) {
1549 $this->database
->forceNextQueryError( 666, 'Evilness' );
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
);
1558 $this->database
->setFlag( Database
::DBO_TRX
);
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
);
1566 $this->assertLastSql( 'BEGIN; DELETE FROM error WHERE 1; ROLLBACK; BEGIN; DELETE FROM x WHERE field = \'1\'; COMMIT' );
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 );
1573 $this->database
->delete( 'x', [ 'field' => 1 ], __METHOD__
);
1574 $this->fail( 'Expected exception not thrown' );
1575 } catch ( DBTransactionStateError
$e ) {
1577 $this->database
->rollback( __METHOD__
, Database
::FLUSHING_INTERNAL
);
1579 $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'1\'; DELETE FROM error WHERE 1; ROLLBACK' );
1583 * @covers \Wikimedia\Rdbms\Database::close
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 );
1591 $this->database
->delete( 'x', [ 'field' => 3 ], __METHOD__
);
1592 $this->database
->close();
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() );
1600 * @covers \Wikimedia\Rdbms\Database::close
1602 public function testPrematureClose2() {
1604 $fname = __METHOD__
;
1605 $this->database
->startAtomic( __METHOD__
);
1606 $this->database
->onTransactionIdle( function () use ( $fname ) {
1607 $this->database
->query( 'SELECT 1', $fname );
1609 $this->database
->delete( 'x', [ 'field' => 3 ], __METHOD__
);
1610 $this->database
->close();
1611 $this->fail( 'Expected exception not thrown' );
1612 } catch ( DBUnexpectedError
$ex ) {
1614 'Wikimedia\Rdbms\Database::close: atomic sections ' .
1615 'DatabaseSQLTest::testPrematureClose2 are still open.',
1620 $this->assertFalse( $this->database
->isOpen() );
1621 $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; ROLLBACK' );
1622 $this->assertEquals( 0, $this->database
->trxLevel() );