<?php
+use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\LikeMatch;
+use Wikimedia\Rdbms\Database;
/**
* Test the parts of the Database abstract class that deal
use MediaWikiCoversValidator;
- /** @var DatabaseTestHelper */
+ /** @var DatabaseTestHelper|Database */
private $database;
protected function setUp() {
"FROM table " .
"WHERE alias = 'text'"
],
+ [
+ [
+ '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' => [],
+ ],
+ "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
];
}
+ /**
+ * @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
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() {
'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' ] ],
];
}
+ 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
$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__, IDatabase::ATOMIC_CANCELABLE );
+ $this->database->cancelAtomic( __METHOD__ );
+ $this->assertLastSql( 'BEGIN; ROLLBACK' );
+
+ $this->database->begin( __METHOD__ );
+ $this->database->startAtomic( __METHOD__ );
+ $this->database->endAtomic( __METHOD__ );
+ $this->database->commit( __METHOD__ );
+ $this->assertLastSql( 'BEGIN; COMMIT' );
+
+ $this->database->begin( __METHOD__ );
+ $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+ $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__, IDatabase::ATOMIC_CANCELABLE );
+ $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__, IDatabase::ATOMIC_CANCELABLE );
+ $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+ $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()
+ );
+ }
+ }
+
+ /**
+ * @covers \Wikimedia\Rdbms\Database::cancelAtomic
+ */
+ public function testUncancellableAtomicSection() {
+ $this->database->startAtomic( __METHOD__ );
+ try {
+ $this->database->cancelAtomic( __METHOD__ );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( DBUnexpectedError $ex ) {
+ $this->assertSame(
+ 'Uncancelable atomic section canceled (got ' . __METHOD__ . ').',
+ $ex->getMessage()
+ );
+ }
+ }
+
}