DatabaseSqliteTest.php 15 KB

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