MediaWiki  REL1_24
DatabaseSqliteTest.php
Go to the documentation of this file.
00001 <?php
00002 
00003 class MockDatabaseSqlite extends DatabaseSqliteStandalone {
00004     private $lastQuery;
00005 
00006     function __construct() {
00007         parent::__construct( ':memory:' );
00008     }
00009 
00010     function query( $sql, $fname = '', $tempIgnore = false ) {
00011         $this->lastQuery = $sql;
00012 
00013         return true;
00014     }
00015 
00019     public function replaceVars( $s ) {
00020         return parent::replaceVars( $s );
00021     }
00022 }
00023 
00029 class DatabaseSqliteTest extends MediaWikiTestCase {
00031     protected $db;
00032 
00033     protected function setUp() {
00034         parent::setUp();
00035 
00036         if ( !Sqlite::isPresent() ) {
00037             $this->markTestSkipped( 'No SQLite support detected' );
00038         }
00039         $this->db = new MockDatabaseSqlite();
00040         if ( version_compare( $this->db->getServerVersion(), '3.6.0', '<' ) ) {
00041             $this->markTestSkipped( "SQLite at least 3.6 required, {$this->db->getServerVersion()} found" );
00042         }
00043     }
00044 
00045     private function replaceVars( $sql ) {
00046         // normalize spacing to hide implementation details
00047         return preg_replace( '/\s+/', ' ', $this->db->replaceVars( $sql ) );
00048     }
00049 
00050     private function assertResultIs( $expected, $res ) {
00051         $this->assertNotNull( $res );
00052         $i = 0;
00053         foreach ( $res as $row ) {
00054             foreach ( $expected[$i] as $key => $value ) {
00055                 $this->assertTrue( isset( $row->$key ) );
00056                 $this->assertEquals( $value, $row->$key );
00057             }
00058             $i++;
00059         }
00060         $this->assertEquals( count( $expected ), $i, 'Unexpected number of rows' );
00061     }
00062 
00063     public static function provideAddQuotes() {
00064         return array(
00065             array( // #0: empty
00066                 '', "''"
00067             ),
00068             array( // #1: simple
00069                 'foo bar', "'foo bar'"
00070             ),
00071             array( // #2: including quote
00072                 'foo\'bar', "'foo''bar'"
00073             ),
00074             // #3: including \0 (must be represented as hex, per https://bugs.php.net/bug.php?id=63419)
00075             array(
00076                 "x\0y",
00077                 "x'780079'",
00078             ),
00079             array( // #4: blob object (must be represented as hex)
00080                 new Blob( "hello" ),
00081                 "x'68656c6c6f'",
00082             ),
00083         );
00084     }
00085 
00090     public function testAddQuotes( $value, $expected ) {
00091         // check quoting
00092         $db = new DatabaseSqliteStandalone( ':memory:' );
00093         $this->assertEquals( $expected, $db->addQuotes( $value ), 'string not quoted as expected' );
00094 
00095         // ok, quoting works as expected, now try a round trip.
00096         $re = $db->query( 'select ' . $db->addQuotes( $value ) );
00097 
00098         $this->assertTrue( $re !== false, 'query failed' );
00099 
00100         if ( $row = $re->fetchRow() ) {
00101             if ( $value instanceof Blob ) {
00102                 $value = $value->fetch();
00103             }
00104 
00105             $this->assertEquals( $value, $row[0], 'string mangled by the database' );
00106         } else {
00107             $this->fail( 'query returned no result' );
00108         }
00109     }
00110 
00114     public function testReplaceVars() {
00115         $this->assertEquals( 'foo', $this->replaceVars( 'foo' ), "Don't break anything accidentally" );
00116 
00117         $this->assertEquals(
00118             "CREATE TABLE /**/foo (foo_key INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
00119                 . "foo_bar TEXT, foo_name TEXT NOT NULL DEFAULT '', foo_int INTEGER, foo_int2 INTEGER );",
00120             $this->replaceVars(
00121                 "CREATE TABLE /**/foo (foo_key int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, "
00122                 . "foo_bar char(13), foo_name varchar(255) binary NOT NULL DEFAULT '', "
00123                 . "foo_int tinyint ( 8 ), foo_int2 int(16) ) ENGINE=MyISAM;"
00124             )
00125         );
00126 
00127         $this->assertEquals(
00128             "CREATE TABLE foo ( foo1 REAL, foo2 REAL, foo3 REAL );",
00129             $this->replaceVars(
00130                 "CREATE TABLE foo ( foo1 FLOAT, foo2 DOUBLE( 1,10), foo3 DOUBLE PRECISION );"
00131             )
00132         );
00133 
00134         $this->assertEquals( "CREATE TABLE foo ( foo_binary1 BLOB, foo_binary2 BLOB );",
00135             $this->replaceVars( "CREATE TABLE foo ( foo_binary1 binary(16), foo_binary2 varbinary(32) );" )
00136         );
00137 
00138         $this->assertEquals( "CREATE TABLE text ( text_foo TEXT );",
00139             $this->replaceVars( "CREATE TABLE text ( text_foo tinytext );" ),
00140             'Table name changed'
00141         );
00142 
00143         $this->assertEquals( "CREATE TABLE foo ( foobar INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL );",
00144             $this->replaceVars( "CREATE TABLE foo ( foobar INT PRIMARY KEY NOT NULL AUTO_INCREMENT );" )
00145         );
00146         $this->assertEquals( "CREATE TABLE foo ( foobar INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL );",
00147             $this->replaceVars( "CREATE TABLE foo ( foobar INT PRIMARY KEY AUTO_INCREMENT NOT NULL );" )
00148         );
00149 
00150         $this->assertEquals( "CREATE TABLE enums( enum1 TEXT, myenum TEXT)",
00151             $this->replaceVars( "CREATE TABLE enums( enum1 ENUM('A', 'B'), myenum ENUM ('X', 'Y'))" )
00152         );
00153 
00154         $this->assertEquals( "ALTER TABLE foo ADD COLUMN foo_bar INTEGER DEFAULT 42",
00155             $this->replaceVars( "ALTER TABLE foo\nADD COLUMN foo_bar int(10) unsigned DEFAULT 42" )
00156         );
00157 
00158         $this->assertEquals( "DROP INDEX foo",
00159             $this->replaceVars( "DROP INDEX /*i*/foo ON /*_*/bar" )
00160         );
00161 
00162         $this->assertEquals( "DROP INDEX foo -- dropping index",
00163             $this->replaceVars( "DROP INDEX /*i*/foo ON /*_*/bar -- dropping index" )
00164         );
00165         $this->assertEquals( "INSERT OR IGNORE INTO foo VALUES ('bar')",
00166             $this->replaceVars( "INSERT OR IGNORE INTO foo VALUES ('bar')" )
00167         );
00168     }
00169 
00173     public function testTableName() {
00174         // @todo Moar!
00175         $db = new DatabaseSqliteStandalone( ':memory:' );
00176         $this->assertEquals( 'foo', $db->tableName( 'foo' ) );
00177         $this->assertEquals( 'sqlite_master', $db->tableName( 'sqlite_master' ) );
00178         $db->tablePrefix( 'foo' );
00179         $this->assertEquals( 'sqlite_master', $db->tableName( 'sqlite_master' ) );
00180         $this->assertEquals( 'foobar', $db->tableName( 'bar' ) );
00181     }
00182 
00186     public function testDuplicateTableStructure() {
00187         $db = new DatabaseSqliteStandalone( ':memory:' );
00188         $db->query( 'CREATE TABLE foo(foo, barfoo)' );
00189 
00190         $db->duplicateTableStructure( 'foo', 'bar' );
00191         $this->assertEquals( 'CREATE TABLE "bar"(foo, barfoo)',
00192             $db->selectField( 'sqlite_master', 'sql', array( 'name' => 'bar' ) ),
00193             'Normal table duplication'
00194         );
00195 
00196         $db->duplicateTableStructure( 'foo', 'baz', true );
00197         $this->assertEquals( 'CREATE TABLE "baz"(foo, barfoo)',
00198             $db->selectField( 'sqlite_temp_master', 'sql', array( 'name' => 'baz' ) ),
00199             'Creation of temporary duplicate'
00200         );
00201         $this->assertEquals( 0,
00202             $db->selectField( 'sqlite_master', 'COUNT(*)', array( 'name' => 'baz' ) ),
00203             'Create a temporary duplicate only'
00204         );
00205     }
00206 
00210     public function testDuplicateTableStructureVirtual() {
00211         $db = new DatabaseSqliteStandalone( ':memory:' );
00212         if ( $db->getFulltextSearchModule() != 'FTS3' ) {
00213             $this->markTestSkipped( 'FTS3 not supported, cannot create virtual tables' );
00214         }
00215         $db->query( 'CREATE VIRTUAL TABLE "foo" USING FTS3(foobar)' );
00216 
00217         $db->duplicateTableStructure( 'foo', 'bar' );
00218         $this->assertEquals( 'CREATE VIRTUAL TABLE "bar" USING FTS3(foobar)',
00219             $db->selectField( 'sqlite_master', 'sql', array( 'name' => 'bar' ) ),
00220             'Duplication of virtual tables'
00221         );
00222 
00223         $db->duplicateTableStructure( 'foo', 'baz', true );
00224         $this->assertEquals( 'CREATE VIRTUAL TABLE "baz" USING FTS3(foobar)',
00225             $db->selectField( 'sqlite_master', 'sql', array( 'name' => 'baz' ) ),
00226             "Can't create temporary virtual tables, should fall back to non-temporary duplication"
00227         );
00228     }
00229 
00233     public function testDeleteJoin() {
00234         $db = new DatabaseSqliteStandalone( ':memory:' );
00235         $db->query( 'CREATE TABLE a (a_1)', __METHOD__ );
00236         $db->query( 'CREATE TABLE b (b_1, b_2)', __METHOD__ );
00237         $db->insert( 'a', array(
00238                 array( 'a_1' => 1 ),
00239                 array( 'a_1' => 2 ),
00240                 array( 'a_1' => 3 ),
00241             ),
00242             __METHOD__
00243         );
00244         $db->insert( 'b', array(
00245                 array( 'b_1' => 2, 'b_2' => 'a' ),
00246                 array( 'b_1' => 3, 'b_2' => 'b' ),
00247             ),
00248             __METHOD__
00249         );
00250         $db->deleteJoin( 'a', 'b', 'a_1', 'b_1', array( 'b_2' => 'a' ), __METHOD__ );
00251         $res = $db->query( "SELECT * FROM a", __METHOD__ );
00252         $this->assertResultIs( array(
00253                 array( 'a_1' => 1 ),
00254                 array( 'a_1' => 3 ),
00255             ),
00256             $res
00257         );
00258     }
00259 
00260     public function testEntireSchema() {
00261         global $IP;
00262 
00263         $result = Sqlite::checkSqlSyntax( "$IP/maintenance/tables.sql" );
00264         if ( $result !== true ) {
00265             $this->fail( $result );
00266         }
00267         $this->assertTrue( true ); // avoid test being marked as incomplete due to lack of assertions
00268     }
00269 
00274     public function testUpgrades() {
00275         global $IP, $wgVersion, $wgProfileToDatabase;
00276 
00277         // Versions tested
00278         $versions = array(
00279             //'1.13', disabled for now, was totally screwed up
00280             // SQLite wasn't included in 1.14
00281             '1.15',
00282             '1.16',
00283             '1.17',
00284             '1.18',
00285         );
00286 
00287         // Mismatches for these columns we can safely ignore
00288         $ignoredColumns = array(
00289             'user_newtalk.user_last_timestamp', // r84185
00290         );
00291 
00292         $currentDB = new DatabaseSqliteStandalone( ':memory:' );
00293         $currentDB->sourceFile( "$IP/maintenance/tables.sql" );
00294         if ( $wgProfileToDatabase ) {
00295             $currentDB->sourceFile( "$IP/maintenance/sqlite/archives/patch-profiling.sql" );
00296         }
00297         $currentTables = $this->getTables( $currentDB );
00298         sort( $currentTables );
00299 
00300         foreach ( $versions as $version ) {
00301             $versions = "upgrading from $version to $wgVersion";
00302             $db = $this->prepareDB( $version );
00303             $tables = $this->getTables( $db );
00304             $this->assertEquals( $currentTables, $tables, "Different tables $versions" );
00305             foreach ( $tables as $table ) {
00306                 $currentCols = $this->getColumns( $currentDB, $table );
00307                 $cols = $this->getColumns( $db, $table );
00308                 $this->assertEquals(
00309                     array_keys( $currentCols ),
00310                     array_keys( $cols ),
00311                     "Mismatching columns for table \"$table\" $versions"
00312                 );
00313                 foreach ( $currentCols as $name => $column ) {
00314                     $fullName = "$table.$name";
00315                     $this->assertEquals(
00316                         (bool)$column->pk,
00317                         (bool)$cols[$name]->pk,
00318                         "PRIMARY KEY status does not match for column $fullName $versions"
00319                     );
00320                     if ( !in_array( $fullName, $ignoredColumns ) ) {
00321                         $this->assertEquals(
00322                             (bool)$column->notnull,
00323                             (bool)$cols[$name]->notnull,
00324                             "NOT NULL status does not match for column $fullName $versions"
00325                         );
00326                         $this->assertEquals(
00327                             $column->dflt_value,
00328                             $cols[$name]->dflt_value,
00329                             "Default values does not match for column $fullName $versions"
00330                         );
00331                     }
00332                 }
00333                 $currentIndexes = $this->getIndexes( $currentDB, $table );
00334                 $indexes = $this->getIndexes( $db, $table );
00335                 $this->assertEquals(
00336                     array_keys( $currentIndexes ),
00337                     array_keys( $indexes ),
00338                     "mismatching indexes for table \"$table\" $versions"
00339                 );
00340             }
00341             $db->close();
00342         }
00343     }
00344 
00348     public function testInsertIdType() {
00349         $db = new DatabaseSqliteStandalone( ':memory:' );
00350 
00351         $databaseCreation = $db->query( 'CREATE TABLE a ( a_1 )', __METHOD__ );
00352         $this->assertInstanceOf( 'ResultWrapper', $databaseCreation, "Database creation" );
00353 
00354         $insertion = $db->insert( 'a', array( 'a_1' => 10 ), __METHOD__ );
00355         $this->assertTrue( $insertion, "Insertion worked" );
00356 
00357         $this->assertInternalType( 'integer', $db->insertId(), "Actual typecheck" );
00358         $this->assertTrue( $db->close(), "closing database" );
00359     }
00360 
00361     private function prepareDB( $version ) {
00362         static $maint = null;
00363         if ( $maint === null ) {
00364             $maint = new FakeMaintenance();
00365             $maint->loadParamsAndArgs( null, array( 'quiet' => 1 ) );
00366         }
00367 
00368         global $IP;
00369         $db = new DatabaseSqliteStandalone( ':memory:' );
00370         $db->sourceFile( "$IP/tests/phpunit/data/db/sqlite/tables-$version.sql" );
00371         $updater = DatabaseUpdater::newForDB( $db, false, $maint );
00372         $updater->doUpdates( array( 'core' ) );
00373 
00374         return $db;
00375     }
00376 
00377     private function getTables( $db ) {
00378         $list = array_flip( $db->listTables() );
00379         $excluded = array(
00380             'external_user', // removed from core in 1.22
00381             'math', // moved out of core in 1.18
00382             'trackbacks', // removed from core in 1.19
00383             'searchindex',
00384             'searchindex_content',
00385             'searchindex_segments',
00386             'searchindex_segdir',
00387             // FTS4 ready!!1
00388             'searchindex_docsize',
00389             'searchindex_stat',
00390         );
00391         foreach ( $excluded as $t ) {
00392             unset( $list[$t] );
00393         }
00394         $list = array_flip( $list );
00395         sort( $list );
00396 
00397         return $list;
00398     }
00399 
00400     private function getColumns( $db, $table ) {
00401         $cols = array();
00402         $res = $db->query( "PRAGMA table_info($table)" );
00403         $this->assertNotNull( $res );
00404         foreach ( $res as $col ) {
00405             $cols[$col->name] = $col;
00406         }
00407         ksort( $cols );
00408 
00409         return $cols;
00410     }
00411 
00412     private function getIndexes( $db, $table ) {
00413         $indexes = array();
00414         $res = $db->query( "PRAGMA index_list($table)" );
00415         $this->assertNotNull( $res );
00416         foreach ( $res as $index ) {
00417             $res2 = $db->query( "PRAGMA index_info({$index->name})" );
00418             $this->assertNotNull( $res2 );
00419             $index->columns = array();
00420             foreach ( $res2 as $col ) {
00421                 $index->columns[] = $col;
00422             }
00423             $indexes[$index->name] = $index;
00424         }
00425         ksort( $indexes );
00426 
00427         return $indexes;
00428     }
00429 
00430     public function testCaseInsensitiveLike() {
00431         // TODO: Test this for all databases
00432         $db = new DatabaseSqliteStandalone( ':memory:' );
00433         $res = $db->query( 'SELECT "a" LIKE "A" AS a' );
00434         $row = $res->fetchRow();
00435         $this->assertFalse( (bool)$row['a'] );
00436     }
00437 
00441     public function testNumFields() {
00442         $db = new DatabaseSqliteStandalone( ':memory:' );
00443 
00444         $databaseCreation = $db->query( 'CREATE TABLE a ( a_1 )', __METHOD__ );
00445         $this->assertInstanceOf( 'ResultWrapper', $databaseCreation, "Failed to create table a" );
00446         $res = $db->select( 'a', '*' );
00447         $this->assertEquals( 0, $db->numFields( $res ), "expects to get 0 fields for an empty table" );
00448         $insertion = $db->insert( 'a', array( 'a_1' => 10 ), __METHOD__ );
00449         $this->assertTrue( $insertion, "Insertion failed" );
00450         $res = $db->select( 'a', '*' );
00451         $this->assertEquals( 1, $db->numFields( $res ), "wrong number of fields" );
00452 
00453         $this->assertTrue( $db->close(), "closing database" );
00454     }
00455 }