X-Git-Url: http://git.heureux-cyclage.org/?a=blobdiff_plain;f=tests%2Fphpunit%2Fincludes%2Flibs%2Frdbms%2Fdatabase%2FDatabaseSQLTest.php;h=badba9616f47a04565ba4b6135a234a1e79d3dfb;hb=52aeaa7a5;hp=184d6260f81d8b127d72cd17694ce048c13f8050;hpb=2b11f88e250156f105338f5a8876133f55b2d831;p=lhc%2Fweb%2Fwiklou.git diff --git a/tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php b/tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php index 184d6260f8..badba9616f 100644 --- a/tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php +++ b/tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php @@ -1,6 +1,7 @@ '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' => [], + ], + "SELECT field,field2 AS alias " . + "FROM table" + ], + [ + [ + 'tables' => 'table', + 'fields' => [ 'field', 'alias' => 'field2' ], + 'conds' => '', + ], + "SELECT field,field2 AS alias " . + "FROM table" + ], + [ + [ + 'tables' => 'table', + 'fields' => [ 'field', 'alias' => 'field2' ], + 'conds' => '0', // T188314 + ], + "SELECT field,field2 AS alias " . + "FROM table " . + "WHERE 0" + ], [ [ // 'tables' with space prepended indicates pre-escaped table name @@ -201,6 +240,101 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase { ]; } + /** + * @covers Wikimedia\Rdbms\Subquery + * @dataProvider provideSelectRowCount + * @param $sql + * @param $sqlText + */ + public function testSelectRowCount( $sql, $sqlText ) { + $this->database->selectRowCount( + $sql['tables'], + $sql['field'], + isset( $sql['conds'] ) ? $sql['conds'] : [], + __METHOD__, + isset( $sql['options'] ) ? $sql['options'] : [], + isset( $sql['join_conds'] ) ? $sql['join_conds'] : [] + ); + $this->assertLastSql( $sqlText ); + } + + public static function provideSelectRowCount() { + return [ + [ + [ + 'tables' => 'table', + 'field' => [ '*' ], + 'conds' => [ 'field' => 'text' ], + ], + "SELECT COUNT(*) AS rowcount FROM " . + "(SELECT 1 FROM table WHERE field = 'text' ) tmp_count" + ], + [ + [ + 'tables' => 'table', + 'field' => [ 'column' ], + 'conds' => [ 'field' => 'text' ], + ], + "SELECT COUNT(*) AS rowcount FROM " . + "(SELECT 1 FROM table WHERE field = 'text' AND (column IS NOT NULL) ) tmp_count" + ], + [ + [ + 'tables' => 'table', + 'field' => [ 'alias' => 'column' ], + 'conds' => [ 'field' => 'text' ], + ], + "SELECT COUNT(*) AS rowcount FROM " . + "(SELECT 1 FROM table WHERE field = 'text' AND (column IS NOT NULL) ) tmp_count" + ], + [ + [ + 'tables' => 'table', + 'field' => [ 'alias' => 'column' ], + 'conds' => '', + ], + "SELECT COUNT(*) AS rowcount FROM " . + "(SELECT 1 FROM table WHERE (column IS NOT NULL) ) tmp_count" + ], + [ + [ + 'tables' => 'table', + 'field' => [ 'alias' => 'column' ], + 'conds' => false, + ], + "SELECT COUNT(*) AS rowcount FROM " . + "(SELECT 1 FROM table WHERE (column IS NOT NULL) ) tmp_count" + ], + [ + [ + 'tables' => 'table', + 'field' => [ 'alias' => 'column' ], + 'conds' => null, + ], + "SELECT COUNT(*) AS rowcount FROM " . + "(SELECT 1 FROM table WHERE (column IS NOT NULL) ) tmp_count" + ], + [ + [ + 'tables' => 'table', + 'field' => [ 'alias' => 'column' ], + 'conds' => '1', + ], + "SELECT COUNT(*) AS rowcount FROM " . + "(SELECT 1 FROM table WHERE (1) AND (column IS NOT NULL) ) tmp_count" + ], + [ + [ + 'tables' => 'table', + 'field' => [ 'alias' => 'column' ], + 'conds' => '0', + ], + "SELECT COUNT(*) AS rowcount FROM " . + "(SELECT 1 FROM table WHERE (0) AND (column IS NOT NULL) ) tmp_count" + ], + ]; + } + /** * @dataProvider provideUpdate * @covers Wikimedia\Rdbms\Database::update @@ -457,7 +591,7 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase { isset( $sql['selectOptions'] ) ? $sql['selectOptions'] : [], isset( $sql['selectJoinConds'] ) ? $sql['selectJoinConds'] : [] ); - $this->assertLastSqlDb( implode( '; ', [ $sqlSelect, $sqlInsert ] ), $dbWeb ); + $this->assertLastSqlDb( implode( '; ', [ $sqlSelect, 'BEGIN', $sqlInsert, 'COMMIT' ] ), $dbWeb ); } public static function provideInsertSelect() { @@ -518,6 +652,7 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase { 'srcTable' => [ 'select_table1', 'select_table2' ], 'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ], 'conds' => [ 'field' => 2 ], + 'insertOptions' => [ 'NO_AUTO_COLUMNS' ], 'selectOptions' => [ 'ORDER BY' => 'field', 'FORCE INDEX' => [ 'select_table1' => 'index1' ] ], 'selectJoinConds' => [ 'select_table2' => [ 'LEFT JOIN', [ 'select_table1.foo = select_table2.bar' ] ], @@ -537,6 +672,30 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase { ]; } + public function testInsertSelectBatching() { + $dbWeb = new DatabaseTestHelper( __CLASS__, [ 'cliMode' => false ] ); + $rows = []; + for ( $i = 0; $i <= 25000; $i++ ) { + $rows[] = [ 'field' => $i ]; + } + $dbWeb->forceNextResult( $rows ); + $dbWeb->insertSelect( + 'insert_table', + 'select_table', + [ 'field' => 'field2' ], + '*', + __METHOD__ + ); + $this->assertLastSqlDb( implode( '; ', [ + 'SELECT field2 AS field FROM select_table WHERE * FOR UPDATE', + 'BEGIN', + "INSERT INTO insert_table (field) VALUES ('" . implode( "'),('", range( 0, 9999 ) ) . "')", + "INSERT INTO insert_table (field) VALUES ('" . implode( "'),('", range( 10000, 19999 ) ) . "')", + "INSERT INTO insert_table (field) VALUES ('" . implode( "'),('", range( 20000, 25000 ) ) . "')", + 'COMMIT' + ] ), $dbWeb ); + } + /** * @dataProvider provideReplace * @covers Wikimedia\Rdbms\Database::replace @@ -559,11 +718,11 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase { 'uniqueIndexes' => [ 'field' ], 'rows' => [ 'field' => 'text', 'field2' => 'text2' ], ], - "DELETE FROM replace_table " . + "BEGIN; DELETE FROM replace_table " . "WHERE (field = 'text'); " . "INSERT INTO replace_table " . "(field,field2) " . - "VALUES ('text','text2')" + "VALUES ('text','text2'); COMMIT" ], [ [ @@ -575,11 +734,11 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase { 'md_deps' => 'deps', ], ], - "DELETE FROM module_deps " . + "BEGIN; 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')" + "VALUES ('module','skin','deps'); COMMIT" ], [ [ @@ -597,7 +756,7 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase { ], ], ], - "DELETE FROM module_deps " . + "BEGIN; DELETE FROM module_deps " . "WHERE (md_module = 'module' AND md_skin = 'skin'); " . "INSERT INTO module_deps " . "(md_module,md_skin,md_deps) " . @@ -606,7 +765,7 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase { "WHERE (md_module = 'module2' AND md_skin = 'skin2'); " . "INSERT INTO module_deps " . "(md_module,md_skin,md_deps) " . - "VALUES ('module2','skin2','deps2')" + "VALUES ('module2','skin2','deps2'); COMMIT" ], [ [ @@ -624,7 +783,7 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase { ], ], ], - "DELETE FROM module_deps " . + "BEGIN; DELETE FROM module_deps " . "WHERE (md_module = 'module') OR (md_skin = 'skin'); " . "INSERT INTO module_deps " . "(md_module,md_skin,md_deps) " . @@ -633,7 +792,7 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase { "WHERE (md_module = 'module2') OR (md_skin = 'skin2'); " . "INSERT INTO module_deps " . "(md_module,md_skin,md_deps) " . - "VALUES ('module2','skin2','deps2')" + "VALUES ('module2','skin2','deps2'); COMMIT" ], [ [ @@ -645,9 +804,9 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase { 'md_deps' => 'deps', ], ], - "INSERT INTO module_deps " . + "BEGIN; INSERT INTO module_deps " . "(md_module,md_skin,md_deps) " . - "VALUES ('module','skin','deps')" + "VALUES ('module','skin','deps'); COMMIT" ], ]; } @@ -1151,4 +1310,152 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase { $this->assertFalse( $this->database->tableExists( "tmp_table_2", __METHOD__ ) ); $this->assertFalse( $this->database->tableExists( "tmp_table_3", __METHOD__ ) ); } + + public function provideBuildSubstring() { + yield [ 'someField', 1, 2, 'SUBSTRING(someField FROM 1 FOR 2)' ]; + yield [ 'someField', 1, null, 'SUBSTRING(someField FROM 1)' ]; + } + + /** + * @covers Wikimedia\Rdbms\Database::buildSubstring + * @dataProvider provideBuildSubstring + */ + public function testBuildSubstring( $input, $start, $length, $expected ) { + $output = $this->database->buildSubstring( $input, $start, $length ); + $this->assertSame( $expected, $output ); + } + + public function provideBuildSubstring_invalidParams() { + yield [ -1, 1 ]; + yield [ 1, -1 ]; + yield [ 1, 'foo' ]; + yield [ 'foo', 1 ]; + yield [ null, 1 ]; + yield [ 0, 1 ]; + } + + /** + * @covers Wikimedia\Rdbms\Database::buildSubstring + * @covers Wikimedia\Rdbms\Database::assertBuildSubstringParams + * @dataProvider provideBuildSubstring_invalidParams + */ + public function testBuildSubstring_invalidParams( $start, $length ) { + $this->setExpectedException( InvalidArgumentException::class ); + $this->database->buildSubstring( 'foo', $start, $length ); + } + + /** + * @covers \Wikimedia\Rdbms\Database::buildIntegerCast + */ + public function testBuildIntegerCast() { + $output = $this->database->buildIntegerCast( 'fieldName' ); + $this->assertSame( 'CAST( fieldName AS INTEGER )', $output ); + } + + /** + * @covers \Wikimedia\Rdbms\Database::doSavepoint + * @covers \Wikimedia\Rdbms\Database::doReleaseSavepoint + * @covers \Wikimedia\Rdbms\Database::doRollbackToSavepoint + * @covers \Wikimedia\Rdbms\Database::startAtomic + * @covers \Wikimedia\Rdbms\Database::endAtomic + * @covers \Wikimedia\Rdbms\Database::cancelAtomic + * @covers \Wikimedia\Rdbms\Database::doAtomicSection + */ + public function testAtomicSections() { + $this->database->startAtomic( __METHOD__ ); + $this->database->endAtomic( __METHOD__ ); + $this->assertLastSql( 'BEGIN; COMMIT' ); + + $this->database->startAtomic( __METHOD__ ); + $this->database->cancelAtomic( __METHOD__ ); + $this->assertLastSql( 'BEGIN; ROLLBACK' ); + + $this->database->begin( __METHOD__ ); + $this->database->startAtomic( __METHOD__ ); + $this->database->endAtomic( __METHOD__ ); + $this->database->commit( __METHOD__ ); + // phpcs:ignore Generic.Files.LineLength + $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; RELEASE SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' ); + + $this->database->begin( __METHOD__ ); + $this->database->startAtomic( __METHOD__ ); + $this->database->cancelAtomic( __METHOD__ ); + $this->database->commit( __METHOD__ ); + // phpcs:ignore Generic.Files.LineLength + $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' ); + + $this->database->startAtomic( __METHOD__ ); + $this->database->startAtomic( __METHOD__ ); + $this->database->cancelAtomic( __METHOD__ ); + $this->database->endAtomic( __METHOD__ ); + // phpcs:ignore Generic.Files.LineLength + $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' ); + + $this->database->doAtomicSection( __METHOD__, function () { + } ); + $this->assertLastSql( 'BEGIN; COMMIT' ); + + $this->database->begin( __METHOD__ ); + $this->database->doAtomicSection( __METHOD__, function () { + } ); + $this->database->rollback( __METHOD__ ); + // phpcs:ignore Generic.Files.LineLength + $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; RELEASE SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK' ); + + $this->database->begin( __METHOD__ ); + try { + $this->database->doAtomicSection( __METHOD__, function () { + throw new RuntimeException( 'Test exception' ); + } ); + $this->fail( 'Expected exception not thrown' ); + } catch ( RuntimeException $ex ) { + $this->assertSame( 'Test exception', $ex->getMessage() ); + } + $this->database->commit( __METHOD__ ); + // phpcs:ignore Generic.Files.LineLength + $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' ); + } + + public static function provideAtomicSectionMethodsForErrors() { + return [ + [ 'endAtomic' ], + [ 'cancelAtomic' ], + ]; + } + + /** + * @dataProvider provideAtomicSectionMethodsForErrors + * @covers \Wikimedia\Rdbms\Database::endAtomic + * @covers \Wikimedia\Rdbms\Database::cancelAtomic + */ + public function testNoAtomicSection( $method ) { + try { + $this->database->$method( __METHOD__ ); + $this->fail( 'Expected exception not thrown' ); + } catch ( DBUnexpectedError $ex ) { + $this->assertSame( + 'No atomic transaction is open (got ' . __METHOD__ . ').', + $ex->getMessage() + ); + } + } + + /** + * @dataProvider provideAtomicSectionMethodsForErrors + * @covers \Wikimedia\Rdbms\Database::endAtomic + * @covers \Wikimedia\Rdbms\Database::cancelAtomic + */ + public function testInvalidAtomicSectionEnded( $method ) { + $this->database->startAtomic( __METHOD__ . 'X' ); + try { + $this->database->$method( __METHOD__ ); + $this->fail( 'Expected exception not thrown' ); + } catch ( DBUnexpectedError $ex ) { + $this->assertSame( + 'Invalid atomic section ended (got ' . __METHOD__ . ').', + $ex->getMessage() + ); + } + } + }