MediaWiki REL1_30
DatabaseSqliteTest.php
Go to the documentation of this file.
1<?php
2
6
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
26 public function replaceVars( $s ) {
27 return parent::replaceVars( $s );
28 }
29}
30
38 protected $db;
39
40 protected function setUp() {
41 parent::setUp();
42
43 if ( !Sqlite::isPresent() ) {
44 $this->markTestSkipped( 'No SQLite support detected' );
45 }
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
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
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
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
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
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
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
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
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
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}
$wgVersion
MediaWiki version number.
$wgProfiler
if(!defined( 'MEDIAWIKI')) $fname
This file is not a valid entry point, perform no further processing unless MEDIAWIKI is defined.
Definition Setup.php:36
replaceVars( $s)
Override parent visibility to public.
query( $sql, $fname='', $tempIgnore=false)
Run an SQL query and return the result.
static newInstance(array $p=[])
sqlite Database medium
testDuplicateTableStructureVirtual()
DatabaseSqlite::duplicateTableStructure.
testInsertIdType()
DatabaseSqlite::insertId.
assertResultIs( $expected, $res)
testAddQuotes( $value, $expected)
provideAddQuotes() DatabaseSqlite::addQuotes
testUpgrades()
Runs upgrades of older databases and compares results with current schema.
testReplaceVars()
DatabaseSqlite::replaceVars.
DatabaseSqliteMock $db
testTableName()
DatabaseSqlite::tableName.
testNumFields()
DatabaseSqlite::numFields.
testDuplicateTableStructure()
DatabaseSqlite::duplicateTableStructure.
testDeleteJoin()
DatabaseSqlite::deleteJoin.
static newForDB(Database $db, $shared=false, $maintenance=null)
Fake maintenance wrapper, mostly used for the web installer/updater.
static isPresent()
Checks whether PHP has SQLite support.
Definition sqlite.inc:38
static checkSqlSyntax( $files)
Checks given files for correctness of SQL syntax.
Definition sqlite.inc:50
tableName( $name, $format='quoted')
Use MySQL's naming (accounts for prefix etc) but remove surrounding backticks.
static getFulltextSearchModule()
Returns version of currently supported SQLite fulltext search module or false if none present.
listTables( $prefix=null, $fname=__METHOD__)
List all tables on the database.
duplicateTableStructure( $oldName, $newName, $temporary=false, $fname=__METHOD__)
insertId()
This must be called after nextSequenceVal.
insert( $table, $a, $fname=__METHOD__, $options=[])
Based on generic method (parent) with some prior SQLite-sepcific adjustments.
Relational database abstraction object.
Definition Database.php:45
tablePrefix( $prefix=null)
Get/set the table prefix.
Definition Database.php:480
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname=__METHOD__)
DELETE where the condition is a join.
lastQuery()
Return the last query that went through IDatabase::query()
Definition Database.php:542
sourceFile( $filename, callable $lineCallback=null, callable $resultCallback=null, $fname=false, callable $inputCallback=null)
Read and execute SQL commands from a file.
close()
Closes a database connection.
Definition Database.php:753
selectField( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a single field from a single result row.
$res
Definition database.txt:21
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist & $tables
Definition hooks.txt:1013
This code would result in ircNotify being run twice when an article is and once for brion Hooks can return three possible true was required This is the default since MediaWiki *some string
Definition hooks.txt:181
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition hooks.txt:862
$IP
Definition update.php:3