]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - tests/phpunit/includes/db/DatabaseSqliteTest.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / tests / phpunit / includes / db / DatabaseSqliteTest.php
1 <?php
2
3 use Wikimedia\Rdbms\Blob;
4 use Wikimedia\Rdbms\Database;
5 use Wikimedia\Rdbms\DatabaseSqlite;
6
7 class DatabaseSqliteMock extends DatabaseSqlite {
8         private $lastQuery;
9
10         public static function newInstance( array $p = [] ) {
11                 $p['dbFilePath'] = ':memory:';
12                 $p['schema'] = false;
13
14                 return Database::factory( 'SqliteMock', $p );
15         }
16
17         function query( $sql, $fname = '', $tempIgnore = false ) {
18                 $this->lastQuery = $sql;
19
20                 return true;
21         }
22
23         /**
24          * Override parent visibility to public
25          */
26         public function replaceVars( $s ) {
27                 return parent::replaceVars( $s );
28         }
29 }
30
31 /**
32  * @group sqlite
33  * @group Database
34  * @group medium
35  */
36 class DatabaseSqliteTest extends MediaWikiTestCase {
37         /** @var DatabaseSqliteMock */
38         protected $db;
39
40         protected function setUp() {
41                 parent::setUp();
42
43                 if ( !Sqlite::isPresent() ) {
44                         $this->markTestSkipped( 'No SQLite support detected' );
45                 }
46                 $this->db = DatabaseSqliteMock::newInstance();
47                 if ( version_compare( $this->db->getServerVersion(), '3.6.0', '<' ) ) {
48                         $this->markTestSkipped( "SQLite at least 3.6 required, {$this->db->getServerVersion()} found" );
49                 }
50         }
51
52         private function replaceVars( $sql ) {
53                 // normalize spacing to hide implementation details
54                 return preg_replace( '/\s+/', ' ', $this->db->replaceVars( $sql ) );
55         }
56
57         private function assertResultIs( $expected, $res ) {
58                 $this->assertNotNull( $res );
59                 $i = 0;
60                 foreach ( $res as $row ) {
61                         foreach ( $expected[$i] as $key => $value ) {
62                                 $this->assertTrue( isset( $row->$key ) );
63                                 $this->assertEquals( $value, $row->$key );
64                         }
65                         $i++;
66                 }
67                 $this->assertEquals( count( $expected ), $i, 'Unexpected number of rows' );
68         }
69
70         public static function provideAddQuotes() {
71                 return [
72                         [ // #0: empty
73                                 '', "''"
74                         ],
75                         [ // #1: simple
76                                 'foo bar', "'foo bar'"
77                         ],
78                         [ // #2: including quote
79                                 'foo\'bar', "'foo''bar'"
80                         ],
81                         // #3: including \0 (must be represented as hex, per https://bugs.php.net/bug.php?id=63419)
82                         [
83                                 "x\0y",
84                                 "x'780079'",
85                         ],
86                         [ // #4: blob object (must be represented as hex)
87                                 new Blob( "hello" ),
88                                 "x'68656c6c6f'",
89                         ],
90                         [ // #5: null
91                                 null,
92                                 "''",
93                         ],
94                 ];
95         }
96
97         /**
98          * @dataProvider provideAddQuotes()
99          * @covers DatabaseSqlite::addQuotes
100          */
101         public function testAddQuotes( $value, $expected ) {
102                 // check quoting
103                 $db = DatabaseSqlite::newStandaloneInstance( ':memory:' );
104                 $this->assertEquals( $expected, $db->addQuotes( $value ), 'string not quoted as expected' );
105
106                 // ok, quoting works as expected, now try a round trip.
107                 $re = $db->query( 'select ' . $db->addQuotes( $value ) );
108
109                 $this->assertTrue( $re !== false, 'query failed' );
110
111                 $row = $re->fetchRow();
112                 if ( $row ) {
113                         if ( $value instanceof Blob ) {
114                                 $value = $value->fetch();
115                         }
116
117                         $this->assertEquals( $value, $row[0], 'string mangled by the database' );
118                 } else {
119                         $this->fail( 'query returned no result' );
120                 }
121         }
122
123         /**
124          * @covers DatabaseSqlite::replaceVars
125          */
126         public function testReplaceVars() {
127                 $this->assertEquals( 'foo', $this->replaceVars( 'foo' ), "Don't break anything accidentally" );
128
129                 $this->assertEquals(
130                         "CREATE TABLE /**/foo (foo_key INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
131                                 . "foo_bar TEXT, foo_name TEXT NOT NULL DEFAULT '', foo_int INTEGER, foo_int2 INTEGER );",
132                         $this->replaceVars(
133                                 "CREATE TABLE /**/foo (foo_key int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, "
134                                 . "foo_bar char(13), foo_name varchar(255) binary NOT NULL DEFAULT '', "
135                                 . "foo_int tinyint ( 8 ), foo_int2 int(16) ) ENGINE=MyISAM;"
136                         )
137                 );
138
139                 $this->assertEquals(
140                         "CREATE TABLE foo ( foo1 REAL, foo2 REAL, foo3 REAL );",
141                         $this->replaceVars(
142                                 "CREATE TABLE foo ( foo1 FLOAT, foo2 DOUBLE( 1,10), foo3 DOUBLE PRECISION );"
143                         )
144                 );
145
146                 $this->assertEquals( "CREATE TABLE foo ( foo_binary1 BLOB, foo_binary2 BLOB );",
147                         $this->replaceVars( "CREATE TABLE foo ( foo_binary1 binary(16), foo_binary2 varbinary(32) );" )
148                 );
149
150                 $this->assertEquals( "CREATE TABLE text ( text_foo TEXT );",
151                         $this->replaceVars( "CREATE TABLE text ( text_foo tinytext );" ),
152                         'Table name changed'
153                 );
154
155                 $this->assertEquals( "CREATE TABLE foo ( foobar INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL );",
156                         $this->replaceVars( "CREATE TABLE foo ( foobar INT PRIMARY KEY NOT NULL AUTO_INCREMENT );" )
157                 );
158                 $this->assertEquals( "CREATE TABLE foo ( foobar INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL );",
159                         $this->replaceVars( "CREATE TABLE foo ( foobar INT PRIMARY KEY AUTO_INCREMENT NOT NULL );" )
160                 );
161
162                 $this->assertEquals( "CREATE TABLE enums( enum1 TEXT, myenum TEXT)",
163                         $this->replaceVars( "CREATE TABLE enums( enum1 ENUM('A', 'B'), myenum ENUM ('X', 'Y'))" )
164                 );
165
166                 $this->assertEquals( "ALTER TABLE foo ADD COLUMN foo_bar INTEGER DEFAULT 42",
167                         $this->replaceVars( "ALTER TABLE foo\nADD COLUMN foo_bar int(10) unsigned DEFAULT 42" )
168                 );
169
170                 $this->assertEquals( "DROP INDEX foo",
171                         $this->replaceVars( "DROP INDEX /*i*/foo ON /*_*/bar" )
172                 );
173
174                 $this->assertEquals( "DROP INDEX foo -- dropping index",
175                         $this->replaceVars( "DROP INDEX /*i*/foo ON /*_*/bar -- dropping index" )
176                 );
177                 $this->assertEquals( "INSERT OR IGNORE INTO foo VALUES ('bar')",
178                         $this->replaceVars( "INSERT OR IGNORE INTO foo VALUES ('bar')" )
179                 );
180         }
181
182         /**
183          * @covers DatabaseSqlite::tableName
184          */
185         public function testTableName() {
186                 // @todo Moar!
187                 $db = DatabaseSqlite::newStandaloneInstance( ':memory:' );
188                 $this->assertEquals( 'foo', $db->tableName( 'foo' ) );
189                 $this->assertEquals( 'sqlite_master', $db->tableName( 'sqlite_master' ) );
190                 $db->tablePrefix( 'foo' );
191                 $this->assertEquals( 'sqlite_master', $db->tableName( 'sqlite_master' ) );
192                 $this->assertEquals( 'foobar', $db->tableName( 'bar' ) );
193         }
194
195         /**
196          * @covers DatabaseSqlite::duplicateTableStructure
197          */
198         public function testDuplicateTableStructure() {
199                 $db = DatabaseSqlite::newStandaloneInstance( ':memory:' );
200                 $db->query( 'CREATE TABLE foo(foo, barfoo)' );
201                 $db->query( 'CREATE INDEX index1 ON foo(foo)' );
202                 $db->query( 'CREATE UNIQUE INDEX index2 ON foo(barfoo)' );
203
204                 $db->duplicateTableStructure( 'foo', 'bar' );
205                 $this->assertEquals( 'CREATE TABLE "bar"(foo, barfoo)',
206                         $db->selectField( 'sqlite_master', 'sql', [ 'name' => 'bar' ] ),
207                         'Normal table duplication'
208                 );
209                 $indexList = $db->query( 'PRAGMA INDEX_LIST("bar")' );
210                 $index = $indexList->next();
211                 $this->assertEquals( 'bar_index1', $index->name );
212                 $this->assertEquals( '0', $index->unique );
213                 $index = $indexList->next();
214                 $this->assertEquals( 'bar_index2', $index->name );
215                 $this->assertEquals( '1', $index->unique );
216
217                 $db->duplicateTableStructure( 'foo', 'baz', true );
218                 $this->assertEquals( 'CREATE TABLE "baz"(foo, barfoo)',
219                         $db->selectField( 'sqlite_temp_master', 'sql', [ 'name' => 'baz' ] ),
220                         'Creation of temporary duplicate'
221                 );
222                 $indexList = $db->query( 'PRAGMA INDEX_LIST("baz")' );
223                 $index = $indexList->next();
224                 $this->assertEquals( 'baz_index1', $index->name );
225                 $this->assertEquals( '0', $index->unique );
226                 $index = $indexList->next();
227                 $this->assertEquals( 'baz_index2', $index->name );
228                 $this->assertEquals( '1', $index->unique );
229                 $this->assertEquals( 0,
230                         $db->selectField( 'sqlite_master', 'COUNT(*)', [ 'name' => 'baz' ] ),
231                         'Create a temporary duplicate only'
232                 );
233         }
234
235         /**
236          * @covers DatabaseSqlite::duplicateTableStructure
237          */
238         public function testDuplicateTableStructureVirtual() {
239                 $db = DatabaseSqlite::newStandaloneInstance( ':memory:' );
240                 if ( $db->getFulltextSearchModule() != 'FTS3' ) {
241                         $this->markTestSkipped( 'FTS3 not supported, cannot create virtual tables' );
242                 }
243                 $db->query( 'CREATE VIRTUAL TABLE "foo" USING FTS3(foobar)' );
244
245                 $db->duplicateTableStructure( 'foo', 'bar' );
246                 $this->assertEquals( 'CREATE VIRTUAL TABLE "bar" USING FTS3(foobar)',
247                         $db->selectField( 'sqlite_master', 'sql', [ 'name' => 'bar' ] ),
248                         'Duplication of virtual tables'
249                 );
250
251                 $db->duplicateTableStructure( 'foo', 'baz', true );
252                 $this->assertEquals( 'CREATE VIRTUAL TABLE "baz" USING FTS3(foobar)',
253                         $db->selectField( 'sqlite_master', 'sql', [ 'name' => 'baz' ] ),
254                         "Can't create temporary virtual tables, should fall back to non-temporary duplication"
255                 );
256         }
257
258         /**
259          * @covers DatabaseSqlite::deleteJoin
260          */
261         public function testDeleteJoin() {
262                 $db = DatabaseSqlite::newStandaloneInstance( ':memory:' );
263                 $db->query( 'CREATE TABLE a (a_1)', __METHOD__ );
264                 $db->query( 'CREATE TABLE b (b_1, b_2)', __METHOD__ );
265                 $db->insert( 'a', [
266                                 [ 'a_1' => 1 ],
267                                 [ 'a_1' => 2 ],
268                                 [ 'a_1' => 3 ],
269                         ],
270                         __METHOD__
271                 );
272                 $db->insert( 'b', [
273                                 [ 'b_1' => 2, 'b_2' => 'a' ],
274                                 [ 'b_1' => 3, 'b_2' => 'b' ],
275                         ],
276                         __METHOD__
277                 );
278                 $db->deleteJoin( 'a', 'b', 'a_1', 'b_1', [ 'b_2' => 'a' ], __METHOD__ );
279                 $res = $db->query( "SELECT * FROM a", __METHOD__ );
280                 $this->assertResultIs( [
281                                 [ 'a_1' => 1 ],
282                                 [ 'a_1' => 3 ],
283                         ],
284                         $res
285                 );
286         }
287
288         public function testEntireSchema() {
289                 global $IP;
290
291                 $result = Sqlite::checkSqlSyntax( "$IP/maintenance/tables.sql" );
292                 if ( $result !== true ) {
293                         $this->fail( $result );
294                 }
295                 $this->assertTrue( true ); // avoid test being marked as incomplete due to lack of assertions
296         }
297
298         /**
299          * Runs upgrades of older databases and compares results with current schema
300          * @todo Currently only checks list of tables
301          */
302         public function testUpgrades() {
303                 global $IP, $wgVersion, $wgProfiler;
304
305                 // Versions tested
306                 $versions = [
307                         // '1.13', disabled for now, was totally screwed up
308                         // SQLite wasn't included in 1.14
309                         '1.15',
310                         '1.16',
311                         '1.17',
312                         '1.18',
313                 ];
314
315                 // Mismatches for these columns we can safely ignore
316                 $ignoredColumns = [
317                         'user_newtalk.user_last_timestamp', // r84185
318                 ];
319
320                 $currentDB = DatabaseSqlite::newStandaloneInstance( ':memory:' );
321                 $currentDB->sourceFile( "$IP/maintenance/tables.sql" );
322
323                 $profileToDb = false;
324                 if ( isset( $wgProfiler['output'] ) ) {
325                         $out = $wgProfiler['output'];
326                         if ( $out === 'db' ) {
327                                 $profileToDb = true;
328                         } elseif ( is_array( $out ) && in_array( 'db', $out ) ) {
329                                 $profileToDb = true;
330                         }
331                 }
332
333                 if ( $profileToDb ) {
334                         $currentDB->sourceFile( "$IP/maintenance/sqlite/archives/patch-profiling.sql" );
335                 }
336                 $currentTables = $this->getTables( $currentDB );
337                 sort( $currentTables );
338
339                 foreach ( $versions as $version ) {
340                         $versions = "upgrading from $version to $wgVersion";
341                         $db = $this->prepareTestDB( $version );
342                         $tables = $this->getTables( $db );
343                         $this->assertEquals( $currentTables, $tables, "Different tables $versions" );
344                         foreach ( $tables as $table ) {
345                                 $currentCols = $this->getColumns( $currentDB, $table );
346                                 $cols = $this->getColumns( $db, $table );
347                                 $this->assertEquals(
348                                         array_keys( $currentCols ),
349                                         array_keys( $cols ),
350                                         "Mismatching columns for table \"$table\" $versions"
351                                 );
352                                 foreach ( $currentCols as $name => $column ) {
353                                         $fullName = "$table.$name";
354                                         $this->assertEquals(
355                                                 (bool)$column->pk,
356                                                 (bool)$cols[$name]->pk,
357                                                 "PRIMARY KEY status does not match for column $fullName $versions"
358                                         );
359                                         if ( !in_array( $fullName, $ignoredColumns ) ) {
360                                                 $this->assertEquals(
361                                                         (bool)$column->notnull,
362                                                         (bool)$cols[$name]->notnull,
363                                                         "NOT NULL status does not match for column $fullName $versions"
364                                                 );
365                                                 $this->assertEquals(
366                                                         $column->dflt_value,
367                                                         $cols[$name]->dflt_value,
368                                                         "Default values does not match for column $fullName $versions"
369                                                 );
370                                         }
371                                 }
372                                 $currentIndexes = $this->getIndexes( $currentDB, $table );
373                                 $indexes = $this->getIndexes( $db, $table );
374                                 $this->assertEquals(
375                                         array_keys( $currentIndexes ),
376                                         array_keys( $indexes ),
377                                         "mismatching indexes for table \"$table\" $versions"
378                                 );
379                         }
380                         $db->close();
381                 }
382         }
383
384         /**
385          * @covers DatabaseSqlite::insertId
386          */
387         public function testInsertIdType() {
388                 $db = DatabaseSqlite::newStandaloneInstance( ':memory:' );
389
390                 $databaseCreation = $db->query( 'CREATE TABLE a ( a_1 )', __METHOD__ );
391                 $this->assertInstanceOf( 'ResultWrapper', $databaseCreation, "Database creation" );
392
393                 $insertion = $db->insert( 'a', [ 'a_1' => 10 ], __METHOD__ );
394                 $this->assertTrue( $insertion, "Insertion worked" );
395
396                 $this->assertInternalType( 'integer', $db->insertId(), "Actual typecheck" );
397                 $this->assertTrue( $db->close(), "closing database" );
398         }
399
400         private function prepareTestDB( $version ) {
401                 static $maint = null;
402                 if ( $maint === null ) {
403                         $maint = new FakeMaintenance();
404                         $maint->loadParamsAndArgs( null, [ 'quiet' => 1 ] );
405                 }
406
407                 global $IP;
408                 $db = DatabaseSqlite::newStandaloneInstance( ':memory:' );
409                 $db->sourceFile( "$IP/tests/phpunit/data/db/sqlite/tables-$version.sql" );
410                 $updater = DatabaseUpdater::newForDB( $db, false, $maint );
411                 $updater->doUpdates( [ 'core' ] );
412
413                 return $db;
414         }
415
416         private function getTables( $db ) {
417                 $list = array_flip( $db->listTables() );
418                 $excluded = [
419                         'external_user', // removed from core in 1.22
420                         'math', // moved out of core in 1.18
421                         'trackbacks', // removed from core in 1.19
422                         'searchindex',
423                         'searchindex_content',
424                         'searchindex_segments',
425                         'searchindex_segdir',
426                         // FTS4 ready!!1
427                         'searchindex_docsize',
428                         'searchindex_stat',
429                 ];
430                 foreach ( $excluded as $t ) {
431                         unset( $list[$t] );
432                 }
433                 $list = array_flip( $list );
434                 sort( $list );
435
436                 return $list;
437         }
438
439         private function getColumns( $db, $table ) {
440                 $cols = [];
441                 $res = $db->query( "PRAGMA table_info($table)" );
442                 $this->assertNotNull( $res );
443                 foreach ( $res as $col ) {
444                         $cols[$col->name] = $col;
445                 }
446                 ksort( $cols );
447
448                 return $cols;
449         }
450
451         private function getIndexes( $db, $table ) {
452                 $indexes = [];
453                 $res = $db->query( "PRAGMA index_list($table)" );
454                 $this->assertNotNull( $res );
455                 foreach ( $res as $index ) {
456                         $res2 = $db->query( "PRAGMA index_info({$index->name})" );
457                         $this->assertNotNull( $res2 );
458                         $index->columns = [];
459                         foreach ( $res2 as $col ) {
460                                 $index->columns[] = $col;
461                         }
462                         $indexes[$index->name] = $index;
463                 }
464                 ksort( $indexes );
465
466                 return $indexes;
467         }
468
469         public function testCaseInsensitiveLike() {
470                 // TODO: Test this for all databases
471                 $db = DatabaseSqlite::newStandaloneInstance( ':memory:' );
472                 $res = $db->query( 'SELECT "a" LIKE "A" AS a' );
473                 $row = $res->fetchRow();
474                 $this->assertFalse( (bool)$row['a'] );
475         }
476
477         /**
478          * @covers DatabaseSqlite::numFields
479          */
480         public function testNumFields() {
481                 $db = DatabaseSqlite::newStandaloneInstance( ':memory:' );
482
483                 $databaseCreation = $db->query( 'CREATE TABLE a ( a_1 )', __METHOD__ );
484                 $this->assertInstanceOf( 'ResultWrapper', $databaseCreation, "Failed to create table a" );
485                 $res = $db->select( 'a', '*' );
486                 $this->assertEquals( 0, $db->numFields( $res ), "expects to get 0 fields for an empty table" );
487                 $insertion = $db->insert( 'a', [ 'a_1' => 10 ], __METHOD__ );
488                 $this->assertTrue( $insertion, "Insertion failed" );
489                 $res = $db->select( 'a', '*' );
490                 $this->assertEquals( 1, $db->numFields( $res ), "wrong number of fields" );
491
492                 $this->assertTrue( $db->close(), "closing database" );
493         }
494
495         public function testToString() {
496                 $db = DatabaseSqlite::newStandaloneInstance( ':memory:' );
497
498                 $toString = (string)$db;
499
500                 $this->assertContains( 'SQLite ', $toString );
501         }
502 }