diff --git a/packages/mysql-on-sqlite/src/sqlite/class-wp-pdo-mysql-on-sqlite.php b/packages/mysql-on-sqlite/src/sqlite/class-wp-pdo-mysql-on-sqlite.php index 34171ac7..f3d0fec3 100644 --- a/packages/mysql-on-sqlite/src/sqlite/class-wp-pdo-mysql-on-sqlite.php +++ b/packages/mysql-on-sqlite/src/sqlite/class-wp-pdo-mysql-on-sqlite.php @@ -2308,6 +2308,38 @@ private function execute_delete_statement( WP_Parser_Node $node ): void { throw $this->new_access_denied_to_information_schema_exception(); } + /* + * SQLite doesn't support DELETE with ORDER BY/LIMIT. + * We need to use a subquery to emulate this behavior. + * + * For instance, the following query: + * DELETE FROM t WHERE c = 2 LIMIT 1; + * Will be rewritten to: + * DELETE FROM t WHERE rowid IN ( SELECT rowid FROM t WHERE c = 2 LIMIT 1 ); + */ + $has_order = $node->has_child_node( 'orderClause' ); + $has_limit = $node->has_child_node( 'simpleLimitClause' ); + if ( $has_order || $has_limit ) { + $where_subquery = 'SELECT rowid FROM ' . $this->translate_sequence( + array( + $table_ref, + $node->get_first_child_node( 'tableAlias' ), + $node->get_first_child_node( 'whereClause' ), + $node->get_first_child_node( 'orderClause' ), + $node->get_first_child_node( 'simpleLimitClause' ), + ) + ); + + $query = sprintf( + 'DELETE FROM %s WHERE rowid IN ( %s )', + $this->translate( $table_ref ), + $where_subquery + ); + + $this->last_result_statement = $this->execute_sqlite_query( $query ); + return; + } + $query = $this->translate( $node ); $this->last_result_statement = $this->execute_sqlite_query( $query ); } diff --git a/packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Tests.php b/packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Tests.php index 4daf7ca7..19a8ffb0 100644 --- a/packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Tests.php +++ b/packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Tests.php @@ -222,6 +222,64 @@ public function testUpdateWithoutWhereButWithLimit() { $this->assertEquals( '2003-05-27 10:08:48', $result2[0]->option_value ); } + public function testDeleteWithLimit() { + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 00:00:45')" + ); + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2003-05-28 00:00:45')" + ); + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('third', '2003-05-29 00:00:45')" + ); + + // LIMIT only: only one row is removed even though WHERE matches every row. + $result = $this->assertQuery( "DELETE FROM _dates WHERE option_name LIKE '%' LIMIT 1" ); + $this->assertSame( 1, $result ); + + $rows = $this->engine->query( 'SELECT option_name FROM _dates ORDER BY option_name' ); + $this->assertCount( 2, $rows ); + + // ORDER BY + LIMIT: deletes the lexicographically-first remaining row. + $result = $this->assertQuery( 'DELETE FROM _dates ORDER BY option_name ASC LIMIT 1' ); + $this->assertSame( 1, $result ); + + $rows = $this->engine->query( 'SELECT option_name FROM _dates' ); + $this->assertCount( 1, $rows ); + $this->assertSame( 'third', $rows[0]->option_name ); + } + + public function testDeleteWithoutWhereButWithLimit() { + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 10:08:48')" + ); + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2003-05-27 10:08:48')" + ); + + $result = $this->assertQuery( 'DELETE FROM _dates LIMIT 1' ); + $this->assertSame( 1, $result ); + + $rows = $this->engine->query( 'SELECT option_name FROM _dates' ); + $this->assertCount( 1, $rows ); + } + + public function testDeleteWithAliasAndLimit() { + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('a', '2003-05-27 00:00:45')" + ); + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('b', '2003-05-28 00:00:45')" + ); + + $result = $this->assertQuery( "DELETE FROM _dates AS d WHERE d.option_name = 'a' LIMIT 1" ); + $this->assertSame( 1, $result ); + + $rows = $this->engine->query( 'SELECT option_name FROM _dates' ); + $this->assertCount( 1, $rows ); + $this->assertSame( 'b', $rows[0]->option_name ); + } + public function testCastAsBinary() { $this->assertQuery( // Use a confusing alias to make sure it replaces only the correct token diff --git a/packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Translation_Tests.php b/packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Translation_Tests.php index e1121570..2c7148f0 100644 --- a/packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Translation_Tests.php +++ b/packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Translation_Tests.php @@ -382,6 +382,36 @@ public function testDelete(): void { 'DELETE FROM `t` WHERE `c` = 1', 'DELETE FROM t WHERE c = 1' ); + + // DELETE with LIMIT. + $this->assertQuery( + 'DELETE FROM `t` WHERE rowid IN ( SELECT rowid FROM `t` LIMIT 1 )', + 'DELETE FROM t LIMIT 1' + ); + + // DELETE with WHERE and LIMIT. + $this->assertQuery( + 'DELETE FROM `t` WHERE rowid IN ( SELECT rowid FROM `t` WHERE `c` = 1 LIMIT 5 )', + 'DELETE FROM t WHERE c = 1 LIMIT 5' + ); + + // DELETE with ORDER BY and LIMIT. + $this->assertQuery( + 'DELETE FROM `t` WHERE rowid IN ( SELECT rowid FROM `t` ORDER BY `c` ASC LIMIT 1 )', + 'DELETE FROM t ORDER BY c ASC LIMIT 1' + ); + + // DELETE with WHERE, ORDER BY, and LIMIT. + $this->assertQuery( + 'DELETE FROM `t` WHERE rowid IN ( SELECT rowid FROM `t` WHERE `c` = 1 ORDER BY `c` ASC LIMIT 1 )', + 'DELETE FROM t WHERE c = 1 ORDER BY c ASC LIMIT 1' + ); + + // DELETE with a table alias and LIMIT. + $this->assertQuery( + 'DELETE FROM `t` WHERE rowid IN ( SELECT rowid FROM `t` AS `a` WHERE `a`.`c` = 1 LIMIT 1 )', + 'DELETE FROM t AS a WHERE a.c = 1 LIMIT 1' + ); } public function testCreateTable(): void {