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