From cc1f852a713d423bd3d327d67833de06d4ec9196 Mon Sep 17 00:00:00 2001 From: Muhammad Zeeshan Sardar <78978869+zeeshansardar08@users.noreply.github.com> Date: Mon, 9 Nov 2026 00:08:50 +0500 Subject: [PATCH] Skip unmatched rows in multi-table DELETE with outer JOINs With a LEFT or RIGHT JOIN, a target table can have no matching row for some of the selected rows. Its ROWID is NULL for those rows, and the NULLs were joined into the ROWID list as empty items, for example "rowid IN ( 1, 2, )", which SQLite rejects with a syntax error. Leave out the NULL ROWIDs, and skip the DELETE for a table when none remain. Fixes #585. --- .../src/sqlite/class-wp-mysql-on-sqlite.php | 17 ++++++++- .../tests/WP_MySQL_On_SQLite_Tests.php | 38 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite.php b/packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite.php index bef1bedeb..8c556d47d 100644 --- a/packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite.php +++ b/packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite.php @@ -2886,12 +2886,27 @@ private function execute_delete_statement( WP_Parser_Node $node ): void { $affected_rows = 0; if ( count( $ids ) > 0 ) { foreach ( $table_aliases as $table ) { + /* + * With an outer JOIN, a table may have no matching row for + * some of the selected rows. Its ROWID is NULL for those rows, + * and there is nothing to delete from the table for them. + */ + $rowids = array_filter( + array_column( $ids, "{$table}_rowid" ), + function ( $rowid ) { + return null !== $rowid; + } + ); + if ( count( $rowids ) === 0 ) { + continue; + } + $stmt = $this->execute_sqlite_query( sprintf( 'DELETE FROM %s AS %s WHERE rowid IN ( %s )', $this->quote_sqlite_identifier( $alias_map[ $table ] ), $this->quote_sqlite_identifier( $table ), - implode( ', ', array_column( $ids, "{$table}_rowid" ) ) + implode( ', ', $rowids ) ) ); $affected_rows += $stmt->rowCount(); diff --git a/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php b/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php index 4f87a3a6f..4c7d7cc7f 100644 --- a/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php +++ b/packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php @@ -6200,6 +6200,44 @@ public function testTranslatesComplexDelete() { ); } + public function testMultiTableDeleteWithLeftJoinSkipsUnmatchedRows(): void { + $this->assertQuery( 'CREATE TABLE p (id INT PRIMARY KEY, type TEXT)' ); + $this->assertQuery( 'CREATE TABLE pm (id INT PRIMARY KEY, post_id INT)' ); + $this->assertQuery( "INSERT INTO p VALUES (1, 'revision'), (2, 'revision'), (3, 'post')" ); + $this->assertQuery( 'INSERT INTO pm VALUES (1, 1), (2, 1), (3, 3)' ); + + // Post 2 has no metadata, so its "pm" ROWID is NULL. + $result = $this->assertQuery( + "DELETE p, pm FROM p LEFT JOIN pm ON pm.post_id = p.id WHERE p.type = 'revision'" + ); + $this->assertSame( 4, $result ); + + $result = $this->assertQuery( 'SELECT id FROM p ORDER BY id' ); + $this->assertSame( array( '3' ), array_column( $result, 'id' ) ); + $result = $this->assertQuery( 'SELECT id FROM pm ORDER BY id' ); + $this->assertSame( array( '3' ), array_column( $result, 'id' ) ); + } + + public function testMultiTableDeleteWithLeftJoinAndNoMatchesInJoinedTable(): void { + $this->assertQuery( 'CREATE TABLE p (id INT PRIMARY KEY)' ); + $this->assertQuery( 'CREATE TABLE pm (id INT PRIMARY KEY, post_id INT)' ); + $this->assertQuery( 'INSERT INTO p VALUES (1), (2), (3)' ); + $this->assertQuery( 'INSERT INTO pm VALUES (1, 3)' ); + + // Only the joined table is a target, and it has no matching rows. + $result = $this->assertQuery( 'DELETE pm FROM p LEFT JOIN pm ON pm.post_id = p.id WHERE p.id < 3' ); + $this->assertSame( 0, $result ); + + // Both tables are targets, and the joined table has no matching rows. + $result = $this->assertQuery( 'DELETE p, pm FROM p LEFT JOIN pm ON pm.post_id = p.id WHERE p.id < 3' ); + $this->assertSame( 2, $result ); + + $result = $this->assertQuery( 'SELECT id FROM p ORDER BY id' ); + $this->assertSame( array( '3' ), array_column( $result, 'id' ) ); + $result = $this->assertQuery( 'SELECT id FROM pm ORDER BY id' ); + $this->assertSame( array( '1' ), array_column( $result, 'id' ) ); + } + public function testDeleteReturnsZeroAffectedRowsWhenNoMatchingRows() { $this->assertQuery( 'CREATE TABLE t (id INT)' ); $this->assertQuery( 'INSERT INTO t (id) VALUES (1)' );