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)' );