Skip to content

Commit bd461a2

Browse files
authored
Add support for DELETE with LIMIT and ORDER BY (#365)
## Summary Adds support for single-table `DELETE` statements with `ORDER BY` and/or `LIMIT` clauses, which SQLite rejects as a syntax error unless it was compiled with `SQLITE_ENABLE_UPDATE_DELETE_LIMIT` (not the case for standard builds, including the one bundled with WordPress Playground). The fix mirrors the existing `UPDATE` LIMIT rewrite: when `ORDER BY` or `LIMIT` is present, the statement is translated to drive deletion off a `rowid` subquery. ```sql -- MySQL DELETE FROM t WHERE c = 1 ORDER BY c LIMIT 10 -- Translated SQLite DELETE FROM `t` WHERE rowid IN ( SELECT rowid FROM `t` WHERE `c` = 1 ORDER BY `c` ASC LIMIT 10 ) ``` The subquery forwards `tableRef`, `tableAlias`, `whereClause`, `orderClause`, and `simpleLimitClause`, so queries that reference an aliased table keep the alias in scope inside the subquery (e.g. `DELETE FROM t AS a WHERE a.c = 1 LIMIT 1` works). ## Scope - **In scope:** single-table `DELETE` with `ORDER BY`, `LIMIT`, or both — with or without `WHERE` and alias. - **Out of scope:** multi-table `DELETE` — MySQL's grammar disallows `ORDER BY`/`LIMIT` there, so there's nothing to add. ## Tests - Translation tests covering plain `LIMIT`, `WHERE` + `LIMIT`, `ORDER BY` + `LIMIT`, `WHERE` + `ORDER BY` + `LIMIT`, and alias + `WHERE` + `LIMIT`. - Runtime tests that insert rows, run `DELETE ... LIMIT` through the driver, and assert on the surviving rows — including a case that matches multiple rows to verify `LIMIT` is actually enforced, and an alias case. Fixes #100
1 parent 3a3baf7 commit bd461a2

3 files changed

Lines changed: 120 additions & 0 deletions

File tree

packages/mysql-on-sqlite/src/sqlite/class-wp-pdo-mysql-on-sqlite.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2308,6 +2308,38 @@ private function execute_delete_statement( WP_Parser_Node $node ): void {
23082308
throw $this->new_access_denied_to_information_schema_exception();
23092309
}
23102310

2311+
/*
2312+
* SQLite doesn't support DELETE with ORDER BY/LIMIT.
2313+
* We need to use a subquery to emulate this behavior.
2314+
*
2315+
* For instance, the following query:
2316+
* DELETE FROM t WHERE c = 2 LIMIT 1;
2317+
* Will be rewritten to:
2318+
* DELETE FROM t WHERE rowid IN ( SELECT rowid FROM t WHERE c = 2 LIMIT 1 );
2319+
*/
2320+
$has_order = $node->has_child_node( 'orderClause' );
2321+
$has_limit = $node->has_child_node( 'simpleLimitClause' );
2322+
if ( $has_order || $has_limit ) {
2323+
$where_subquery = 'SELECT rowid FROM ' . $this->translate_sequence(
2324+
array(
2325+
$table_ref,
2326+
$node->get_first_child_node( 'tableAlias' ),
2327+
$node->get_first_child_node( 'whereClause' ),
2328+
$node->get_first_child_node( 'orderClause' ),
2329+
$node->get_first_child_node( 'simpleLimitClause' ),
2330+
)
2331+
);
2332+
2333+
$query = sprintf(
2334+
'DELETE FROM %s WHERE rowid IN ( %s )',
2335+
$this->translate( $table_ref ),
2336+
$where_subquery
2337+
);
2338+
2339+
$this->last_result_statement = $this->execute_sqlite_query( $query );
2340+
return;
2341+
}
2342+
23112343
$query = $this->translate( $node );
23122344
$this->last_result_statement = $this->execute_sqlite_query( $query );
23132345
}

packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Tests.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,64 @@ public function testUpdateWithoutWhereButWithLimit() {
222222
$this->assertEquals( '2003-05-27 10:08:48', $result2[0]->option_value );
223223
}
224224

225+
public function testDeleteWithLimit() {
226+
$this->assertQuery(
227+
"INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 00:00:45')"
228+
);
229+
$this->assertQuery(
230+
"INSERT INTO _dates (option_name, option_value) VALUES ('second', '2003-05-28 00:00:45')"
231+
);
232+
$this->assertQuery(
233+
"INSERT INTO _dates (option_name, option_value) VALUES ('third', '2003-05-29 00:00:45')"
234+
);
235+
236+
// LIMIT only: only one row is removed even though WHERE matches every row.
237+
$result = $this->assertQuery( "DELETE FROM _dates WHERE option_name LIKE '%' LIMIT 1" );
238+
$this->assertSame( 1, $result );
239+
240+
$rows = $this->engine->query( 'SELECT option_name FROM _dates ORDER BY option_name' );
241+
$this->assertCount( 2, $rows );
242+
243+
// ORDER BY + LIMIT: deletes the lexicographically-first remaining row.
244+
$result = $this->assertQuery( 'DELETE FROM _dates ORDER BY option_name ASC LIMIT 1' );
245+
$this->assertSame( 1, $result );
246+
247+
$rows = $this->engine->query( 'SELECT option_name FROM _dates' );
248+
$this->assertCount( 1, $rows );
249+
$this->assertSame( 'third', $rows[0]->option_name );
250+
}
251+
252+
public function testDeleteWithoutWhereButWithLimit() {
253+
$this->assertQuery(
254+
"INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 10:08:48')"
255+
);
256+
$this->assertQuery(
257+
"INSERT INTO _dates (option_name, option_value) VALUES ('second', '2003-05-27 10:08:48')"
258+
);
259+
260+
$result = $this->assertQuery( 'DELETE FROM _dates LIMIT 1' );
261+
$this->assertSame( 1, $result );
262+
263+
$rows = $this->engine->query( 'SELECT option_name FROM _dates' );
264+
$this->assertCount( 1, $rows );
265+
}
266+
267+
public function testDeleteWithAliasAndLimit() {
268+
$this->assertQuery(
269+
"INSERT INTO _dates (option_name, option_value) VALUES ('a', '2003-05-27 00:00:45')"
270+
);
271+
$this->assertQuery(
272+
"INSERT INTO _dates (option_name, option_value) VALUES ('b', '2003-05-28 00:00:45')"
273+
);
274+
275+
$result = $this->assertQuery( "DELETE FROM _dates AS d WHERE d.option_name = 'a' LIMIT 1" );
276+
$this->assertSame( 1, $result );
277+
278+
$rows = $this->engine->query( 'SELECT option_name FROM _dates' );
279+
$this->assertCount( 1, $rows );
280+
$this->assertSame( 'b', $rows[0]->option_name );
281+
}
282+
225283
public function testCastAsBinary() {
226284
$this->assertQuery(
227285
// Use a confusing alias to make sure it replaces only the correct token

packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Translation_Tests.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,36 @@ public function testDelete(): void {
382382
'DELETE FROM `t` WHERE `c` = 1',
383383
'DELETE FROM t WHERE c = 1'
384384
);
385+
386+
// DELETE with LIMIT.
387+
$this->assertQuery(
388+
'DELETE FROM `t` WHERE rowid IN ( SELECT rowid FROM `t` LIMIT 1 )',
389+
'DELETE FROM t LIMIT 1'
390+
);
391+
392+
// DELETE with WHERE and LIMIT.
393+
$this->assertQuery(
394+
'DELETE FROM `t` WHERE rowid IN ( SELECT rowid FROM `t` WHERE `c` = 1 LIMIT 5 )',
395+
'DELETE FROM t WHERE c = 1 LIMIT 5'
396+
);
397+
398+
// DELETE with ORDER BY and LIMIT.
399+
$this->assertQuery(
400+
'DELETE FROM `t` WHERE rowid IN ( SELECT rowid FROM `t` ORDER BY `c` ASC LIMIT 1 )',
401+
'DELETE FROM t ORDER BY c ASC LIMIT 1'
402+
);
403+
404+
// DELETE with WHERE, ORDER BY, and LIMIT.
405+
$this->assertQuery(
406+
'DELETE FROM `t` WHERE rowid IN ( SELECT rowid FROM `t` WHERE `c` = 1 ORDER BY `c` ASC LIMIT 1 )',
407+
'DELETE FROM t WHERE c = 1 ORDER BY c ASC LIMIT 1'
408+
);
409+
410+
// DELETE with a table alias and LIMIT.
411+
$this->assertQuery(
412+
'DELETE FROM `t` WHERE rowid IN ( SELECT rowid FROM `t` AS `a` WHERE `a`.`c` = 1 LIMIT 1 )',
413+
'DELETE FROM t AS a WHERE a.c = 1 LIMIT 1'
414+
);
385415
}
386416

387417
public function testCreateTable(): void {

0 commit comments

Comments
 (0)