Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4107,6 +4107,7 @@ private function execute_set_user_variable_statement(
private function execute_administration_statement( WP_Parser_Node $node ): void {
$first_token = $node->get_first_child_token();
$table_ref_list = $node->get_first_child_node( 'tableRefList' );
$savepoint_name = $this->get_internal_savepoint_name( 'table_administration' );
$results = array();
foreach ( $table_ref_list->get_child_nodes( 'tableRef' ) as $table_ref ) {
$database = $this->get_database_name( $table_ref );
Expand All @@ -4116,6 +4117,9 @@ private function execute_administration_statement( WP_Parser_Node $node ): void

$table_name = $this->unquote_sqlite_identifier( $this->translate( $table_ref ) );
$quoted_table_name = $this->quote_sqlite_identifier( $table_name );

// Each table must be atomic because failures are reported as result rows.
$this->execute_sqlite_query( sprintf( 'SAVEPOINT %s', $savepoint_name ) );
try {
switch ( $first_token->id ) {
case WP_MySQL_Lexer::ANALYZE_SYMBOL:
Expand Down Expand Up @@ -4153,6 +4157,7 @@ private function execute_administration_statement( WP_Parser_Node $node ): void
);
}
} catch ( PDOException $e ) {
$this->execute_sqlite_query( sprintf( 'ROLLBACK TO SAVEPOINT %s', $savepoint_name ) );
if ( '42S02' === $e->getCode() && isset( $e->errorInfo[2] ) ) {
$errors = array( $e->errorInfo[2] );
} elseif (
Expand All @@ -4165,6 +4170,7 @@ private function execute_administration_statement( WP_Parser_Node $node ): void
$errors = array( $e->getMessage() );
}
}
$this->execute_sqlite_query( sprintf( 'RELEASE SAVEPOINT %s', $savepoint_name ) );

$operation = strtolower( $first_token->get_value() );
foreach ( $errors as $error ) {
Expand Down
151 changes: 145 additions & 6 deletions packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Metadata_Tests.php
Original file line number Diff line number Diff line change
Expand Up @@ -474,31 +474,40 @@ public function testOptimizeTable() {
);
}

public function testOptimizeTableWithFailedRecreation() {
$this->assertQuery( 'CREATE TABLE t (id INT, name TEXT)' );
/**
* @dataProvider data_rebuild_operations_and_table_types
*/
public function testAdministrationWithFailedRecreation( string $operation, bool $temporary ): void {
$this->assertQuery( 'CREATE ' . ( $temporary ? 'TEMPORARY ' : '' ) . 'TABLE t (id INT, name TEXT)' );
$this->assertQuery( "INSERT INTO t VALUES (1, 'a'), (2, 'b')" );

// Make recreating the table fail, as its information schema names a column that doesn't exist.
$this->sqlite->exec( "UPDATE _wp_sqlite_mysql_information_schema_columns SET column_name = 'title' WHERE table_name = 't' AND column_name = 'name'" );
$columns_table = $temporary ? '_wp_sqlite_mysql_information_schema_tmp_columns' : '_wp_sqlite_mysql_information_schema_columns';
$this->sqlite->exec( "UPDATE $columns_table SET column_name = 'title' WHERE table_name = 't' AND column_name = 'name'" );
$schema = $this->getSqliteSchema();
$data = $this->sqlite->query( 'SELECT rowid, * FROM t ORDER BY rowid' )->fetchAll( PDO::FETCH_ASSOC );

// The error from SQLite is reported, not a missing table.
$result = $this->assertQuery( 'OPTIMIZE TABLE t' );
$result = $this->assertQuery( "$operation TABLE t" );
$this->assertEquals(
array(
(object) array(
'Table' => 'wp.t',
'Op' => 'optimize',
'Op' => strtolower( $operation ),
'Msg_type' => 'Error',
'Msg_text' => 'SQLSTATE[HY000]: General error: 1 no such column: title',
),
(object) array(
'Table' => 'wp.t',
'Op' => 'optimize',
'Op' => strtolower( $operation ),
'Msg_type' => 'status',
'Msg_text' => 'Operation failed',
),
),
$result
);
$this->assertSame( $schema, $this->getSqliteSchema() );
$this->assertSame( $data, $this->sqlite->query( 'SELECT rowid, * FROM t ORDER BY rowid' )->fetchAll( PDO::FETCH_ASSOC ) );
}

public function testRepairTable() {
Expand Down Expand Up @@ -586,6 +595,127 @@ public function testRepairTable() {
);
}

/**
* @dataProvider data_rebuild_operations
*/
public function testAdministrationRollsBackFailedDrop( string $operation ): void {
$this->assertQuery( 'CREATE TABLE parent (id INT PRIMARY KEY, name TEXT)' );
$this->assertQuery( 'CREATE TABLE child (id INT PRIMARY KEY, parent_id INT, FOREIGN KEY (parent_id) REFERENCES parent (id))' );
$this->assertQuery( "INSERT INTO parent VALUES (1, 'a'), (2, 'b')" );
$this->assertQuery( 'INSERT INTO child VALUES (1, 1)' );

$schema = $this->getSqliteSchema();
$parent = $this->sqlite->query( 'SELECT * FROM parent ORDER BY id' )->fetchAll( PDO::FETCH_ASSOC );
$child = $this->sqlite->query( 'SELECT * FROM child ORDER BY id' )->fetchAll( PDO::FETCH_ASSOC );

// Repeated failures must not leave copies of the parent table behind.
for ( $i = 0; $i < 2; ++$i ) {
$result = $this->assertQuery( "$operation TABLE parent" );
$this->assertCount( 2, $result );
$this->assertStringContainsString( 'FOREIGN KEY constraint failed', $result[0]->Msg_text );
$this->assertSame( 'Operation failed', $result[1]->Msg_text );
$this->assertSame( $schema, $this->getSqliteSchema() );
$this->assertSame( $parent, $this->sqlite->query( 'SELECT * FROM parent ORDER BY id' )->fetchAll( PDO::FETCH_ASSOC ) );
$this->assertSame( $child, $this->sqlite->query( 'SELECT * FROM child ORDER BY id' )->fetchAll( PDO::FETCH_ASSOC ) );
}
}

/**
* @dataProvider data_rebuild_operations_and_table_types
*/
public function testAdministrationRollsBackFailedIndexRecreation( string $operation, bool $temporary ): void {
$this->assertQuery(
'CREATE ' . ( $temporary ? 'TEMPORARY ' : '' ) . 'TABLE t (id INT AUTO_INCREMENT PRIMARY KEY, name TEXT, UNIQUE KEY u (name), KEY k (id)) AUTO_INCREMENT = 100'
);

// These values fit the existing binary index but conflict when its NOCASE definition is restored.
$this->sqlite->exec( 'DROP INDEX t__u' );
$this->sqlite->exec( 'CREATE UNIQUE INDEX t__u ON t (name COLLATE BINARY)' );
$this->sqlite->exec( "INSERT INTO t VALUES (1, 'a'), (2, 'A')" );
$schema = $this->getSqliteSchema();
$data = $this->sqlite->query( 'SELECT rowid, * FROM t ORDER BY rowid' )->fetchAll( PDO::FETCH_ASSOC );
$sequence_table = $temporary ? 'temp.sqlite_sequence' : 'main.sqlite_sequence';
$sequence = $this->sqlite->query( "SELECT seq FROM $sequence_table WHERE name = 't'" )->fetchColumn();

$result = $this->assertQuery( "$operation TABLE t" );
$this->assertCount( 2, $result );
$this->assertStringContainsString( 'UNIQUE constraint failed', $result[0]->Msg_text );
$this->assertSame( 'Operation failed', $result[1]->Msg_text );
$this->assertSame( $schema, $this->getSqliteSchema() );
$this->assertSame( $data, $this->sqlite->query( 'SELECT rowid, * FROM t ORDER BY rowid' )->fetchAll( PDO::FETCH_ASSOC ) );
$this->assertSame( $sequence, $this->sqlite->query( "SELECT seq FROM $sequence_table WHERE name = 't'" )->fetchColumn() );
}

/**
* @dataProvider data_rebuild_operations_and_transactions
*/
public function testAdministrationKeepsSuccessfulRebuilds( string $operation, bool $in_transaction ): void {
$this->assertQuery( 'CREATE TABLE good_before (id INT, KEY k (id))' );
$this->assertQuery( 'CREATE TABLE bad (id INT)' );
$this->assertQuery( 'CREATE TABLE good_after (id INT, KEY k (id))' );
$this->assertQuery( 'INSERT INTO bad VALUES (NULL)' );
$this->sqlite->exec( "UPDATE _wp_sqlite_mysql_information_schema_columns SET is_nullable = 'NO' WHERE table_name = 'bad'" );

// Rebuilding the good tables must restore their missing indexes.
$this->sqlite->exec( 'DROP INDEX good_before__k' );
$this->sqlite->exec( 'DROP INDEX good_after__k' );
$schema = $this->getSqliteSchema();
if ( $in_transaction ) {
$this->assertQuery( 'START TRANSACTION' );
$this->assertQuery( 'INSERT INTO good_before VALUES (1)' );
$this->assertQuery( 'SAVEPOINT caller' );
$this->assertQuery( 'INSERT INTO good_before VALUES (2)' );
}

$result = $this->assertQuery( "$operation TABLE good_before, bad, good_after" );
$this->assertSame( array( 'wp.good_before', 'wp.bad', 'wp.bad', 'wp.good_after' ), array_column( $result, 'Table' ) );
$this->assertSame( array( 'status', 'Error', 'status', 'status' ), array_column( $result, 'Msg_type' ) );
$this->assertSame( 'OK', $result[0]->Msg_text );
$this->assertStringContainsString( 'NOT NULL constraint failed', $result[1]->Msg_text );
$this->assertSame( 'Operation failed', $result[2]->Msg_text );
$this->assertSame( 'OK', $result[3]->Msg_text );
$this->assertSame(
array( 'good_after__k', 'good_before__k' ),
$this->sqlite->query( "SELECT name FROM sqlite_master WHERE type = 'index' AND name IN ('good_before__k', 'good_after__k') ORDER BY name" )->fetchAll( PDO::FETCH_COLUMN )
);
$this->assertSame( array( null ), $this->sqlite->query( 'SELECT id FROM bad' )->fetchAll( PDO::FETCH_COLUMN ) );
$this->assertSame( array(), $this->sqlite->query( "SELECT name FROM sqlite_master WHERE name GLOB '_wp_sqlite_tmp_*'" )->fetchAll( PDO::FETCH_COLUMN ) );
$this->assertSame( $in_transaction, $this->engine->inTransaction() );

if ( $in_transaction ) {
$this->assertQuery( 'ROLLBACK TO SAVEPOINT caller' );
$this->assertEquals( array( (object) array( 'id' => '1' ) ), $this->assertQuery( 'SELECT id FROM good_before' ) );
$this->assertSame( $schema, $this->getSqliteSchema() );
$this->assertQuery( 'COMMIT' );
$this->assertEquals( array( (object) array( 'id' => '1' ) ), $this->assertQuery( 'SELECT id FROM good_before' ) );
}
}

public function data_rebuild_operations(): array {
return array(
'optimize' => array( 'OPTIMIZE' ),
'repair' => array( 'REPAIR' ),
);
}

public function data_rebuild_operations_and_table_types(): array {
return array(
'optimize permanent' => array( 'OPTIMIZE', false ),
'optimize temporary' => array( 'OPTIMIZE', true ),
'repair permanent' => array( 'REPAIR', false ),
'repair temporary' => array( 'REPAIR', true ),
);
}

public function data_rebuild_operations_and_transactions(): array {
return array(
'optimize autocommit' => array( 'OPTIMIZE', false ),
'optimize transaction' => array( 'OPTIMIZE', true ),
'repair autocommit' => array( 'REPAIR', false ),
'repair transaction' => array( 'REPAIR', true ),
);
}

public function testShowCollation(): void {
// Simple.
$this->assertQuery( 'SHOW COLLATION' );
Expand Down Expand Up @@ -2713,4 +2843,13 @@ public function testInformationSchemaAlterTableDropCheckConstraint(): void {
$result = $this->assertQuery( 'SELECT * FROM information_schema.check_constraints' );
$this->assertCount( 0, $result );
}

private function getSqliteSchema(): array {
return $this->sqlite->query(
"SELECT 'main' AS db, type, name, tbl_name, sql FROM sqlite_master
UNION ALL
SELECT 'temp' AS db, type, name, tbl_name, sql FROM sqlite_temp_master
ORDER BY db, type, name"
)->fetchAll( PDO::FETCH_ASSOC );
}
}
Loading