+++ /dev/null
-<?php
-
-use Wikimedia\Rdbms\LikeMatch;
-
-/**
- * Test the abstract database layer
- * This is a non DBMS depending test.
- */
-class DatabaseSQLTest extends MediaWikiTestCase {
- /** @var DatabaseTestHelper */
- private $database;
-
- protected function setUp() {
- parent::setUp();
- $this->database = new DatabaseTestHelper( __CLASS__, [ 'cliMode' => true ] );
- }
-
- protected function assertLastSql( $sqlText ) {
- $this->assertEquals(
- $sqlText,
- $this->database->getLastSqls()
- );
- }
-
- protected function assertLastSqlDb( $sqlText, $db ) {
- $this->assertEquals( $sqlText, $db->getLastSqls() );
- }
-
- /**
- * @dataProvider provideSelect
- * @covers Database::select
- */
- public function testSelect( $sql, $sqlText ) {
- $this->database->select(
- $sql['tables'],
- $sql['fields'],
- isset( $sql['conds'] ) ? $sql['conds'] : [],
- __METHOD__,
- isset( $sql['options'] ) ? $sql['options'] : [],
- isset( $sql['join_conds'] ) ? $sql['join_conds'] : []
- );
- $this->assertLastSql( $sqlText );
- }
-
- public static function provideSelect() {
- return [
- [
- [
- 'tables' => 'table',
- 'fields' => [ 'field', 'alias' => 'field2' ],
- 'conds' => [ 'alias' => 'text' ],
- ],
- "SELECT field,field2 AS alias " .
- "FROM table " .
- "WHERE alias = 'text'"
- ],
- [
- [
- 'tables' => 'table',
- 'fields' => [ 'field', 'alias' => 'field2' ],
- 'conds' => [ 'alias' => 'text' ],
- 'options' => [ 'LIMIT' => 1, 'ORDER BY' => 'field' ],
- ],
- "SELECT field,field2 AS alias " .
- "FROM table " .
- "WHERE alias = 'text' " .
- "ORDER BY field " .
- "LIMIT 1"
- ],
- [
- [
- 'tables' => [ 'table', 't2' => 'table2' ],
- 'fields' => [ 'tid', 'field', 'alias' => 'field2', 't2.id' ],
- 'conds' => [ 'alias' => 'text' ],
- 'options' => [ 'LIMIT' => 1, 'ORDER BY' => 'field' ],
- 'join_conds' => [ 't2' => [
- 'LEFT JOIN', 'tid = t2.id'
- ] ],
- ],
- "SELECT tid,field,field2 AS alias,t2.id " .
- "FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " .
- "WHERE alias = 'text' " .
- "ORDER BY field " .
- "LIMIT 1"
- ],
- [
- [
- 'tables' => [ 'table', 't2' => 'table2' ],
- 'fields' => [ 'tid', 'field', 'alias' => 'field2', 't2.id' ],
- 'conds' => [ 'alias' => 'text' ],
- 'options' => [ 'LIMIT' => 1, 'GROUP BY' => 'field', 'HAVING' => 'COUNT(*) > 1' ],
- 'join_conds' => [ 't2' => [
- 'LEFT JOIN', 'tid = t2.id'
- ] ],
- ],
- "SELECT tid,field,field2 AS alias,t2.id " .
- "FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " .
- "WHERE alias = 'text' " .
- "GROUP BY field HAVING COUNT(*) > 1 " .
- "LIMIT 1"
- ],
- [
- [
- 'tables' => [ 'table', 't2' => 'table2' ],
- 'fields' => [ 'tid', 'field', 'alias' => 'field2', 't2.id' ],
- 'conds' => [ 'alias' => 'text' ],
- 'options' => [
- 'LIMIT' => 1,
- 'GROUP BY' => [ 'field', 'field2' ],
- 'HAVING' => [ 'COUNT(*) > 1', 'field' => 1 ]
- ],
- 'join_conds' => [ 't2' => [
- 'LEFT JOIN', 'tid = t2.id'
- ] ],
- ],
- "SELECT tid,field,field2 AS alias,t2.id " .
- "FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " .
- "WHERE alias = 'text' " .
- "GROUP BY field,field2 HAVING (COUNT(*) > 1) AND field = '1' " .
- "LIMIT 1"
- ],
- [
- [
- 'tables' => [ 'table' ],
- 'fields' => [ 'alias' => 'field' ],
- 'conds' => [ 'alias' => [ 1, 2, 3, 4 ] ],
- ],
- "SELECT field AS alias " .
- "FROM table " .
- "WHERE alias IN ('1','2','3','4')"
- ],
- ];
- }
-
- /**
- * @dataProvider provideUpdate
- * @covers Database::update
- */
- public function testUpdate( $sql, $sqlText ) {
- $this->database->update(
- $sql['table'],
- $sql['values'],
- $sql['conds'],
- __METHOD__,
- isset( $sql['options'] ) ? $sql['options'] : []
- );
- $this->assertLastSql( $sqlText );
- }
-
- public static function provideUpdate() {
- return [
- [
- [
- 'table' => 'table',
- 'values' => [ 'field' => 'text', 'field2' => 'text2' ],
- 'conds' => [ 'alias' => 'text' ],
- ],
- "UPDATE table " .
- "SET field = 'text'" .
- ",field2 = 'text2' " .
- "WHERE alias = 'text'"
- ],
- [
- [
- 'table' => 'table',
- 'values' => [ 'field = other', 'field2' => 'text2' ],
- 'conds' => [ 'id' => '1' ],
- ],
- "UPDATE table " .
- "SET field = other" .
- ",field2 = 'text2' " .
- "WHERE id = '1'"
- ],
- [
- [
- 'table' => 'table',
- 'values' => [ 'field = other', 'field2' => 'text2' ],
- 'conds' => '*',
- ],
- "UPDATE table " .
- "SET field = other" .
- ",field2 = 'text2'"
- ],
- ];
- }
-
- /**
- * @dataProvider provideDelete
- * @covers Database::delete
- */
- public function testDelete( $sql, $sqlText ) {
- $this->database->delete(
- $sql['table'],
- $sql['conds'],
- __METHOD__
- );
- $this->assertLastSql( $sqlText );
- }
-
- public static function provideDelete() {
- return [
- [
- [
- 'table' => 'table',
- 'conds' => [ 'alias' => 'text' ],
- ],
- "DELETE FROM table " .
- "WHERE alias = 'text'"
- ],
- [
- [
- 'table' => 'table',
- 'conds' => '*',
- ],
- "DELETE FROM table"
- ],
- ];
- }
-
- /**
- * @dataProvider provideUpsert
- * @covers Database::upsert
- */
- public function testUpsert( $sql, $sqlText ) {
- $this->database->upsert(
- $sql['table'],
- $sql['rows'],
- $sql['uniqueIndexes'],
- $sql['set'],
- __METHOD__
- );
- $this->assertLastSql( $sqlText );
- }
-
- public static function provideUpsert() {
- return [
- [
- [
- 'table' => 'upsert_table',
- 'rows' => [ 'field' => 'text', 'field2' => 'text2' ],
- 'uniqueIndexes' => [ 'field' ],
- 'set' => [ 'field' => 'set' ],
- ],
- "BEGIN; " .
- "UPDATE upsert_table " .
- "SET field = 'set' " .
- "WHERE ((field = 'text')); " .
- "INSERT IGNORE INTO upsert_table " .
- "(field,field2) " .
- "VALUES ('text','text2'); " .
- "COMMIT"
- ],
- ];
- }
-
- /**
- * @dataProvider provideDeleteJoin
- * @covers Database::deleteJoin
- */
- public function testDeleteJoin( $sql, $sqlText ) {
- $this->database->deleteJoin(
- $sql['delTable'],
- $sql['joinTable'],
- $sql['delVar'],
- $sql['joinVar'],
- $sql['conds'],
- __METHOD__
- );
- $this->assertLastSql( $sqlText );
- }
-
- public static function provideDeleteJoin() {
- return [
- [
- [
- 'delTable' => 'table',
- 'joinTable' => 'table_join',
- 'delVar' => 'field',
- 'joinVar' => 'field_join',
- 'conds' => [ 'alias' => 'text' ],
- ],
- "DELETE FROM table " .
- "WHERE field IN (" .
- "SELECT field_join FROM table_join WHERE alias = 'text'" .
- ")"
- ],
- [
- [
- 'delTable' => 'table',
- 'joinTable' => 'table_join',
- 'delVar' => 'field',
- 'joinVar' => 'field_join',
- 'conds' => '*',
- ],
- "DELETE FROM table " .
- "WHERE field IN (" .
- "SELECT field_join FROM table_join " .
- ")"
- ],
- ];
- }
-
- /**
- * @dataProvider provideInsert
- * @covers Database::insert
- */
- public function testInsert( $sql, $sqlText ) {
- $this->database->insert(
- $sql['table'],
- $sql['rows'],
- __METHOD__,
- isset( $sql['options'] ) ? $sql['options'] : []
- );
- $this->assertLastSql( $sqlText );
- }
-
- public static function provideInsert() {
- return [
- [
- [
- 'table' => 'table',
- 'rows' => [ 'field' => 'text', 'field2' => 2 ],
- ],
- "INSERT INTO table " .
- "(field,field2) " .
- "VALUES ('text','2')"
- ],
- [
- [
- 'table' => 'table',
- 'rows' => [ 'field' => 'text', 'field2' => 2 ],
- 'options' => 'IGNORE',
- ],
- "INSERT IGNORE INTO table " .
- "(field,field2) " .
- "VALUES ('text','2')"
- ],
- [
- [
- 'table' => 'table',
- 'rows' => [
- [ 'field' => 'text', 'field2' => 2 ],
- [ 'field' => 'multi', 'field2' => 3 ],
- ],
- 'options' => 'IGNORE',
- ],
- "INSERT IGNORE INTO table " .
- "(field,field2) " .
- "VALUES " .
- "('text','2')," .
- "('multi','3')"
- ],
- ];
- }
-
- /**
- * @dataProvider provideInsertSelect
- * @covers Database::insertSelect
- */
- public function testInsertSelect( $sql, $sqlTextNative, $sqlSelect, $sqlInsert ) {
- $this->database->insertSelect(
- $sql['destTable'],
- $sql['srcTable'],
- $sql['varMap'],
- $sql['conds'],
- __METHOD__,
- isset( $sql['insertOptions'] ) ? $sql['insertOptions'] : [],
- isset( $sql['selectOptions'] ) ? $sql['selectOptions'] : [],
- isset( $sql['selectJoinConds'] ) ? $sql['selectJoinConds'] : []
- );
- $this->assertLastSql( $sqlTextNative );
-
- $dbWeb = new DatabaseTestHelper( __CLASS__, [ 'cliMode' => false ] );
- $dbWeb->forceNextResult( [
- array_flip( array_keys( $sql['varMap'] ) )
- ] );
- $dbWeb->insertSelect(
- $sql['destTable'],
- $sql['srcTable'],
- $sql['varMap'],
- $sql['conds'],
- __METHOD__,
- isset( $sql['insertOptions'] ) ? $sql['insertOptions'] : [],
- isset( $sql['selectOptions'] ) ? $sql['selectOptions'] : [],
- isset( $sql['selectJoinConds'] ) ? $sql['selectJoinConds'] : []
- );
- $this->assertLastSqlDb( implode( '; ', [ $sqlSelect, $sqlInsert ] ), $dbWeb );
- }
-
- public static function provideInsertSelect() {
- return [
- [
- [
- 'destTable' => 'insert_table',
- 'srcTable' => 'select_table',
- 'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
- 'conds' => '*',
- ],
- "INSERT INTO insert_table " .
- "(field_insert,field) " .
- "SELECT field_select,field2 " .
- "FROM select_table WHERE *",
- "SELECT field_select AS field_insert,field2 AS field " .
- "FROM select_table WHERE * FOR UPDATE",
- "INSERT INTO insert_table (field_insert,field) VALUES ('0','1')"
- ],
- [
- [
- 'destTable' => 'insert_table',
- 'srcTable' => 'select_table',
- 'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
- 'conds' => [ 'field' => 2 ],
- ],
- "INSERT INTO insert_table " .
- "(field_insert,field) " .
- "SELECT field_select,field2 " .
- "FROM select_table " .
- "WHERE field = '2'",
- "SELECT field_select AS field_insert,field2 AS field FROM " .
- "select_table WHERE field = '2' FOR UPDATE",
- "INSERT INTO insert_table (field_insert,field) VALUES ('0','1')"
- ],
- [
- [
- 'destTable' => 'insert_table',
- 'srcTable' => 'select_table',
- 'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
- 'conds' => [ 'field' => 2 ],
- 'insertOptions' => 'IGNORE',
- 'selectOptions' => [ 'ORDER BY' => 'field' ],
- ],
- "INSERT IGNORE INTO insert_table " .
- "(field_insert,field) " .
- "SELECT field_select,field2 " .
- "FROM select_table " .
- "WHERE field = '2' " .
- "ORDER BY field",
- "SELECT field_select AS field_insert,field2 AS field " .
- "FROM select_table WHERE field = '2' ORDER BY field FOR UPDATE",
- "INSERT IGNORE INTO insert_table (field_insert,field) VALUES ('0','1')"
- ],
- [
- [
- 'destTable' => 'insert_table',
- 'srcTable' => [ 'select_table1', 'select_table2' ],
- 'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
- 'conds' => [ 'field' => 2 ],
- 'selectOptions' => [ 'ORDER BY' => 'field', 'FORCE INDEX' => [ 'select_table1' => 'index1' ] ],
- 'selectJoinConds' => [
- 'select_table2' => [ 'LEFT JOIN', [ 'select_table1.foo = select_table2.bar' ] ],
- ],
- ],
- "INSERT INTO insert_table " .
- "(field_insert,field) " .
- "SELECT field_select,field2 " .
- "FROM select_table1 LEFT JOIN select_table2 ON ((select_table1.foo = select_table2.bar)) " .
- "WHERE field = '2' " .
- "ORDER BY field",
- "SELECT field_select AS field_insert,field2 AS field " .
- "FROM select_table1 LEFT JOIN select_table2 ON ((select_table1.foo = select_table2.bar)) " .
- "WHERE field = '2' ORDER BY field FOR UPDATE",
- "INSERT INTO insert_table (field_insert,field) VALUES ('0','1')"
- ],
- ];
- }
-
- /**
- * @dataProvider provideReplace
- * @covers Database::replace
- */
- public function testReplace( $sql, $sqlText ) {
- $this->database->replace(
- $sql['table'],
- $sql['uniqueIndexes'],
- $sql['rows'],
- __METHOD__
- );
- $this->assertLastSql( $sqlText );
- }
-
- public static function provideReplace() {
- return [
- [
- [
- 'table' => 'replace_table',
- 'uniqueIndexes' => [ 'field' ],
- 'rows' => [ 'field' => 'text', 'field2' => 'text2' ],
- ],
- "DELETE FROM replace_table " .
- "WHERE ( field='text' ); " .
- "INSERT INTO replace_table " .
- "(field,field2) " .
- "VALUES ('text','text2')"
- ],
- [
- [
- 'table' => 'module_deps',
- 'uniqueIndexes' => [ [ 'md_module', 'md_skin' ] ],
- 'rows' => [
- 'md_module' => 'module',
- 'md_skin' => 'skin',
- 'md_deps' => 'deps',
- ],
- ],
- "DELETE FROM module_deps " .
- "WHERE ( md_module='module' AND md_skin='skin' ); " .
- "INSERT INTO module_deps " .
- "(md_module,md_skin,md_deps) " .
- "VALUES ('module','skin','deps')"
- ],
- [
- [
- 'table' => 'module_deps',
- 'uniqueIndexes' => [ [ 'md_module', 'md_skin' ] ],
- 'rows' => [
- [
- 'md_module' => 'module',
- 'md_skin' => 'skin',
- 'md_deps' => 'deps',
- ], [
- 'md_module' => 'module2',
- 'md_skin' => 'skin2',
- 'md_deps' => 'deps2',
- ],
- ],
- ],
- "DELETE FROM module_deps " .
- "WHERE ( md_module='module' AND md_skin='skin' ); " .
- "INSERT INTO module_deps " .
- "(md_module,md_skin,md_deps) " .
- "VALUES ('module','skin','deps'); " .
- "DELETE FROM module_deps " .
- "WHERE ( md_module='module2' AND md_skin='skin2' ); " .
- "INSERT INTO module_deps " .
- "(md_module,md_skin,md_deps) " .
- "VALUES ('module2','skin2','deps2')"
- ],
- [
- [
- 'table' => 'module_deps',
- 'uniqueIndexes' => [ 'md_module', 'md_skin' ],
- 'rows' => [
- [
- 'md_module' => 'module',
- 'md_skin' => 'skin',
- 'md_deps' => 'deps',
- ], [
- 'md_module' => 'module2',
- 'md_skin' => 'skin2',
- 'md_deps' => 'deps2',
- ],
- ],
- ],
- "DELETE FROM module_deps " .
- "WHERE ( md_module='module' ) OR ( md_skin='skin' ); " .
- "INSERT INTO module_deps " .
- "(md_module,md_skin,md_deps) " .
- "VALUES ('module','skin','deps'); " .
- "DELETE FROM module_deps " .
- "WHERE ( md_module='module2' ) OR ( md_skin='skin2' ); " .
- "INSERT INTO module_deps " .
- "(md_module,md_skin,md_deps) " .
- "VALUES ('module2','skin2','deps2')"
- ],
- [
- [
- 'table' => 'module_deps',
- 'uniqueIndexes' => [],
- 'rows' => [
- 'md_module' => 'module',
- 'md_skin' => 'skin',
- 'md_deps' => 'deps',
- ],
- ],
- "INSERT INTO module_deps " .
- "(md_module,md_skin,md_deps) " .
- "VALUES ('module','skin','deps')"
- ],
- ];
- }
-
- /**
- * @dataProvider provideNativeReplace
- * @covers Database::nativeReplace
- */
- public function testNativeReplace( $sql, $sqlText ) {
- $this->database->nativeReplace(
- $sql['table'],
- $sql['rows'],
- __METHOD__
- );
- $this->assertLastSql( $sqlText );
- }
-
- public static function provideNativeReplace() {
- return [
- [
- [
- 'table' => 'replace_table',
- 'rows' => [ 'field' => 'text', 'field2' => 'text2' ],
- ],
- "REPLACE INTO replace_table " .
- "(field,field2) " .
- "VALUES ('text','text2')"
- ],
- ];
- }
-
- /**
- * @dataProvider provideConditional
- * @covers Database::conditional
- */
- public function testConditional( $sql, $sqlText ) {
- $this->assertEquals( trim( $this->database->conditional(
- $sql['conds'],
- $sql['true'],
- $sql['false']
- ) ), $sqlText );
- }
-
- public static function provideConditional() {
- return [
- [
- [
- 'conds' => [ 'field' => 'text' ],
- 'true' => 1,
- 'false' => 'NULL',
- ],
- "(CASE WHEN field = 'text' THEN 1 ELSE NULL END)"
- ],
- [
- [
- 'conds' => [ 'field' => 'text', 'field2' => 'anothertext' ],
- 'true' => 1,
- 'false' => 'NULL',
- ],
- "(CASE WHEN field = 'text' AND field2 = 'anothertext' THEN 1 ELSE NULL END)"
- ],
- [
- [
- 'conds' => 'field=1',
- 'true' => 1,
- 'false' => 'NULL',
- ],
- "(CASE WHEN field=1 THEN 1 ELSE NULL END)"
- ],
- ];
- }
-
- /**
- * @dataProvider provideBuildConcat
- * @covers Database::buildConcat
- */
- public function testBuildConcat( $stringList, $sqlText ) {
- $this->assertEquals( trim( $this->database->buildConcat(
- $stringList
- ) ), $sqlText );
- }
-
- public static function provideBuildConcat() {
- return [
- [
- [ 'field', 'field2' ],
- "CONCAT(field,field2)"
- ],
- [
- [ "'test'", 'field2' ],
- "CONCAT('test',field2)"
- ],
- ];
- }
-
- /**
- * @dataProvider provideBuildLike
- * @covers Database::buildLike
- */
- public function testBuildLike( $array, $sqlText ) {
- $this->assertEquals( trim( $this->database->buildLike(
- $array
- ) ), $sqlText );
- }
-
- public static function provideBuildLike() {
- return [
- [
- 'text',
- "LIKE 'text' ESCAPE '`'"
- ],
- [
- [ 'text', new LikeMatch( '%' ) ],
- "LIKE 'text%' ESCAPE '`'"
- ],
- [
- [ 'text', new LikeMatch( '%' ), 'text2' ],
- "LIKE 'text%text2' ESCAPE '`'"
- ],
- [
- [ 'text', new LikeMatch( '_' ) ],
- "LIKE 'text_' ESCAPE '`'"
- ],
- [
- 'more_text',
- "LIKE 'more`_text' ESCAPE '`'"
- ],
- [
- [ 'C:\\Windows\\', new LikeMatch( '%' ) ],
- "LIKE 'C:\\Windows\\%' ESCAPE '`'"
- ],
- [
- [ 'accent`_test`', new LikeMatch( '%' ) ],
- "LIKE 'accent```_test``%' ESCAPE '`'"
- ],
- ];
- }
-
- /**
- * @dataProvider provideUnionQueries
- * @covers Database::unionQueries
- */
- public function testUnionQueries( $sql, $sqlText ) {
- $this->assertEquals( trim( $this->database->unionQueries(
- $sql['sqls'],
- $sql['all']
- ) ), $sqlText );
- }
-
- public static function provideUnionQueries() {
- return [
- [
- [
- 'sqls' => [ 'RAW SQL', 'RAW2SQL' ],
- 'all' => true,
- ],
- "(RAW SQL) UNION ALL (RAW2SQL)"
- ],
- [
- [
- 'sqls' => [ 'RAW SQL', 'RAW2SQL' ],
- 'all' => false,
- ],
- "(RAW SQL) UNION (RAW2SQL)"
- ],
- [
- [
- 'sqls' => [ 'RAW SQL', 'RAW2SQL', 'RAW3SQL' ],
- 'all' => false,
- ],
- "(RAW SQL) UNION (RAW2SQL) UNION (RAW3SQL)"
- ],
- ];
- }
-
- /**
- * @dataProvider provideUnionConditionPermutations
- * @covers Database::unionConditionPermutations
- */
- public function testUnionConditionPermutations( $params, $expect ) {
- if ( isset( $params['unionSupportsOrderAndLimit'] ) ) {
- $this->database->setUnionSupportsOrderAndLimit( $params['unionSupportsOrderAndLimit'] );
- }
-
- $sql = trim( $this->database->unionConditionPermutations(
- $params['table'],
- $params['vars'],
- $params['permute_conds'],
- isset( $params['extra_conds'] ) ? $params['extra_conds'] : '',
- 'FNAME',
- isset( $params['options'] ) ? $params['options'] : [],
- isset( $params['join_conds'] ) ? $params['join_conds'] : []
- ) );
- $this->assertEquals( $expect, $sql );
- }
-
- public static function provideUnionConditionPermutations() {
- return [
- // @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong
- [
- [
- 'table' => [ 'table1', 'table2' ],
- 'vars' => [ 'field1', 'alias' => 'field2' ],
- 'permute_conds' => [
- 'field3' => [ 1, 2, 3 ],
- 'duplicates' => [ 4, 5, 4 ],
- 'empty' => [],
- 'single' => [ 0 ],
- ],
- 'extra_conds' => 'table2.bar > 23',
- 'options' => [
- 'ORDER BY' => [ 'field1', 'alias' ],
- 'INNER ORDER BY' => [ 'field1', 'field2' ],
- 'LIMIT' => 100,
- ],
- 'join_conds' => [
- 'table2' => [ 'JOIN', 'table1.foo_id = table2.foo_id' ],
- ],
- ],
- "(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 " .
- "(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 " .
- "(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 " .
- "(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 " .
- "(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 " .
- "(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 ) " .
- "ORDER BY field1,alias LIMIT 100"
- ],
- [
- [
- 'table' => 'foo',
- 'vars' => [ 'foo_id' ],
- 'permute_conds' => [
- 'bar' => [ 1, 2, 3 ],
- ],
- 'extra_conds' => [ 'baz' => null ],
- 'options' => [
- 'NOTALL',
- 'ORDER BY' => [ 'foo_id' ],
- 'LIMIT' => 25,
- ],
- ],
- "(SELECT foo_id FROM foo WHERE bar = '1' AND baz IS NULL ORDER BY foo_id LIMIT 25 ) UNION " .
- "(SELECT foo_id FROM foo WHERE bar = '2' AND baz IS NULL ORDER BY foo_id LIMIT 25 ) UNION " .
- "(SELECT foo_id FROM foo WHERE bar = '3' AND baz IS NULL ORDER BY foo_id LIMIT 25 ) " .
- "ORDER BY foo_id LIMIT 25"
- ],
- [
- [
- 'table' => 'foo',
- 'vars' => [ 'foo_id' ],
- 'permute_conds' => [
- 'bar' => [ 1, 2, 3 ],
- ],
- 'extra_conds' => [ 'baz' => null ],
- 'options' => [
- 'NOTALL' => true,
- 'ORDER BY' => [ 'foo_id' ],
- 'LIMIT' => 25,
- ],
- 'unionSupportsOrderAndLimit' => false,
- ],
- "(SELECT foo_id FROM foo WHERE bar = '1' AND baz IS NULL ) UNION " .
- "(SELECT foo_id FROM foo WHERE bar = '2' AND baz IS NULL ) UNION " .
- "(SELECT foo_id FROM foo WHERE bar = '3' AND baz IS NULL ) " .
- "ORDER BY foo_id LIMIT 25"
- ],
- [
- [
- 'table' => 'foo',
- 'vars' => [ 'foo_id' ],
- 'permute_conds' => [],
- 'extra_conds' => [ 'baz' => null ],
- 'options' => [
- 'ORDER BY' => [ 'foo_id' ],
- 'LIMIT' => 25,
- ],
- ],
- "SELECT foo_id FROM foo WHERE baz IS NULL ORDER BY foo_id LIMIT 25"
- ],
- [
- [
- 'table' => 'foo',
- 'vars' => [ 'foo_id' ],
- 'permute_conds' => [
- 'bar' => [],
- ],
- 'extra_conds' => [ 'baz' => null ],
- 'options' => [
- 'ORDER BY' => [ 'foo_id' ],
- 'LIMIT' => 25,
- ],
- ],
- "SELECT foo_id FROM foo WHERE baz IS NULL ORDER BY foo_id LIMIT 25"
- ],
- [
- [
- 'table' => 'foo',
- 'vars' => [ 'foo_id' ],
- 'permute_conds' => [
- 'bar' => [ 1 ],
- ],
- 'options' => [
- 'ORDER BY' => [ 'foo_id' ],
- 'LIMIT' => 25,
- 'OFFSET' => 150,
- ],
- ],
- "SELECT foo_id FROM foo WHERE bar = '1' ORDER BY foo_id LIMIT 150,25"
- ],
- [
- [
- 'table' => 'foo',
- 'vars' => [ 'foo_id' ],
- 'permute_conds' => [],
- 'extra_conds' => [ 'baz' => null ],
- 'options' => [
- 'ORDER BY' => [ 'foo_id' ],
- 'LIMIT' => 25,
- 'OFFSET' => 150,
- 'INNER ORDER BY' => [ 'bar_id' ],
- ],
- ],
- "(SELECT foo_id FROM foo WHERE baz IS NULL ORDER BY bar_id LIMIT 175 ) ORDER BY foo_id LIMIT 150,25"
- ],
- [
- [
- 'table' => 'foo',
- 'vars' => [ 'foo_id' ],
- 'permute_conds' => [],
- 'extra_conds' => [ 'baz' => null ],
- 'options' => [
- 'ORDER BY' => [ 'foo_id' ],
- 'LIMIT' => 25,
- 'OFFSET' => 150,
- 'INNER ORDER BY' => [ 'bar_id' ],
- ],
- 'unionSupportsOrderAndLimit' => false,
- ],
- "SELECT foo_id FROM foo WHERE baz IS NULL ORDER BY foo_id LIMIT 150,25"
- ],
- // @codingStandardsIgnoreEnd
- ];
- }
-
- /**
- * @covers Database::commit
- */
- public function testTransactionCommit() {
- $this->database->begin( __METHOD__ );
- $this->database->commit( __METHOD__ );
- $this->assertLastSql( 'BEGIN; COMMIT' );
- }
-
- /**
- * @covers Database::rollback
- */
- public function testTransactionRollback() {
- $this->database->begin( __METHOD__ );
- $this->database->rollback( __METHOD__ );
- $this->assertLastSql( 'BEGIN; ROLLBACK' );
- }
-
- /**
- * @covers Database::dropTable
- */
- public function testDropTable() {
- $this->database->setExistingTables( [ 'table' ] );
- $this->database->dropTable( 'table', __METHOD__ );
- $this->assertLastSql( 'DROP TABLE table CASCADE' );
- }
-
- /**
- * @covers Database::dropTable
- */
- public function testDropNonExistingTable() {
- $this->assertFalse(
- $this->database->dropTable( 'non_existing', __METHOD__ )
- );
- }
-
- /**
- * @dataProvider provideMakeList
- * @covers Database::makeList
- */
- public function testMakeList( $list, $mode, $sqlText ) {
- $this->assertEquals( trim( $this->database->makeList(
- $list, $mode
- ) ), $sqlText );
- }
-
- public static function provideMakeList() {
- return [
- [
- [ 'value', 'value2' ],
- LIST_COMMA,
- "'value','value2'"
- ],
- [
- [ 'field', 'field2' ],
- LIST_NAMES,
- "field,field2"
- ],
- [
- [ 'field' => 'value', 'field2' => 'value2' ],
- LIST_AND,
- "field = 'value' AND field2 = 'value2'"
- ],
- [
- [ 'field' => null, "field2 != 'value2'" ],
- LIST_AND,
- "field IS NULL AND (field2 != 'value2')"
- ],
- [
- [ 'field' => [ 'value', null, 'value2' ], 'field2' => 'value2' ],
- LIST_AND,
- "(field IN ('value','value2') OR field IS NULL) AND field2 = 'value2'"
- ],
- [
- [ 'field' => [ null ], 'field2' => null ],
- LIST_AND,
- "field IS NULL AND field2 IS NULL"
- ],
- [
- [ 'field' => 'value', 'field2' => 'value2' ],
- LIST_OR,
- "field = 'value' OR field2 = 'value2'"
- ],
- [
- [ 'field' => 'value', 'field2' => null ],
- LIST_OR,
- "field = 'value' OR field2 IS NULL"
- ],
- [
- [ 'field' => [ 'value', 'value2' ], 'field2' => [ 'value' ] ],
- LIST_OR,
- "field IN ('value','value2') OR field2 = 'value'"
- ],
- [
- [ 'field' => [ null, 'value', null, 'value2' ], "field2 != 'value2'" ],
- LIST_OR,
- "(field IN ('value','value2') OR field IS NULL) OR (field2 != 'value2')"
- ],
- [
- [ 'field' => 'value', 'field2' => 'value2' ],
- LIST_SET,
- "field = 'value',field2 = 'value2'"
- ],
- [
- [ 'field' => 'value', 'field2' => null ],
- LIST_SET,
- "field = 'value',field2 = NULL"
- ],
- [
- [ 'field' => 'value', "field2 != 'value2'" ],
- LIST_SET,
- "field = 'value',field2 != 'value2'"
- ],
- ];
- }
-
- public function testSessionTempTables() {
- $temp1 = $this->database->tableName( 'tmp_table_1' );
- $temp2 = $this->database->tableName( 'tmp_table_2' );
- $temp3 = $this->database->tableName( 'tmp_table_3' );
-
- $this->database->query( "CREATE TEMPORARY TABLE $temp1 LIKE orig_tbl", __METHOD__ );
- $this->database->query( "CREATE TEMPORARY TABLE $temp2 LIKE orig_tbl", __METHOD__ );
- $this->database->query( "CREATE TEMPORARY TABLE $temp3 LIKE orig_tbl", __METHOD__ );
-
- $this->assertTrue( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
- $this->assertTrue( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
- $this->assertTrue( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
-
- $this->database->dropTable( 'tmp_table_1', __METHOD__ );
- $this->database->dropTable( 'tmp_table_2', __METHOD__ );
- $this->database->dropTable( 'tmp_table_3', __METHOD__ );
-
- $this->assertFalse( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
- $this->assertFalse( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
- $this->assertFalse( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
-
- $this->database->query( "CREATE TEMPORARY TABLE tmp_table_1 LIKE orig_tbl", __METHOD__ );
- $this->database->query( "CREATE TEMPORARY TABLE 'tmp_table_2' LIKE orig_tbl", __METHOD__ );
- $this->database->query( "CREATE TEMPORARY TABLE `tmp_table_3` LIKE orig_tbl", __METHOD__ );
-
- $this->assertTrue( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
- $this->assertTrue( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
- $this->assertTrue( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
-
- $this->database->query( "DROP TEMPORARY TABLE tmp_table_1 LIKE orig_tbl", __METHOD__ );
- $this->database->query( "DROP TEMPORARY TABLE 'tmp_table_2' LIKE orig_tbl", __METHOD__ );
- $this->database->query( "DROP TABLE `tmp_table_3` LIKE orig_tbl", __METHOD__ );
-
- $this->assertFalse( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
- $this->assertFalse( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
- $this->assertFalse( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
- }
-}
new Blob( "hello" ),
"x'68656c6c6f'",
],
+ [ // #5: null
+ null,
+ "''",
+ ],
];
}
+++ /dev/null
-<?php
-
-use Wikimedia\Rdbms\IDatabase;
-
-/**
- * @group Database
- * @group Database
- */
-class DatabaseTest extends MediaWikiTestCase {
- /**
- * @var Database
- */
- protected $db;
-
- private $functionTest = false;
-
- protected function setUp() {
- parent::setUp();
- $this->db = wfGetDB( DB_MASTER );
- }
-
- protected function tearDown() {
- parent::tearDown();
- if ( $this->functionTest ) {
- $this->dropFunctions();
- $this->functionTest = false;
- }
- $this->db->restoreFlags( IDatabase::RESTORE_INITIAL );
- }
-
- /**
- * @covers Database::dropTable
- */
- public function testAddQuotesNull() {
- $check = "NULL";
- if ( $this->db->getType() === 'sqlite' || $this->db->getType() === 'oracle' ) {
- $check = "''";
- }
- $this->assertEquals( $check, $this->db->addQuotes( null ) );
- }
-
- public function testAddQuotesInt() {
- # returning just "1234" should be ok too, though...
- # maybe
- $this->assertEquals(
- "'1234'",
- $this->db->addQuotes( 1234 ) );
- }
-
- public function testAddQuotesFloat() {
- # returning just "1234.5678" would be ok too, though
- $this->assertEquals(
- "'1234.5678'",
- $this->db->addQuotes( 1234.5678 ) );
- }
-
- public function testAddQuotesString() {
- $this->assertEquals(
- "'string'",
- $this->db->addQuotes( 'string' ) );
- }
-
- public function testAddQuotesStringQuote() {
- $check = "'string''s cause trouble'";
- if ( $this->db->getType() === 'mysql' ) {
- $check = "'string\'s cause trouble'";
- }
- $this->assertEquals(
- $check,
- $this->db->addQuotes( "string's cause trouble" ) );
- }
-
- private function getSharedTableName( $table, $database, $prefix, $format = 'quoted' ) {
- global $wgSharedDB, $wgSharedTables, $wgSharedPrefix, $wgSharedSchema;
-
- $this->db->setTableAliases( [
- $table => [
- 'dbname' => $database,
- 'schema' => null,
- 'prefix' => $prefix
- ]
- ] );
-
- $ret = $this->db->tableName( $table, $format );
-
- $this->db->setTableAliases( array_fill_keys(
- $wgSharedDB ? $wgSharedTables : [],
- [
- 'dbname' => $wgSharedDB,
- 'schema' => $wgSharedSchema,
- 'prefix' => $wgSharedPrefix
- ]
- ) );
-
- return $ret;
- }
-
- private function prefixAndQuote( $table, $database = null, $prefix = null, $format = 'quoted' ) {
- if ( $this->db->getType() === 'sqlite' || $format !== 'quoted' ) {
- $quote = '';
- } elseif ( $this->db->getType() === 'mysql' ) {
- $quote = '`';
- } elseif ( $this->db->getType() === 'oracle' ) {
- $quote = '/*Q*/';
- } else {
- $quote = '"';
- }
-
- if ( $database !== null ) {
- if ( $this->db->getType() === 'oracle' ) {
- $database = $quote . $database . '.';
- } else {
- $database = $quote . $database . $quote . '.';
- }
- }
-
- if ( $prefix === null ) {
- $prefix = $this->dbPrefix();
- }
-
- if ( $this->db->getType() === 'oracle' ) {
- return strtoupper( $database . $quote . $prefix . $table );
- } else {
- return $database . $quote . $prefix . $table . $quote;
- }
- }
-
- public function testTableNameLocal() {
- $this->assertEquals(
- $this->prefixAndQuote( 'tablename' ),
- $this->db->tableName( 'tablename' )
- );
- }
-
- public function testTableNameRawLocal() {
- $this->assertEquals(
- $this->prefixAndQuote( 'tablename', null, null, 'raw' ),
- $this->db->tableName( 'tablename', 'raw' )
- );
- }
-
- public function testTableNameShared() {
- $this->assertEquals(
- $this->prefixAndQuote( 'tablename', 'sharedatabase', 'sh_' ),
- $this->getSharedTableName( 'tablename', 'sharedatabase', 'sh_' )
- );
-
- $this->assertEquals(
- $this->prefixAndQuote( 'tablename', 'sharedatabase', null ),
- $this->getSharedTableName( 'tablename', 'sharedatabase', null )
- );
- }
-
- public function testTableNameRawShared() {
- $this->assertEquals(
- $this->prefixAndQuote( 'tablename', 'sharedatabase', 'sh_', 'raw' ),
- $this->getSharedTableName( 'tablename', 'sharedatabase', 'sh_', 'raw' )
- );
-
- $this->assertEquals(
- $this->prefixAndQuote( 'tablename', 'sharedatabase', null, 'raw' ),
- $this->getSharedTableName( 'tablename', 'sharedatabase', null, 'raw' )
- );
- }
-
- public function testTableNameForeign() {
- $this->assertEquals(
- $this->prefixAndQuote( 'tablename', 'databasename', '' ),
- $this->db->tableName( 'databasename.tablename' )
- );
- }
-
- public function testTableNameRawForeign() {
- $this->assertEquals(
- $this->prefixAndQuote( 'tablename', 'databasename', '', 'raw' ),
- $this->db->tableName( 'databasename.tablename', 'raw' )
- );
- }
-
- public function testStoredFunctions() {
- if ( !in_array( wfGetDB( DB_MASTER )->getType(), [ 'mysql', 'postgres' ] ) ) {
- $this->markTestSkipped( 'MySQL or Postgres required' );
- }
- global $IP;
- $this->dropFunctions();
- $this->functionTest = true;
- $this->assertTrue(
- $this->db->sourceFile( "$IP/tests/phpunit/data/db/{$this->db->getType()}/functions.sql" )
- );
- $res = $this->db->query( 'SELECT mw_test_function() AS test', __METHOD__ );
- $this->assertEquals( 42, $res->fetchObject()->test );
- }
-
- private function dropFunctions() {
- $this->db->query( 'DROP FUNCTION IF EXISTS mw_test_function'
- . ( $this->db->getType() == 'postgres' ? '()' : '' )
- );
- }
-
- public function testUnknownTableCorruptsResults() {
- $res = $this->db->select( 'page', '*', [ 'page_id' => 1 ] );
- $this->assertFalse( $this->db->tableExists( 'foobarbaz' ) );
- $this->assertInternalType( 'int', $res->numRows() );
- }
-
- public function testTransactionIdle() {
- $db = $this->db;
-
- $db->setFlag( DBO_TRX );
- $called = false;
- $flagSet = null;
- $db->onTransactionIdle(
- function () use ( $db, &$flagSet, &$called ) {
- $called = true;
- $flagSet = $db->getFlag( DBO_TRX );
- },
- __METHOD__
- );
- $this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
- $this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
- $this->assertTrue( $called, 'Callback reached' );
-
- $db->clearFlag( DBO_TRX );
- $flagSet = null;
- $db->onTransactionIdle(
- function () use ( $db, &$flagSet ) {
- $flagSet = $db->getFlag( DBO_TRX );
- },
- __METHOD__
- );
- $this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
- $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
-
- $db->clearFlag( DBO_TRX );
- $db->onTransactionIdle(
- function () use ( $db ) {
- $db->setFlag( DBO_TRX );
- },
- __METHOD__
- );
- $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
- }
-
- public function testTransactionResolution() {
- $db = $this->db;
-
- $db->clearFlag( DBO_TRX );
- $db->begin( __METHOD__ );
- $called = false;
- $db->onTransactionResolution( function () use ( $db, &$called ) {
- $called = true;
- $db->setFlag( DBO_TRX );
- } );
- $db->commit( __METHOD__ );
- $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
- $this->assertTrue( $called, 'Callback reached' );
-
- $db->clearFlag( DBO_TRX );
- $db->begin( __METHOD__ );
- $called = false;
- $db->onTransactionResolution( function () use ( $db, &$called ) {
- $called = true;
- $db->setFlag( DBO_TRX );
- } );
- $db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
- $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
- $this->assertTrue( $called, 'Callback reached' );
- }
-
- /**
- * @covers Database::setTransactionListener()
- */
- public function testTransactionListener() {
- $db = $this->db;
-
- $db->setTransactionListener( 'ping', function () use ( $db, &$called ) {
- $called = true;
- } );
-
- $called = false;
- $db->begin( __METHOD__ );
- $db->commit( __METHOD__ );
- $this->assertTrue( $called, 'Callback reached' );
-
- $called = false;
- $db->begin( __METHOD__ );
- $db->commit( __METHOD__ );
- $this->assertTrue( $called, 'Callback still reached' );
-
- $called = false;
- $db->begin( __METHOD__ );
- $db->rollback( __METHOD__ );
- $this->assertTrue( $called, 'Callback reached' );
-
- $db->setTransactionListener( 'ping', null );
- $called = false;
- $db->begin( __METHOD__ );
- $db->commit( __METHOD__ );
- $this->assertFalse( $called, 'Callback not reached' );
- }
-
- /**
- * @covers Database::flushSnapshot()
- */
- public function testFlushSnapshot() {
- $db = $this->db;
-
- $db->flushSnapshot( __METHOD__ ); // ok
- $db->flushSnapshot( __METHOD__ ); // ok
-
- $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
- $db->query( 'SELECT 1', __METHOD__ );
- $this->assertTrue( (bool)$db->trxLevel(), "Transaction started." );
- $db->flushSnapshot( __METHOD__ ); // ok
- $db->restoreFlags( $db::RESTORE_PRIOR );
-
- $this->assertFalse( (bool)$db->trxLevel(), "Transaction cleared." );
- }
-
- public function testGetScopedLock() {
- $db = $this->db;
-
- $db->setFlag( DBO_TRX );
- try {
- $this->badLockingMethodImplicit( $db );
- } catch ( RunTimeException $e ) {
- $this->assertTrue( $db->trxLevel() > 0, "Transaction not committed." );
- }
- $db->clearFlag( DBO_TRX );
- $db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
- $this->assertTrue( $db->lockIsFree( 'meow', __METHOD__ ) );
-
- try {
- $this->badLockingMethodExplicit( $db );
- } catch ( RunTimeException $e ) {
- $this->assertTrue( $db->trxLevel() > 0, "Transaction not committed." );
- }
- $db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
- $this->assertTrue( $db->lockIsFree( 'meow', __METHOD__ ) );
- }
-
- private function badLockingMethodImplicit( IDatabase $db ) {
- $lock = $db->getScopedLockAndFlush( 'meow', __METHOD__, 1 );
- $db->query( "SELECT 1" ); // trigger DBO_TRX
- throw new RunTimeException( "Uh oh!" );
- }
-
- private function badLockingMethodExplicit( IDatabase $db ) {
- $lock = $db->getScopedLockAndFlush( 'meow', __METHOD__, 1 );
- $db->begin( __METHOD__ );
- throw new RunTimeException( "Uh oh!" );
- }
-
- /**
- * @covers Database::getFlag(
- * @covers Database::setFlag()
- * @covers Database::restoreFlags()
- */
- public function testFlagSetting() {
- $db = $this->db;
- $origTrx = $db->getFlag( DBO_TRX );
- $origSsl = $db->getFlag( DBO_SSL );
-
- $origTrx
- ? $db->clearFlag( DBO_TRX, $db::REMEMBER_PRIOR )
- : $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
- $this->assertEquals( !$origTrx, $db->getFlag( DBO_TRX ) );
-
- $origSsl
- ? $db->clearFlag( DBO_SSL, $db::REMEMBER_PRIOR )
- : $db->setFlag( DBO_SSL, $db::REMEMBER_PRIOR );
- $this->assertEquals( !$origSsl, $db->getFlag( DBO_SSL ) );
-
- $db->restoreFlags( $db::RESTORE_INITIAL );
- $this->assertEquals( $origTrx, $db->getFlag( DBO_TRX ) );
- $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
-
- $origTrx
- ? $db->clearFlag( DBO_TRX, $db::REMEMBER_PRIOR )
- : $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
- $origSsl
- ? $db->clearFlag( DBO_SSL, $db::REMEMBER_PRIOR )
- : $db->setFlag( DBO_SSL, $db::REMEMBER_PRIOR );
-
- $db->restoreFlags();
- $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
- $this->assertEquals( !$origTrx, $db->getFlag( DBO_TRX ) );
-
- $db->restoreFlags();
- $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
- $this->assertEquals( $origTrx, $db->getFlag( DBO_TRX ) );
- }
-
- /**
- * @covers Database::tablePrefix()
- * @covers Database::dbSchema()
- */
- public function testMutators() {
- $old = $this->db->tablePrefix();
- $this->assertType( 'string', $old, 'Prefix is string' );
- $this->assertEquals( $old, $this->db->tablePrefix(), "Prefix unchanged" );
- $this->assertEquals( $old, $this->db->tablePrefix( 'xxx' ) );
- $this->assertEquals( 'xxx', $this->db->tablePrefix(), "Prefix set" );
- $this->db->tablePrefix( $old );
- $this->assertNotEquals( 'xxx', $this->db->tablePrefix() );
-
- $old = $this->db->dbSchema();
- $this->assertType( 'string', $old, 'Schema is string' );
- $this->assertEquals( $old, $this->db->dbSchema(), "Schema unchanged" );
- $this->assertEquals( $old, $this->db->dbSchema( 'xxx' ) );
- $this->assertEquals( 'xxx', $this->db->dbSchema(), "Schema set" );
- $this->db->dbSchema( $old );
- $this->assertNotEquals( 'xxx', $this->db->dbSchema() );
- }
-}
class DatabaseMysqlBaseTest extends PHPUnit_Framework_TestCase {
/**
* @dataProvider provideDiapers
- * @covers DatabaseMysqlBase::addIdentifierQuotes
+ * @covers Wikimedia\Rdbms\DatabaseMysqlBase::addIdentifierQuotes
*/
public function testAddIdentifierQuotes( $expected, $in ) {
$db = new FakeDatabaseMysqlBase();
}
/**
- * @covers DatabaseMysqlBase::listViews
+ * @covers Wikimedia\Rdbms\DatabaseMysqlBase::listViews
*/
public function testListviews() {
$db = $this->getMockForViews();
/**
* @dataProvider provideComparePositions
- * @covers MySQLMasterPos
+ * @covers Wikimedia\Rdbms\MySQLMasterPos
*/
public function testHasReached( MySQLMasterPos $lowerPos, MySQLMasterPos $higherPos, $match ) {
if ( $match ) {
/**
* @dataProvider provideChannelPositions
- * @covers MySQLMasterPos
+ * @covers Wikimedia\Rdbms\MySQLMasterPos
*/
public function testChannelsMatch( MySQLMasterPos $pos1, MySQLMasterPos $pos2, $matches ) {
$this->assertEquals( $matches, $pos1->channelsMatch( $pos2 ) );
/**
* @dataProvider provideLagAmounts
- * @covers DatabaseMysqlBase::getLag
- * @covers DatabaseMysqlBase::getLagFromPtHeartbeat
+ * @covers Wikimedia\Rdbms\DatabaseMysqlBase::getLag
+ * @covers Wikimedia\Rdbms\DatabaseMysqlBase::getLagFromPtHeartbeat
*/
public function testPtHeartbeat( $lag ) {
$db = $this->getMockBuilder( 'DatabaseMysqli' )
--- /dev/null
+<?php
+
+use Wikimedia\Rdbms\LikeMatch;
+
+/**
+ * Test the parts of the Database abstract class that deal
+ * with creating SQL text.
+ */
+class DatabaseSQLTest extends PHPUnit_Framework_TestCase {
+ /** @var DatabaseTestHelper */
+ private $database;
+
+ protected function setUp() {
+ parent::setUp();
+ $this->database = new DatabaseTestHelper( __CLASS__, [ 'cliMode' => true ] );
+ }
+
+ protected function assertLastSql( $sqlText ) {
+ $this->assertEquals(
+ $sqlText,
+ $this->database->getLastSqls()
+ );
+ }
+
+ protected function assertLastSqlDb( $sqlText, $db ) {
+ $this->assertEquals( $sqlText, $db->getLastSqls() );
+ }
+
+ /**
+ * @dataProvider provideSelect
+ * @covers Wikimedia\Rdbms\Database::select
+ */
+ public function testSelect( $sql, $sqlText ) {
+ $this->database->select(
+ $sql['tables'],
+ $sql['fields'],
+ isset( $sql['conds'] ) ? $sql['conds'] : [],
+ __METHOD__,
+ isset( $sql['options'] ) ? $sql['options'] : [],
+ isset( $sql['join_conds'] ) ? $sql['join_conds'] : []
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideSelect() {
+ return [
+ [
+ [
+ 'tables' => 'table',
+ 'fields' => [ 'field', 'alias' => 'field2' ],
+ 'conds' => [ 'alias' => 'text' ],
+ ],
+ "SELECT field,field2 AS alias " .
+ "FROM table " .
+ "WHERE alias = 'text'"
+ ],
+ [
+ [
+ 'tables' => 'table',
+ 'fields' => [ 'field', 'alias' => 'field2' ],
+ 'conds' => [ 'alias' => 'text' ],
+ 'options' => [ 'LIMIT' => 1, 'ORDER BY' => 'field' ],
+ ],
+ "SELECT field,field2 AS alias " .
+ "FROM table " .
+ "WHERE alias = 'text' " .
+ "ORDER BY field " .
+ "LIMIT 1"
+ ],
+ [
+ [
+ 'tables' => [ 'table', 't2' => 'table2' ],
+ 'fields' => [ 'tid', 'field', 'alias' => 'field2', 't2.id' ],
+ 'conds' => [ 'alias' => 'text' ],
+ 'options' => [ 'LIMIT' => 1, 'ORDER BY' => 'field' ],
+ 'join_conds' => [ 't2' => [
+ 'LEFT JOIN', 'tid = t2.id'
+ ] ],
+ ],
+ "SELECT tid,field,field2 AS alias,t2.id " .
+ "FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " .
+ "WHERE alias = 'text' " .
+ "ORDER BY field " .
+ "LIMIT 1"
+ ],
+ [
+ [
+ 'tables' => [ 'table', 't2' => 'table2' ],
+ 'fields' => [ 'tid', 'field', 'alias' => 'field2', 't2.id' ],
+ 'conds' => [ 'alias' => 'text' ],
+ 'options' => [ 'LIMIT' => 1, 'GROUP BY' => 'field', 'HAVING' => 'COUNT(*) > 1' ],
+ 'join_conds' => [ 't2' => [
+ 'LEFT JOIN', 'tid = t2.id'
+ ] ],
+ ],
+ "SELECT tid,field,field2 AS alias,t2.id " .
+ "FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " .
+ "WHERE alias = 'text' " .
+ "GROUP BY field HAVING COUNT(*) > 1 " .
+ "LIMIT 1"
+ ],
+ [
+ [
+ 'tables' => [ 'table', 't2' => 'table2' ],
+ 'fields' => [ 'tid', 'field', 'alias' => 'field2', 't2.id' ],
+ 'conds' => [ 'alias' => 'text' ],
+ 'options' => [
+ 'LIMIT' => 1,
+ 'GROUP BY' => [ 'field', 'field2' ],
+ 'HAVING' => [ 'COUNT(*) > 1', 'field' => 1 ]
+ ],
+ 'join_conds' => [ 't2' => [
+ 'LEFT JOIN', 'tid = t2.id'
+ ] ],
+ ],
+ "SELECT tid,field,field2 AS alias,t2.id " .
+ "FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " .
+ "WHERE alias = 'text' " .
+ "GROUP BY field,field2 HAVING (COUNT(*) > 1) AND field = '1' " .
+ "LIMIT 1"
+ ],
+ [
+ [
+ 'tables' => [ 'table' ],
+ 'fields' => [ 'alias' => 'field' ],
+ 'conds' => [ 'alias' => [ 1, 2, 3, 4 ] ],
+ ],
+ "SELECT field AS alias " .
+ "FROM table " .
+ "WHERE alias IN ('1','2','3','4')"
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideUpdate
+ * @covers Wikimedia\Rdbms\Database::update
+ */
+ public function testUpdate( $sql, $sqlText ) {
+ $this->database->update(
+ $sql['table'],
+ $sql['values'],
+ $sql['conds'],
+ __METHOD__,
+ isset( $sql['options'] ) ? $sql['options'] : []
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideUpdate() {
+ return [
+ [
+ [
+ 'table' => 'table',
+ 'values' => [ 'field' => 'text', 'field2' => 'text2' ],
+ 'conds' => [ 'alias' => 'text' ],
+ ],
+ "UPDATE table " .
+ "SET field = 'text'" .
+ ",field2 = 'text2' " .
+ "WHERE alias = 'text'"
+ ],
+ [
+ [
+ 'table' => 'table',
+ 'values' => [ 'field = other', 'field2' => 'text2' ],
+ 'conds' => [ 'id' => '1' ],
+ ],
+ "UPDATE table " .
+ "SET field = other" .
+ ",field2 = 'text2' " .
+ "WHERE id = '1'"
+ ],
+ [
+ [
+ 'table' => 'table',
+ 'values' => [ 'field = other', 'field2' => 'text2' ],
+ 'conds' => '*',
+ ],
+ "UPDATE table " .
+ "SET field = other" .
+ ",field2 = 'text2'"
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideDelete
+ * @covers Wikimedia\Rdbms\Database::delete
+ */
+ public function testDelete( $sql, $sqlText ) {
+ $this->database->delete(
+ $sql['table'],
+ $sql['conds'],
+ __METHOD__
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideDelete() {
+ return [
+ [
+ [
+ 'table' => 'table',
+ 'conds' => [ 'alias' => 'text' ],
+ ],
+ "DELETE FROM table " .
+ "WHERE alias = 'text'"
+ ],
+ [
+ [
+ 'table' => 'table',
+ 'conds' => '*',
+ ],
+ "DELETE FROM table"
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideUpsert
+ * @covers Wikimedia\Rdbms\Database::upsert
+ */
+ public function testUpsert( $sql, $sqlText ) {
+ $this->database->upsert(
+ $sql['table'],
+ $sql['rows'],
+ $sql['uniqueIndexes'],
+ $sql['set'],
+ __METHOD__
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideUpsert() {
+ return [
+ [
+ [
+ 'table' => 'upsert_table',
+ 'rows' => [ 'field' => 'text', 'field2' => 'text2' ],
+ 'uniqueIndexes' => [ 'field' ],
+ 'set' => [ 'field' => 'set' ],
+ ],
+ "BEGIN; " .
+ "UPDATE upsert_table " .
+ "SET field = 'set' " .
+ "WHERE ((field = 'text')); " .
+ "INSERT IGNORE INTO upsert_table " .
+ "(field,field2) " .
+ "VALUES ('text','text2'); " .
+ "COMMIT"
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideDeleteJoin
+ * @covers Wikimedia\Rdbms\Database::deleteJoin
+ */
+ public function testDeleteJoin( $sql, $sqlText ) {
+ $this->database->deleteJoin(
+ $sql['delTable'],
+ $sql['joinTable'],
+ $sql['delVar'],
+ $sql['joinVar'],
+ $sql['conds'],
+ __METHOD__
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideDeleteJoin() {
+ return [
+ [
+ [
+ 'delTable' => 'table',
+ 'joinTable' => 'table_join',
+ 'delVar' => 'field',
+ 'joinVar' => 'field_join',
+ 'conds' => [ 'alias' => 'text' ],
+ ],
+ "DELETE FROM table " .
+ "WHERE field IN (" .
+ "SELECT field_join FROM table_join WHERE alias = 'text'" .
+ ")"
+ ],
+ [
+ [
+ 'delTable' => 'table',
+ 'joinTable' => 'table_join',
+ 'delVar' => 'field',
+ 'joinVar' => 'field_join',
+ 'conds' => '*',
+ ],
+ "DELETE FROM table " .
+ "WHERE field IN (" .
+ "SELECT field_join FROM table_join " .
+ ")"
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideInsert
+ * @covers Wikimedia\Rdbms\Database::insert
+ */
+ public function testInsert( $sql, $sqlText ) {
+ $this->database->insert(
+ $sql['table'],
+ $sql['rows'],
+ __METHOD__,
+ isset( $sql['options'] ) ? $sql['options'] : []
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideInsert() {
+ return [
+ [
+ [
+ 'table' => 'table',
+ 'rows' => [ 'field' => 'text', 'field2' => 2 ],
+ ],
+ "INSERT INTO table " .
+ "(field,field2) " .
+ "VALUES ('text','2')"
+ ],
+ [
+ [
+ 'table' => 'table',
+ 'rows' => [ 'field' => 'text', 'field2' => 2 ],
+ 'options' => 'IGNORE',
+ ],
+ "INSERT IGNORE INTO table " .
+ "(field,field2) " .
+ "VALUES ('text','2')"
+ ],
+ [
+ [
+ 'table' => 'table',
+ 'rows' => [
+ [ 'field' => 'text', 'field2' => 2 ],
+ [ 'field' => 'multi', 'field2' => 3 ],
+ ],
+ 'options' => 'IGNORE',
+ ],
+ "INSERT IGNORE INTO table " .
+ "(field,field2) " .
+ "VALUES " .
+ "('text','2')," .
+ "('multi','3')"
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideInsertSelect
+ * @covers Wikimedia\Rdbms\Database::insertSelect
+ */
+ public function testInsertSelect( $sql, $sqlTextNative, $sqlSelect, $sqlInsert ) {
+ $this->database->insertSelect(
+ $sql['destTable'],
+ $sql['srcTable'],
+ $sql['varMap'],
+ $sql['conds'],
+ __METHOD__,
+ isset( $sql['insertOptions'] ) ? $sql['insertOptions'] : [],
+ isset( $sql['selectOptions'] ) ? $sql['selectOptions'] : [],
+ isset( $sql['selectJoinConds'] ) ? $sql['selectJoinConds'] : []
+ );
+ $this->assertLastSql( $sqlTextNative );
+
+ $dbWeb = new DatabaseTestHelper( __CLASS__, [ 'cliMode' => false ] );
+ $dbWeb->forceNextResult( [
+ array_flip( array_keys( $sql['varMap'] ) )
+ ] );
+ $dbWeb->insertSelect(
+ $sql['destTable'],
+ $sql['srcTable'],
+ $sql['varMap'],
+ $sql['conds'],
+ __METHOD__,
+ isset( $sql['insertOptions'] ) ? $sql['insertOptions'] : [],
+ isset( $sql['selectOptions'] ) ? $sql['selectOptions'] : [],
+ isset( $sql['selectJoinConds'] ) ? $sql['selectJoinConds'] : []
+ );
+ $this->assertLastSqlDb( implode( '; ', [ $sqlSelect, $sqlInsert ] ), $dbWeb );
+ }
+
+ public static function provideInsertSelect() {
+ return [
+ [
+ [
+ 'destTable' => 'insert_table',
+ 'srcTable' => 'select_table',
+ 'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
+ 'conds' => '*',
+ ],
+ "INSERT INTO insert_table " .
+ "(field_insert,field) " .
+ "SELECT field_select,field2 " .
+ "FROM select_table WHERE *",
+ "SELECT field_select AS field_insert,field2 AS field " .
+ "FROM select_table WHERE * FOR UPDATE",
+ "INSERT INTO insert_table (field_insert,field) VALUES ('0','1')"
+ ],
+ [
+ [
+ 'destTable' => 'insert_table',
+ 'srcTable' => 'select_table',
+ 'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
+ 'conds' => [ 'field' => 2 ],
+ ],
+ "INSERT INTO insert_table " .
+ "(field_insert,field) " .
+ "SELECT field_select,field2 " .
+ "FROM select_table " .
+ "WHERE field = '2'",
+ "SELECT field_select AS field_insert,field2 AS field FROM " .
+ "select_table WHERE field = '2' FOR UPDATE",
+ "INSERT INTO insert_table (field_insert,field) VALUES ('0','1')"
+ ],
+ [
+ [
+ 'destTable' => 'insert_table',
+ 'srcTable' => 'select_table',
+ 'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
+ 'conds' => [ 'field' => 2 ],
+ 'insertOptions' => 'IGNORE',
+ 'selectOptions' => [ 'ORDER BY' => 'field' ],
+ ],
+ "INSERT IGNORE INTO insert_table " .
+ "(field_insert,field) " .
+ "SELECT field_select,field2 " .
+ "FROM select_table " .
+ "WHERE field = '2' " .
+ "ORDER BY field",
+ "SELECT field_select AS field_insert,field2 AS field " .
+ "FROM select_table WHERE field = '2' ORDER BY field FOR UPDATE",
+ "INSERT IGNORE INTO insert_table (field_insert,field) VALUES ('0','1')"
+ ],
+ [
+ [
+ 'destTable' => 'insert_table',
+ 'srcTable' => [ 'select_table1', 'select_table2' ],
+ 'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
+ 'conds' => [ 'field' => 2 ],
+ 'selectOptions' => [ 'ORDER BY' => 'field', 'FORCE INDEX' => [ 'select_table1' => 'index1' ] ],
+ 'selectJoinConds' => [
+ 'select_table2' => [ 'LEFT JOIN', [ 'select_table1.foo = select_table2.bar' ] ],
+ ],
+ ],
+ "INSERT INTO insert_table " .
+ "(field_insert,field) " .
+ "SELECT field_select,field2 " .
+ "FROM select_table1 LEFT JOIN select_table2 ON ((select_table1.foo = select_table2.bar)) " .
+ "WHERE field = '2' " .
+ "ORDER BY field",
+ "SELECT field_select AS field_insert,field2 AS field " .
+ "FROM select_table1 LEFT JOIN select_table2 ON ((select_table1.foo = select_table2.bar)) " .
+ "WHERE field = '2' ORDER BY field FOR UPDATE",
+ "INSERT INTO insert_table (field_insert,field) VALUES ('0','1')"
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideReplace
+ * @covers Wikimedia\Rdbms\Database::replace
+ */
+ public function testReplace( $sql, $sqlText ) {
+ $this->database->replace(
+ $sql['table'],
+ $sql['uniqueIndexes'],
+ $sql['rows'],
+ __METHOD__
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideReplace() {
+ return [
+ [
+ [
+ 'table' => 'replace_table',
+ 'uniqueIndexes' => [ 'field' ],
+ 'rows' => [ 'field' => 'text', 'field2' => 'text2' ],
+ ],
+ "DELETE FROM replace_table " .
+ "WHERE ( field='text' ); " .
+ "INSERT INTO replace_table " .
+ "(field,field2) " .
+ "VALUES ('text','text2')"
+ ],
+ [
+ [
+ 'table' => 'module_deps',
+ 'uniqueIndexes' => [ [ 'md_module', 'md_skin' ] ],
+ 'rows' => [
+ 'md_module' => 'module',
+ 'md_skin' => 'skin',
+ 'md_deps' => 'deps',
+ ],
+ ],
+ "DELETE FROM module_deps " .
+ "WHERE ( md_module='module' AND md_skin='skin' ); " .
+ "INSERT INTO module_deps " .
+ "(md_module,md_skin,md_deps) " .
+ "VALUES ('module','skin','deps')"
+ ],
+ [
+ [
+ 'table' => 'module_deps',
+ 'uniqueIndexes' => [ [ 'md_module', 'md_skin' ] ],
+ 'rows' => [
+ [
+ 'md_module' => 'module',
+ 'md_skin' => 'skin',
+ 'md_deps' => 'deps',
+ ], [
+ 'md_module' => 'module2',
+ 'md_skin' => 'skin2',
+ 'md_deps' => 'deps2',
+ ],
+ ],
+ ],
+ "DELETE FROM module_deps " .
+ "WHERE ( md_module='module' AND md_skin='skin' ); " .
+ "INSERT INTO module_deps " .
+ "(md_module,md_skin,md_deps) " .
+ "VALUES ('module','skin','deps'); " .
+ "DELETE FROM module_deps " .
+ "WHERE ( md_module='module2' AND md_skin='skin2' ); " .
+ "INSERT INTO module_deps " .
+ "(md_module,md_skin,md_deps) " .
+ "VALUES ('module2','skin2','deps2')"
+ ],
+ [
+ [
+ 'table' => 'module_deps',
+ 'uniqueIndexes' => [ 'md_module', 'md_skin' ],
+ 'rows' => [
+ [
+ 'md_module' => 'module',
+ 'md_skin' => 'skin',
+ 'md_deps' => 'deps',
+ ], [
+ 'md_module' => 'module2',
+ 'md_skin' => 'skin2',
+ 'md_deps' => 'deps2',
+ ],
+ ],
+ ],
+ "DELETE FROM module_deps " .
+ "WHERE ( md_module='module' ) OR ( md_skin='skin' ); " .
+ "INSERT INTO module_deps " .
+ "(md_module,md_skin,md_deps) " .
+ "VALUES ('module','skin','deps'); " .
+ "DELETE FROM module_deps " .
+ "WHERE ( md_module='module2' ) OR ( md_skin='skin2' ); " .
+ "INSERT INTO module_deps " .
+ "(md_module,md_skin,md_deps) " .
+ "VALUES ('module2','skin2','deps2')"
+ ],
+ [
+ [
+ 'table' => 'module_deps',
+ 'uniqueIndexes' => [],
+ 'rows' => [
+ 'md_module' => 'module',
+ 'md_skin' => 'skin',
+ 'md_deps' => 'deps',
+ ],
+ ],
+ "INSERT INTO module_deps " .
+ "(md_module,md_skin,md_deps) " .
+ "VALUES ('module','skin','deps')"
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideNativeReplace
+ * @covers Wikimedia\Rdbms\Database::nativeReplace
+ */
+ public function testNativeReplace( $sql, $sqlText ) {
+ $this->database->nativeReplace(
+ $sql['table'],
+ $sql['rows'],
+ __METHOD__
+ );
+ $this->assertLastSql( $sqlText );
+ }
+
+ public static function provideNativeReplace() {
+ return [
+ [
+ [
+ 'table' => 'replace_table',
+ 'rows' => [ 'field' => 'text', 'field2' => 'text2' ],
+ ],
+ "REPLACE INTO replace_table " .
+ "(field,field2) " .
+ "VALUES ('text','text2')"
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideConditional
+ * @covers Wikimedia\Rdbms\Database::conditional
+ */
+ public function testConditional( $sql, $sqlText ) {
+ $this->assertEquals( trim( $this->database->conditional(
+ $sql['conds'],
+ $sql['true'],
+ $sql['false']
+ ) ), $sqlText );
+ }
+
+ public static function provideConditional() {
+ return [
+ [
+ [
+ 'conds' => [ 'field' => 'text' ],
+ 'true' => 1,
+ 'false' => 'NULL',
+ ],
+ "(CASE WHEN field = 'text' THEN 1 ELSE NULL END)"
+ ],
+ [
+ [
+ 'conds' => [ 'field' => 'text', 'field2' => 'anothertext' ],
+ 'true' => 1,
+ 'false' => 'NULL',
+ ],
+ "(CASE WHEN field = 'text' AND field2 = 'anothertext' THEN 1 ELSE NULL END)"
+ ],
+ [
+ [
+ 'conds' => 'field=1',
+ 'true' => 1,
+ 'false' => 'NULL',
+ ],
+ "(CASE WHEN field=1 THEN 1 ELSE NULL END)"
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideBuildConcat
+ * @covers Wikimedia\Rdbms\Database::buildConcat
+ */
+ public function testBuildConcat( $stringList, $sqlText ) {
+ $this->assertEquals( trim( $this->database->buildConcat(
+ $stringList
+ ) ), $sqlText );
+ }
+
+ public static function provideBuildConcat() {
+ return [
+ [
+ [ 'field', 'field2' ],
+ "CONCAT(field,field2)"
+ ],
+ [
+ [ "'test'", 'field2' ],
+ "CONCAT('test',field2)"
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideBuildLike
+ * @covers Wikimedia\Rdbms\Database::buildLike
+ */
+ public function testBuildLike( $array, $sqlText ) {
+ $this->assertEquals( trim( $this->database->buildLike(
+ $array
+ ) ), $sqlText );
+ }
+
+ public static function provideBuildLike() {
+ return [
+ [
+ 'text',
+ "LIKE 'text' ESCAPE '`'"
+ ],
+ [
+ [ 'text', new LikeMatch( '%' ) ],
+ "LIKE 'text%' ESCAPE '`'"
+ ],
+ [
+ [ 'text', new LikeMatch( '%' ), 'text2' ],
+ "LIKE 'text%text2' ESCAPE '`'"
+ ],
+ [
+ [ 'text', new LikeMatch( '_' ) ],
+ "LIKE 'text_' ESCAPE '`'"
+ ],
+ [
+ 'more_text',
+ "LIKE 'more`_text' ESCAPE '`'"
+ ],
+ [
+ [ 'C:\\Windows\\', new LikeMatch( '%' ) ],
+ "LIKE 'C:\\Windows\\%' ESCAPE '`'"
+ ],
+ [
+ [ 'accent`_test`', new LikeMatch( '%' ) ],
+ "LIKE 'accent```_test``%' ESCAPE '`'"
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideUnionQueries
+ * @covers Wikimedia\Rdbms\Database::unionQueries
+ */
+ public function testUnionQueries( $sql, $sqlText ) {
+ $this->assertEquals( trim( $this->database->unionQueries(
+ $sql['sqls'],
+ $sql['all']
+ ) ), $sqlText );
+ }
+
+ public static function provideUnionQueries() {
+ return [
+ [
+ [
+ 'sqls' => [ 'RAW SQL', 'RAW2SQL' ],
+ 'all' => true,
+ ],
+ "(RAW SQL) UNION ALL (RAW2SQL)"
+ ],
+ [
+ [
+ 'sqls' => [ 'RAW SQL', 'RAW2SQL' ],
+ 'all' => false,
+ ],
+ "(RAW SQL) UNION (RAW2SQL)"
+ ],
+ [
+ [
+ 'sqls' => [ 'RAW SQL', 'RAW2SQL', 'RAW3SQL' ],
+ 'all' => false,
+ ],
+ "(RAW SQL) UNION (RAW2SQL) UNION (RAW3SQL)"
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideUnionConditionPermutations
+ * @covers Wikimedia\Rdbms\Database::unionConditionPermutations
+ */
+ public function testUnionConditionPermutations( $params, $expect ) {
+ if ( isset( $params['unionSupportsOrderAndLimit'] ) ) {
+ $this->database->setUnionSupportsOrderAndLimit( $params['unionSupportsOrderAndLimit'] );
+ }
+
+ $sql = trim( $this->database->unionConditionPermutations(
+ $params['table'],
+ $params['vars'],
+ $params['permute_conds'],
+ isset( $params['extra_conds'] ) ? $params['extra_conds'] : '',
+ 'FNAME',
+ isset( $params['options'] ) ? $params['options'] : [],
+ isset( $params['join_conds'] ) ? $params['join_conds'] : []
+ ) );
+ $this->assertEquals( $expect, $sql );
+ }
+
+ public static function provideUnionConditionPermutations() {
+ return [
+ // @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong
+ [
+ [
+ 'table' => [ 'table1', 'table2' ],
+ 'vars' => [ 'field1', 'alias' => 'field2' ],
+ 'permute_conds' => [
+ 'field3' => [ 1, 2, 3 ],
+ 'duplicates' => [ 4, 5, 4 ],
+ 'empty' => [],
+ 'single' => [ 0 ],
+ ],
+ 'extra_conds' => 'table2.bar > 23',
+ 'options' => [
+ 'ORDER BY' => [ 'field1', 'alias' ],
+ 'INNER ORDER BY' => [ 'field1', 'field2' ],
+ 'LIMIT' => 100,
+ ],
+ 'join_conds' => [
+ 'table2' => [ 'JOIN', 'table1.foo_id = table2.foo_id' ],
+ ],
+ ],
+ "(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 " .
+ "(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 " .
+ "(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 " .
+ "(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 " .
+ "(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 " .
+ "(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 ) " .
+ "ORDER BY field1,alias LIMIT 100"
+ ],
+ [
+ [
+ 'table' => 'foo',
+ 'vars' => [ 'foo_id' ],
+ 'permute_conds' => [
+ 'bar' => [ 1, 2, 3 ],
+ ],
+ 'extra_conds' => [ 'baz' => null ],
+ 'options' => [
+ 'NOTALL',
+ 'ORDER BY' => [ 'foo_id' ],
+ 'LIMIT' => 25,
+ ],
+ ],
+ "(SELECT foo_id FROM foo WHERE bar = '1' AND baz IS NULL ORDER BY foo_id LIMIT 25 ) UNION " .
+ "(SELECT foo_id FROM foo WHERE bar = '2' AND baz IS NULL ORDER BY foo_id LIMIT 25 ) UNION " .
+ "(SELECT foo_id FROM foo WHERE bar = '3' AND baz IS NULL ORDER BY foo_id LIMIT 25 ) " .
+ "ORDER BY foo_id LIMIT 25"
+ ],
+ [
+ [
+ 'table' => 'foo',
+ 'vars' => [ 'foo_id' ],
+ 'permute_conds' => [
+ 'bar' => [ 1, 2, 3 ],
+ ],
+ 'extra_conds' => [ 'baz' => null ],
+ 'options' => [
+ 'NOTALL' => true,
+ 'ORDER BY' => [ 'foo_id' ],
+ 'LIMIT' => 25,
+ ],
+ 'unionSupportsOrderAndLimit' => false,
+ ],
+ "(SELECT foo_id FROM foo WHERE bar = '1' AND baz IS NULL ) UNION " .
+ "(SELECT foo_id FROM foo WHERE bar = '2' AND baz IS NULL ) UNION " .
+ "(SELECT foo_id FROM foo WHERE bar = '3' AND baz IS NULL ) " .
+ "ORDER BY foo_id LIMIT 25"
+ ],
+ [
+ [
+ 'table' => 'foo',
+ 'vars' => [ 'foo_id' ],
+ 'permute_conds' => [],
+ 'extra_conds' => [ 'baz' => null ],
+ 'options' => [
+ 'ORDER BY' => [ 'foo_id' ],
+ 'LIMIT' => 25,
+ ],
+ ],
+ "SELECT foo_id FROM foo WHERE baz IS NULL ORDER BY foo_id LIMIT 25"
+ ],
+ [
+ [
+ 'table' => 'foo',
+ 'vars' => [ 'foo_id' ],
+ 'permute_conds' => [
+ 'bar' => [],
+ ],
+ 'extra_conds' => [ 'baz' => null ],
+ 'options' => [
+ 'ORDER BY' => [ 'foo_id' ],
+ 'LIMIT' => 25,
+ ],
+ ],
+ "SELECT foo_id FROM foo WHERE baz IS NULL ORDER BY foo_id LIMIT 25"
+ ],
+ [
+ [
+ 'table' => 'foo',
+ 'vars' => [ 'foo_id' ],
+ 'permute_conds' => [
+ 'bar' => [ 1 ],
+ ],
+ 'options' => [
+ 'ORDER BY' => [ 'foo_id' ],
+ 'LIMIT' => 25,
+ 'OFFSET' => 150,
+ ],
+ ],
+ "SELECT foo_id FROM foo WHERE bar = '1' ORDER BY foo_id LIMIT 150,25"
+ ],
+ [
+ [
+ 'table' => 'foo',
+ 'vars' => [ 'foo_id' ],
+ 'permute_conds' => [],
+ 'extra_conds' => [ 'baz' => null ],
+ 'options' => [
+ 'ORDER BY' => [ 'foo_id' ],
+ 'LIMIT' => 25,
+ 'OFFSET' => 150,
+ 'INNER ORDER BY' => [ 'bar_id' ],
+ ],
+ ],
+ "(SELECT foo_id FROM foo WHERE baz IS NULL ORDER BY bar_id LIMIT 175 ) ORDER BY foo_id LIMIT 150,25"
+ ],
+ [
+ [
+ 'table' => 'foo',
+ 'vars' => [ 'foo_id' ],
+ 'permute_conds' => [],
+ 'extra_conds' => [ 'baz' => null ],
+ 'options' => [
+ 'ORDER BY' => [ 'foo_id' ],
+ 'LIMIT' => 25,
+ 'OFFSET' => 150,
+ 'INNER ORDER BY' => [ 'bar_id' ],
+ ],
+ 'unionSupportsOrderAndLimit' => false,
+ ],
+ "SELECT foo_id FROM foo WHERE baz IS NULL ORDER BY foo_id LIMIT 150,25"
+ ],
+ // @codingStandardsIgnoreEnd
+ ];
+ }
+
+ /**
+ * @covers Wikimedia\Rdbms\Database::commit
+ */
+ public function testTransactionCommit() {
+ $this->database->begin( __METHOD__ );
+ $this->database->commit( __METHOD__ );
+ $this->assertLastSql( 'BEGIN; COMMIT' );
+ }
+
+ /**
+ * @covers Wikimedia\Rdbms\Database::rollback
+ */
+ public function testTransactionRollback() {
+ $this->database->begin( __METHOD__ );
+ $this->database->rollback( __METHOD__ );
+ $this->assertLastSql( 'BEGIN; ROLLBACK' );
+ }
+
+ /**
+ * @covers Wikimedia\Rdbms\Database::dropTable
+ */
+ public function testDropTable() {
+ $this->database->setExistingTables( [ 'table' ] );
+ $this->database->dropTable( 'table', __METHOD__ );
+ $this->assertLastSql( 'DROP TABLE table CASCADE' );
+ }
+
+ /**
+ * @covers Wikimedia\Rdbms\Database::dropTable
+ */
+ public function testDropNonExistingTable() {
+ $this->assertFalse(
+ $this->database->dropTable( 'non_existing', __METHOD__ )
+ );
+ }
+
+ /**
+ * @dataProvider provideMakeList
+ * @covers Wikimedia\Rdbms\Database::makeList
+ */
+ public function testMakeList( $list, $mode, $sqlText ) {
+ $this->assertEquals( trim( $this->database->makeList(
+ $list, $mode
+ ) ), $sqlText );
+ }
+
+ public static function provideMakeList() {
+ return [
+ [
+ [ 'value', 'value2' ],
+ LIST_COMMA,
+ "'value','value2'"
+ ],
+ [
+ [ 'field', 'field2' ],
+ LIST_NAMES,
+ "field,field2"
+ ],
+ [
+ [ 'field' => 'value', 'field2' => 'value2' ],
+ LIST_AND,
+ "field = 'value' AND field2 = 'value2'"
+ ],
+ [
+ [ 'field' => null, "field2 != 'value2'" ],
+ LIST_AND,
+ "field IS NULL AND (field2 != 'value2')"
+ ],
+ [
+ [ 'field' => [ 'value', null, 'value2' ], 'field2' => 'value2' ],
+ LIST_AND,
+ "(field IN ('value','value2') OR field IS NULL) AND field2 = 'value2'"
+ ],
+ [
+ [ 'field' => [ null ], 'field2' => null ],
+ LIST_AND,
+ "field IS NULL AND field2 IS NULL"
+ ],
+ [
+ [ 'field' => 'value', 'field2' => 'value2' ],
+ LIST_OR,
+ "field = 'value' OR field2 = 'value2'"
+ ],
+ [
+ [ 'field' => 'value', 'field2' => null ],
+ LIST_OR,
+ "field = 'value' OR field2 IS NULL"
+ ],
+ [
+ [ 'field' => [ 'value', 'value2' ], 'field2' => [ 'value' ] ],
+ LIST_OR,
+ "field IN ('value','value2') OR field2 = 'value'"
+ ],
+ [
+ [ 'field' => [ null, 'value', null, 'value2' ], "field2 != 'value2'" ],
+ LIST_OR,
+ "(field IN ('value','value2') OR field IS NULL) OR (field2 != 'value2')"
+ ],
+ [
+ [ 'field' => 'value', 'field2' => 'value2' ],
+ LIST_SET,
+ "field = 'value',field2 = 'value2'"
+ ],
+ [
+ [ 'field' => 'value', 'field2' => null ],
+ LIST_SET,
+ "field = 'value',field2 = NULL"
+ ],
+ [
+ [ 'field' => 'value', "field2 != 'value2'" ],
+ LIST_SET,
+ "field = 'value',field2 != 'value2'"
+ ],
+ ];
+ }
+
+ public function testSessionTempTables() {
+ $temp1 = $this->database->tableName( 'tmp_table_1' );
+ $temp2 = $this->database->tableName( 'tmp_table_2' );
+ $temp3 = $this->database->tableName( 'tmp_table_3' );
+
+ $this->database->query( "CREATE TEMPORARY TABLE $temp1 LIKE orig_tbl", __METHOD__ );
+ $this->database->query( "CREATE TEMPORARY TABLE $temp2 LIKE orig_tbl", __METHOD__ );
+ $this->database->query( "CREATE TEMPORARY TABLE $temp3 LIKE orig_tbl", __METHOD__ );
+
+ $this->assertTrue( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
+ $this->assertTrue( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
+ $this->assertTrue( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
+
+ $this->database->dropTable( 'tmp_table_1', __METHOD__ );
+ $this->database->dropTable( 'tmp_table_2', __METHOD__ );
+ $this->database->dropTable( 'tmp_table_3', __METHOD__ );
+
+ $this->assertFalse( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
+ $this->assertFalse( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
+ $this->assertFalse( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
+
+ $this->database->query( "CREATE TEMPORARY TABLE tmp_table_1 LIKE orig_tbl", __METHOD__ );
+ $this->database->query( "CREATE TEMPORARY TABLE 'tmp_table_2' LIKE orig_tbl", __METHOD__ );
+ $this->database->query( "CREATE TEMPORARY TABLE `tmp_table_3` LIKE orig_tbl", __METHOD__ );
+
+ $this->assertTrue( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
+ $this->assertTrue( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
+ $this->assertTrue( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
+
+ $this->database->query( "DROP TEMPORARY TABLE tmp_table_1 LIKE orig_tbl", __METHOD__ );
+ $this->database->query( "DROP TEMPORARY TABLE 'tmp_table_2' LIKE orig_tbl", __METHOD__ );
+ $this->database->query( "DROP TABLE `tmp_table_3` LIKE orig_tbl", __METHOD__ );
+
+ $this->assertFalse( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
+ $this->assertFalse( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
+ $this->assertFalse( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
+ }
+}
--- /dev/null
+<?php
+
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\TransactionProfiler;
+use Wikimedia\TestingAccessWrapper;
+
+class DatabaseTest extends PHPUnit_Framework_TestCase {
+
+ protected function setUp() {
+ $this->db = new DatabaseTestHelper( __CLASS__ . '::' . $this->getName() );
+ }
+
+ public static function provideAddQuotes() {
+ return [
+ [ null, 'NULL' ],
+ [ 1234, "'1234'" ],
+ [ 1234.5678, "'1234.5678'" ],
+ [ 'string', "'string'" ],
+ [ 'string\'s cause trouble', "'string\'s cause trouble'" ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideAddQuotes
+ * @covers Wikimedia\Rdbms\Database::addQuotes
+ */
+ public function testAddQuotes( $input, $expected ) {
+ $this->assertEquals( $expected, $this->db->addQuotes( $input ) );
+ }
+
+ public static function provideTableName() {
+ // Formatting is mostly ignored since addIdentifierQuotes is abstract.
+ // For testing of addIdentifierQuotes, see actual Database subclas tests.
+ return [
+ 'local' => [
+ 'tablename',
+ 'tablename',
+ 'quoted',
+ ],
+ 'local-raw' => [
+ 'tablename',
+ 'tablename',
+ 'raw',
+ ],
+ 'shared' => [
+ 'sharedb.tablename',
+ 'tablename',
+ 'quoted',
+ [ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => '' ],
+ ],
+ 'shared-raw' => [
+ 'sharedb.tablename',
+ 'tablename',
+ 'raw',
+ [ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => '' ],
+ ],
+ 'shared-prefix' => [
+ 'sharedb.sh_tablename',
+ 'tablename',
+ 'quoted',
+ [ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => 'sh_' ],
+ ],
+ 'shared-prefix-raw' => [
+ 'sharedb.sh_tablename',
+ 'tablename',
+ 'raw',
+ [ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => 'sh_' ],
+ ],
+ 'foreign' => [
+ 'databasename.tablename',
+ 'databasename.tablename',
+ 'quoted',
+ ],
+ 'foreign-raw' => [
+ 'databasename.tablename',
+ 'databasename.tablename',
+ 'raw',
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideTableName
+ * @covers Wikimedia\Rdbms\Database::tableName
+ */
+ public function testTableName( $expected, $table, $format, array $alias = null ) {
+ if ( $alias ) {
+ $this->db->setTableAliases( [ $table => $alias ] );
+ }
+ $this->assertEquals(
+ $expected,
+ $this->db->tableName( $table, $format ?: 'quoted' )
+ );
+ }
+
+ /**
+ * @covers Wikimedia\Rdbms\Database::onTransactionIdle
+ * @covers Wikimedia\Rdbms\Database::runOnTransactionIdleCallbacks
+ */
+ public function testTransactionIdle() {
+ $db = $this->db;
+
+ $db->setFlag( DBO_TRX );
+ $called = false;
+ $flagSet = null;
+ $db->onTransactionIdle(
+ function () use ( $db, &$flagSet, &$called ) {
+ $called = true;
+ $flagSet = $db->getFlag( DBO_TRX );
+ },
+ __METHOD__
+ );
+ $this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
+ $this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
+ $this->assertTrue( $called, 'Callback reached' );
+
+ $db->clearFlag( DBO_TRX );
+ $flagSet = null;
+ $db->onTransactionIdle(
+ function () use ( $db, &$flagSet ) {
+ $flagSet = $db->getFlag( DBO_TRX );
+ },
+ __METHOD__
+ );
+ $this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
+ $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
+
+ $db->clearFlag( DBO_TRX );
+ $db->onTransactionIdle(
+ function () use ( $db ) {
+ $db->setFlag( DBO_TRX );
+ },
+ __METHOD__
+ );
+ $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
+ }
+
+ /**
+ * @covers Wikimedia\Rdbms\Database::onTransactionResolution
+ * @covers Wikimedia\Rdbms\Database::runOnTransactionIdleCallbacks
+ */
+ public function testTransactionResolution() {
+ $db = $this->db;
+
+ $db->clearFlag( DBO_TRX );
+ $db->begin( __METHOD__ );
+ $called = false;
+ $db->onTransactionResolution( function () use ( $db, &$called ) {
+ $called = true;
+ $db->setFlag( DBO_TRX );
+ } );
+ $db->commit( __METHOD__ );
+ $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
+ $this->assertTrue( $called, 'Callback reached' );
+
+ $db->clearFlag( DBO_TRX );
+ $db->begin( __METHOD__ );
+ $called = false;
+ $db->onTransactionResolution( function () use ( $db, &$called ) {
+ $called = true;
+ $db->setFlag( DBO_TRX );
+ } );
+ $db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
+ $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
+ $this->assertTrue( $called, 'Callback reached' );
+ }
+
+ /**
+ * @covers Wikimedia\Rdbms\Database::setTransactionListener
+ */
+ public function testTransactionListener() {
+ $db = $this->db;
+
+ $db->setTransactionListener( 'ping', function () use ( $db, &$called ) {
+ $called = true;
+ } );
+
+ $called = false;
+ $db->begin( __METHOD__ );
+ $db->commit( __METHOD__ );
+ $this->assertTrue( $called, 'Callback reached' );
+
+ $called = false;
+ $db->begin( __METHOD__ );
+ $db->commit( __METHOD__ );
+ $this->assertTrue( $called, 'Callback still reached' );
+
+ $called = false;
+ $db->begin( __METHOD__ );
+ $db->rollback( __METHOD__ );
+ $this->assertTrue( $called, 'Callback reached' );
+
+ $db->setTransactionListener( 'ping', null );
+ $called = false;
+ $db->begin( __METHOD__ );
+ $db->commit( __METHOD__ );
+ $this->assertFalse( $called, 'Callback not reached' );
+ }
+
+ /**
+ * Use this mock instead of DatabaseTestHelper for cases where
+ * DatabaseTestHelper is too inflexibile due to mocking too much
+ * or being too restrictive about fname matching (e.g. for tests
+ * that assert behaviour when the name is a mismatch, we need to
+ * catch the error here instead of there).
+ *
+ * @return Database
+ */
+ private function getMockDB( $methods = [] ) {
+ static $abstractMethods = [
+ 'affectedRows',
+ 'closeConnection',
+ 'dataSeek',
+ 'doQuery',
+ 'fetchObject', 'fetchRow',
+ 'fieldInfo', 'fieldName',
+ 'getSoftwareLink', 'getServerVersion',
+ 'getType',
+ 'indexInfo',
+ 'insertId',
+ 'lastError', 'lastErrno',
+ 'numFields', 'numRows',
+ 'open',
+ 'strencode',
+ ];
+ $db = $this->getMockBuilder( Database::class )
+ ->disableOriginalConstructor()
+ ->setMethods( array_values( array_unique( array_merge(
+ $abstractMethods,
+ $methods
+ ) ) ) )
+ ->getMock();
+ $wdb = TestingAccessWrapper::newFromObject( $db );
+ $wdb->trxProfiler = new TransactionProfiler();
+ $wdb->connLogger = new \Psr\Log\NullLogger();
+ $wdb->queryLogger = new \Psr\Log\NullLogger();
+ return $db;
+ }
+
+ /**
+ * @covers Wikimedia\Rdbms\Database::flushSnapshot
+ */
+ public function testFlushSnapshot() {
+ $db = $this->getMockDB( [ 'isOpen' ] );
+ $db->method( 'isOpen' )->willReturn( true );
+
+ $db->flushSnapshot( __METHOD__ ); // ok
+ $db->flushSnapshot( __METHOD__ ); // ok
+
+ $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
+ $db->query( 'SELECT 1', __METHOD__ );
+ $this->assertTrue( (bool)$db->trxLevel(), "Transaction started." );
+ $db->flushSnapshot( __METHOD__ ); // ok
+ $db->restoreFlags( $db::RESTORE_PRIOR );
+
+ $this->assertFalse( (bool)$db->trxLevel(), "Transaction cleared." );
+ }
+
+ public function testGetScopedLock() {
+ $db = $this->getMockDB( [ 'isOpen' ] );
+ $db->method( 'isOpen' )->willReturn( true );
+
+ $db->setFlag( DBO_TRX );
+ try {
+ $this->badLockingMethodImplicit( $db );
+ } catch ( RunTimeException $e ) {
+ $this->assertTrue( $db->trxLevel() > 0, "Transaction not committed." );
+ }
+ $db->clearFlag( DBO_TRX );
+ $db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
+ $this->assertTrue( $db->lockIsFree( 'meow', __METHOD__ ) );
+
+ try {
+ $this->badLockingMethodExplicit( $db );
+ } catch ( RunTimeException $e ) {
+ $this->assertTrue( $db->trxLevel() > 0, "Transaction not committed." );
+ }
+ $db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
+ $this->assertTrue( $db->lockIsFree( 'meow', __METHOD__ ) );
+ }
+
+ private function badLockingMethodImplicit( IDatabase $db ) {
+ $lock = $db->getScopedLockAndFlush( 'meow', __METHOD__, 1 );
+ $db->query( "SELECT 1" ); // trigger DBO_TRX
+ throw new RunTimeException( "Uh oh!" );
+ }
+
+ private function badLockingMethodExplicit( IDatabase $db ) {
+ $lock = $db->getScopedLockAndFlush( 'meow', __METHOD__, 1 );
+ $db->begin( __METHOD__ );
+ throw new RunTimeException( "Uh oh!" );
+ }
+
+ /**
+ * @covers Wikimedia\Rdbms\Database::getFlag
+ * @covers Wikimedia\Rdbms\Database::setFlag
+ * @covers Wikimedia\Rdbms\Database::restoreFlags
+ */
+ public function testFlagSetting() {
+ $db = $this->db;
+ $origTrx = $db->getFlag( DBO_TRX );
+ $origSsl = $db->getFlag( DBO_SSL );
+
+ $origTrx
+ ? $db->clearFlag( DBO_TRX, $db::REMEMBER_PRIOR )
+ : $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
+ $this->assertEquals( !$origTrx, $db->getFlag( DBO_TRX ) );
+
+ $origSsl
+ ? $db->clearFlag( DBO_SSL, $db::REMEMBER_PRIOR )
+ : $db->setFlag( DBO_SSL, $db::REMEMBER_PRIOR );
+ $this->assertEquals( !$origSsl, $db->getFlag( DBO_SSL ) );
+
+ $db->restoreFlags( $db::RESTORE_INITIAL );
+ $this->assertEquals( $origTrx, $db->getFlag( DBO_TRX ) );
+ $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
+
+ $origTrx
+ ? $db->clearFlag( DBO_TRX, $db::REMEMBER_PRIOR )
+ : $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
+ $origSsl
+ ? $db->clearFlag( DBO_SSL, $db::REMEMBER_PRIOR )
+ : $db->setFlag( DBO_SSL, $db::REMEMBER_PRIOR );
+
+ $db->restoreFlags();
+ $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
+ $this->assertEquals( !$origTrx, $db->getFlag( DBO_TRX ) );
+
+ $db->restoreFlags();
+ $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
+ $this->assertEquals( $origTrx, $db->getFlag( DBO_TRX ) );
+ }
+
+ /**
+ * @covers Wikimedia\Rdbms\Database::tablePrefix
+ * @covers Wikimedia\Rdbms\Database::dbSchema
+ */
+ public function testMutators() {
+ $old = $this->db->tablePrefix();
+ $this->assertInternalType( 'string', $old, 'Prefix is string' );
+ $this->assertEquals( $old, $this->db->tablePrefix(), "Prefix unchanged" );
+ $this->assertEquals( $old, $this->db->tablePrefix( 'xxx' ) );
+ $this->assertEquals( 'xxx', $this->db->tablePrefix(), "Prefix set" );
+ $this->db->tablePrefix( $old );
+ $this->assertNotEquals( 'xxx', $this->db->tablePrefix() );
+
+ $old = $this->db->dbSchema();
+ $this->assertInternalType( 'string', $old, 'Schema is string' );
+ $this->assertEquals( $old, $this->db->dbSchema(), "Schema unchanged" );
+ $this->assertEquals( $old, $this->db->dbSchema( 'xxx' ) );
+ $this->assertEquals( 'xxx', $this->db->dbSchema(), "Schema set" );
+ $this->db->dbSchema( $old );
+ $this->assertNotEquals( 'xxx', $this->db->dbSchema() );
+ }
+}
--- /dev/null
+<?php
+
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\Database;
+
+/**
+ * @group Database
+ */
+class DatabaseIntegrationTest extends MediaWikiTestCase {
+ /**
+ * @var Database
+ */
+ protected $db;
+
+ private $functionTest = false;
+
+ protected function setUp() {
+ parent::setUp();
+ $this->db = wfGetDB( DB_MASTER );
+ }
+
+ protected function tearDown() {
+ parent::tearDown();
+ if ( $this->functionTest ) {
+ $this->dropFunctions();
+ $this->functionTest = false;
+ }
+ $this->db->restoreFlags( IDatabase::RESTORE_INITIAL );
+ }
+
+ public function testStoredFunctions() {
+ if ( !in_array( wfGetDB( DB_MASTER )->getType(), [ 'mysql', 'postgres' ] ) ) {
+ $this->markTestSkipped( 'MySQL or Postgres required' );
+ }
+ global $IP;
+ $this->dropFunctions();
+ $this->functionTest = true;
+ $this->assertTrue(
+ $this->db->sourceFile( "$IP/tests/phpunit/data/db/{$this->db->getType()}/functions.sql" )
+ );
+ $res = $this->db->query( 'SELECT mw_test_function() AS test', __METHOD__ );
+ $this->assertEquals( 42, $res->fetchObject()->test );
+ }
+
+ private function dropFunctions() {
+ $this->db->query( 'DROP FUNCTION IF EXISTS mw_test_function'
+ . ( $this->db->getType() == 'postgres' ? '()' : '' )
+ );
+ }
+
+ public function testUnknownTableCorruptsResults() {
+ $res = $this->db->select( 'page', '*', [ 'page_id' => 1 ] );
+ $this->assertFalse( $this->db->tableExists( 'foobarbaz' ) );
+ $this->assertInternalType( 'int', $res->numRows() );
+ }
+}