From 0489af63938518e638886c593fc936ee50b50e88 Mon Sep 17 00:00:00 2001 From: Volodymyr Shelmuk Date: Thu, 30 Oct 2025 21:07:49 +0200 Subject: [PATCH 1/5] Refactor `\StellarWP\DB\QueryBuilder\Concerns\CRUD::delete` method to support advanced DELETE queries --- src/DB/QueryBuilder/Concerns/CRUD.php | 7 +- src/DB/QueryBuilder/QueryBuilder.php | 33 +++- tests/wpunit/QueryBuilder/CRUDTest.php | 252 +++++++++++++++++++++++++ 3 files changed, 285 insertions(+), 7 deletions(-) diff --git a/src/DB/QueryBuilder/Concerns/CRUD.php b/src/DB/QueryBuilder/Concerns/CRUD.php index 269ef5f..aed0b85 100644 --- a/src/DB/QueryBuilder/Concerns/CRUD.php +++ b/src/DB/QueryBuilder/Concerns/CRUD.php @@ -79,14 +79,9 @@ public function upsert( $data, $match = [], $format = null ) { * * @return false|int * - * @see https://developer.wordpress.org/reference/classes/wpdb/delete/ */ public function delete() { - return DB::delete( - $this->getTable(), - $this->getWhere(), - null - ); + return DB::query( $this->deleteSQL() ); } /** diff --git a/src/DB/QueryBuilder/QueryBuilder.php b/src/DB/QueryBuilder/QueryBuilder.php index 76b8bc2..21f2452 100644 --- a/src/DB/QueryBuilder/QueryBuilder.php +++ b/src/DB/QueryBuilder/QueryBuilder.php @@ -53,7 +53,38 @@ public function getSQL() { $this->getUnionSQL() ); - // Trim double spaces added by DB::prepare + return $this->buildSQL($sql); + } + + /** + * Generate the SQL for a DELETE query. Only the FROM, WHERE, ORDER BY, and LIMIT clauses are included. + * RETURNING is not supported. + * Note that aliases are supported only on MySQL >= 8.0.24 and MariaDB >= 11.6. + * + * @see https://mariadb.com/docs/server/reference/sql-statements/data-manipulation/changing-deleting-data/delete + * @see https://dev.mysql.com/doc/refman/8.4/en/delete.html + * + * @return string DELETE query. + */ + public function deleteSQL() { + $sql = array_merge( + $this->getFromSQL(), + $this->getWhereSQL(), + $this->getOrderBySQL(), + $this->getLimitSQL() + ); + + return 'DELETE ' . $this->buildSQL($sql); + } + + /** + * Build the SQL query from the given parts. + * + * @param array $sql The SQL query parts. + * + * @return string SQL query. + */ + private function buildSQL( $sql ){ return str_replace( [ ' ', ' ' ], ' ', diff --git a/tests/wpunit/QueryBuilder/CRUDTest.php b/tests/wpunit/QueryBuilder/CRUDTest.php index 894e692..2a20ae6 100644 --- a/tests/wpunit/QueryBuilder/CRUDTest.php +++ b/tests/wpunit/QueryBuilder/CRUDTest.php @@ -214,4 +214,256 @@ public function testUpsertShouldUpdateRowInDatabase() $this->assertEquals($further_updated_data['post_content'], $post->post_content); } + /** + * Tests if delete() can delete with both ORDER BY and LIMIT clauses. + * + * @return void + */ + public function testDeleteShouldWorkWithOrderByAndLimit() + { + // Insert multiple posts + $posts = [ + ['post_title' => 'Delete Combined A', 'post_type' => 'delete_combined_test', 'post_content' => 'Content A'], + ['post_title' => 'Delete Combined B', 'post_type' => 'delete_combined_test', 'post_content' => 'Content B'], + ['post_title' => 'Delete Combined C', 'post_type' => 'delete_combined_test', 'post_content' => 'Content C'], + ['post_title' => 'Delete Combined D', 'post_type' => 'delete_combined_test', 'post_content' => 'Content D'], + ]; + + $ids = []; + foreach ($posts as $post) { + DB::table('posts')->insert($post); + $ids[] = DB::last_insert_id(); + } + + // Delete the 2 oldest posts (lowest IDs) + DB::table('posts') + ->where('post_type', 'delete_combined_test') + ->orderBy('ID', 'ASC') + ->limit(2) + ->delete(); + + // Verify the first 2 posts were deleted + $post1 = DB::table('posts') + ->where('ID', $ids[0]) + ->get(); + $post2 = DB::table('posts') + ->where('ID', $ids[1]) + ->get(); + + $this->assertNull($post1); + $this->assertNull($post2); + + // Verify the other 2 posts still exist + $count = DB::table('posts') + ->where('post_type', 'delete_combined_test') + ->count(); + + $this->assertEquals(2, $count); + } + + /** + * Tests if delete() works with complex WHERE clauses using whereIn. + * + * @return void + */ + public function testDeleteShouldWorkWithWhereIn() + { + // Insert multiple posts + $posts = [ + ['post_title' => 'Delete WhereIn 1', 'post_type' => 'delete_wherein_test', 'post_content' => 'Content 1'], + ['post_title' => 'Delete WhereIn 2', 'post_type' => 'delete_wherein_test', 'post_content' => 'Content 2'], + ['post_title' => 'Delete WhereIn 3', 'post_type' => 'delete_wherein_test', 'post_content' => 'Content 3'], + ['post_title' => 'Delete WhereIn 4', 'post_type' => 'delete_wherein_test', 'post_content' => 'Content 4'], + ]; + + $ids = []; + foreach ($posts as $post) { + DB::table('posts')->insert($post); + $ids[] = DB::last_insert_id(); + } + + // Delete posts with specific IDs using whereIn + DB::table('posts') + ->whereIn('ID', [$ids[0], $ids[2]]) + ->delete(); + + // Verify specific posts were deleted + $post1 = DB::table('posts')->where('ID', $ids[0])->get(); + $post3 = DB::table('posts')->where('ID', $ids[2])->get(); + + $this->assertNull($post1); + $this->assertNull($post3); + + // Verify other posts still exist + $post2 = DB::table('posts')->where('ID', $ids[1])->get(); + $post4 = DB::table('posts')->where('ID', $ids[3])->get(); + + $this->assertNotNull($post2); + $this->assertNotNull($post4); + } + + /** + * Tests if delete() works with complex WHERE clauses using whereBetween. + * + * @return void + */ + public function testDeleteShouldWorkWithWhereBetween() + { + // Insert posts with specific menu_order values + $posts = [ + ['post_title' => 'Delete Between 1', 'post_type' => 'delete_between_test', 'menu_order' => 10], + ['post_title' => 'Delete Between 2', 'post_type' => 'delete_between_test', 'menu_order' => 20], + ['post_title' => 'Delete Between 3', 'post_type' => 'delete_between_test', 'menu_order' => 30], + ['post_title' => 'Delete Between 4', 'post_type' => 'delete_between_test', 'menu_order' => 40], + ]; + + foreach ($posts as $post) { + DB::table('posts')->insert($post); + } + + // Delete posts with menu_order between 15 and 35 + DB::table('posts') + ->where('post_type', 'delete_between_test') + ->whereBetween('menu_order', 15, 35) + ->delete(); + + // Should have deleted 2 posts (menu_order 20 and 30) + $remaining = DB::table('posts') + ->where('post_type', 'delete_between_test') + ->count(); + + $this->assertEquals(2, $remaining); + + // Verify the correct posts remain (menu_order 10 and 40) + $posts = DB::table('posts') + ->select('menu_order') + ->where('post_type', 'delete_between_test') + ->orderBy('menu_order', 'ASC') + ->getAll(); + + $this->assertEquals(10, $posts[0]->menu_order); + $this->assertEquals(40, $posts[1]->menu_order); + } + + /** + * Tests if delete() works with multiple WHERE conditions. + * + * @return void + */ + public function testDeleteShouldWorkWithMultipleWhereConditions() + { + // Insert multiple posts with different types + $posts = [ + ['post_title' => 'Delete Multi 1', 'post_type' => 'type_a', 'post_status' => 'publish'], + ['post_title' => 'Delete Multi 2', 'post_type' => 'type_a', 'post_status' => 'draft'], + ['post_title' => 'Delete Multi 3', 'post_type' => 'type_b', 'post_status' => 'publish'], + ['post_title' => 'Delete Multi 4', 'post_type' => 'type_b', 'post_status' => 'draft'], + ]; + + foreach ($posts as $post) { + DB::table('posts')->insert($post); + } + + // Delete only posts with type_a AND status publish + DB::table('posts') + ->where('post_type', 'type_a') + ->where('post_status', 'publish') + ->where('post_title', 'Delete Multi 1') + ->delete(); + + // Verify only 1 post was deleted + $deleted = DB::table('posts') + ->where('post_title', 'Delete Multi 1') + ->get(); + + $this->assertNull($deleted); + + // Verify other posts still exist + $remaining = DB::table('posts') + ->whereIn('post_title', ['Delete Multi 2', 'Delete Multi 3', 'Delete Multi 4']) + ->count(); + + $this->assertEquals(3, $remaining); + } + + /** + * Tests if delete() works with whereLike clause. + * + * @return void + */ + public function testDeleteShouldWorkWithWhereLike() + { + // Insert multiple posts with similar titles + $posts = [ + ['post_title' => 'Product: Widget ABC', 'post_type' => 'delete_like_test', 'post_content' => 'Content 1'], + ['post_title' => 'Product: WidgetXYZ', 'post_type' => 'delete_like_test', 'post_content' => 'Content 2'], + ['post_title' => 'Product: Gadget ABC', 'post_type' => 'delete_like_test', 'post_content' => 'Content 3'], + ['post_title' => 'Service: Widget ABC', 'post_type' => 'delete_like_test', 'post_content' => 'Content 4'], + ]; + + foreach ($posts as $post) { + DB::table('posts')->insert($post); + } + + // Delete all posts with titles containing "Widget" + DB::table('posts') + ->where('post_type', 'delete_like_test') + ->whereLike('post_title', '%Widget%') + ->delete(); + + // Should have deleted 3 posts (all with "Widget" in title) + $remaining = DB::table('posts') + ->where('post_type', 'delete_like_test') + ->count(); + + $this->assertEquals(1, $remaining); + + // Verify the correct post remains (Gadget) + $post = DB::table('posts') + ->where('post_type', 'delete_like_test') + ->get(); + + $this->assertStringContainsString('Gadget', $post->post_title); + } + + /** + * Tests if delete() works with whereLike using wildcard prefix. + * + * @return void + */ + public function testDeleteShouldWorkWithWhereLikePrefix() + { + // Insert multiple posts with different prefixes + $posts = [ + ['post_title' => 'Draft: Important Document', 'post_type' => 'delete_prefix_test', 'post_content' => 'Content 1'], + ['post_title' => 'Draft: Meeting Notes', 'post_type' => 'delete_prefix_test', 'post_content' => 'Content 2'], + ['post_title' => 'Final: Important Document Not Draft', 'post_type' => 'delete_prefix_test', 'post_content' => 'Content 3'], + ['post_title' => 'Review: Meeting Notes', 'post_type' => 'delete_prefix_test', 'post_content' => 'Content 4'], + ]; + + foreach ($posts as $post) { + DB::table('posts')->insert($post); + } + + // Delete all posts starting with "Draft:" + DB::table('posts') + ->where('post_type', 'delete_prefix_test') + ->whereLike('post_title', 'Draft:%') + ->delete(); + + // Should have deleted 2 posts (both starting with "Draft:") + $remaining = DB::table('posts') + ->where('post_type', 'delete_prefix_test') + ->count(); + + $this->assertEquals(2, $remaining); + + // Verify no "Draft:" posts remain + $draftPosts = DB::table('posts') + ->where('post_type', 'delete_prefix_test') + ->whereLike('post_title', 'Draft:%') + ->count(); + + $this->assertEquals(0, $draftPosts); + } } From 3623a8cfbd6803fbe7e8a445d28e31f9668378e1 Mon Sep 17 00:00:00 2001 From: Volodymyr Shelmuk Date: Thu, 30 Oct 2025 21:12:04 +0200 Subject: [PATCH 2/5] Expand documentation for the `delete` method with usage examples and restrictions --- README.md | 62 +++++++++++++++++++++++++++ src/DB/QueryBuilder/Concerns/CRUD.php | 45 ++++++++++++++++++- src/DB/QueryBuilder/QueryBuilder.php | 2 +- 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 696ff70..af7abbf 100644 --- a/README.md +++ b/README.md @@ -874,12 +874,74 @@ DB::table('table_name') The `QueryBuilder::delete` method may be used to delete records from the table. +Unlike WordPress's `$wpdb->delete()` method, this implementation generates and executes a DELETE SQL statement directly, which allows for advanced features like ORDER BY and LIMIT. + +#### Basic delete with WHERE + ```php DB::table('posts') ->where('post_author', 1) ->delete(); ``` +#### Delete with LIMIT + +Limit the number of rows to delete: + +```php +// Delete only the first 10 draft posts +DB::table('posts') + ->where('post_status', 'draft') + ->limit(10) + ->delete(); +``` + +#### Delete with ORDER BY and LIMIT + +Control which rows are deleted when using LIMIT: + +```php +// Delete the 100 oldest posts in trash +DB::table('posts') + ->where('post_status', 'trash') + ->orderBy('post_date', 'ASC') + ->limit(100) + ->delete(); +``` + +#### Delete with LIKE patterns + +Use pattern matching to delete rows: + +```php +// Delete all posts with titles starting with "Draft:" +DB::table('posts') + ->whereLike('post_title', 'Draft:%') + ->delete(); +``` + +#### Delete with complex WHERE conditions + +Combine multiple WHERE clauses for precise deletion: + +```php +// Delete auto-draft pages with IDs between 1 and 1000 +DB::table('posts') + ->where('post_type', 'page') + ->where('post_status', 'auto-draft') + ->whereBetween('ID', 1, 1000) + ->delete(); + +// Delete posts using whereIn +DB::table('posts') + ->whereIn('ID', [5, 10, 15, 20]) + ->delete(); +``` + +**Important restrictions:** +- Table aliases in the FROM clause may not be supported on older database versions (MySQL < 8.0.24, MariaDB < 11.6). Avoid using table aliases when calling `delete()`. +- JOINs are not supported in DELETE statements with this implementation. + ### Get diff --git a/src/DB/QueryBuilder/Concerns/CRUD.php b/src/DB/QueryBuilder/Concerns/CRUD.php index aed0b85..8775d4b 100644 --- a/src/DB/QueryBuilder/Concerns/CRUD.php +++ b/src/DB/QueryBuilder/Concerns/CRUD.php @@ -75,10 +75,53 @@ public function upsert( $data, $match = [], $format = null ) { } /** + * Delete rows from the database. + * + * Unlike WordPress's $wpdb->delete() method, this implementation generates and executes + * a DELETE SQL statement directly, which allows for advanced features like ORDER BY and LIMIT. + * + * Supports: + * - WHERE clauses (including whereLike, whereIn, whereBetween, etc.) + * - ORDER BY for controlling which rows are deleted first + * - LIMIT to restrict the number of rows deleted + * - Complex WHERE conditions (AND, OR, nested queries) + * + * Usage examples: + * ```php + * // Simple delete with WHERE + * DB::table('posts')->where('post_status', 'draft')->delete(); + * + * // Delete with LIMIT (delete only 10 rows) + * DB::table('posts')->where('post_type', 'temp')->limit(10)->delete(); + * + * // Delete oldest posts first using ORDER BY and LIMIT + * DB::table('posts') + * ->where('post_status', 'trash') + * ->orderBy('post_date', 'ASC') + * ->limit(100) + * ->delete(); + * + * // Delete with LIKE pattern + * DB::table('posts')->whereLike('post_title', 'Draft:%')->delete(); + * + * // Delete with multiple conditions + * DB::table('posts') + * ->where('post_type', 'page') + * ->where('post_status', 'auto-draft') + * ->whereBetween('ID', 1, 1000) + * ->delete(); + * ``` + * + * Restrictions: + * - Table aliases in the FROM clause may not be supported on older database versions + * (MySQL < 8.0.24, MariaDB < 11.6). Avoid using table aliases with delete(). + * - JOINs are not supported in DELETE statements with this implementation + * * @since 1.0.0 * - * @return false|int + * @return false|int Number of rows deleted, or false on error. * + * @see QueryBuilder::deleteSQL() for the SQL generation logic */ public function delete() { return DB::query( $this->deleteSQL() ); diff --git a/src/DB/QueryBuilder/QueryBuilder.php b/src/DB/QueryBuilder/QueryBuilder.php index 21f2452..15d441b 100644 --- a/src/DB/QueryBuilder/QueryBuilder.php +++ b/src/DB/QueryBuilder/QueryBuilder.php @@ -84,7 +84,7 @@ public function deleteSQL() { * * @return string SQL query. */ - private function buildSQL( $sql ){ + private function buildSQL( $sql ) { return str_replace( [ ' ', ' ' ], ' ', From ba1e1d532a02ad807e472b6092c7805ecbff4e20 Mon Sep 17 00:00:00 2001 From: Volodymyr Shelmuk Date: Thu, 30 Oct 2025 21:16:28 +0200 Subject: [PATCH 3/5] Fix CI test run --- .github/workflows/tests-php.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests-php.yml b/.github/workflows/tests-php.yml index 1fb7867..9446707 100644 --- a/.github/workflows/tests-php.yml +++ b/.github/workflows/tests-php.yml @@ -28,13 +28,12 @@ jobs: # Prepare our composer cache directory # ------------------------------------------------------------------------------ - name: Get Composer Cache Directory - id: get-composer-cache-dir - run: | - echo "::set-output name=dir::$(composer config cache-files-dir)" - - uses: actions/cache@v2 id: composer-cache + run: | + echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - uses: actions/cache@v4 with: - path: ${{ steps.get-composer-cache-dir.outputs.dir }} + path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: | ${{ runner.os }}-composer- From 97e065010a84c34068fa6b10cf69288702eed6e3 Mon Sep 17 00:00:00 2001 From: Volodymyr Shelmuk Date: Tue, 4 Nov 2025 20:37:39 +0200 Subject: [PATCH 4/5] Improve pre- and post-assertions for deletion tests --- tests/wpunit/QueryBuilder/CRUDTest.php | 178 +++++++++++-------------- 1 file changed, 78 insertions(+), 100 deletions(-) diff --git a/tests/wpunit/QueryBuilder/CRUDTest.php b/tests/wpunit/QueryBuilder/CRUDTest.php index 2a20ae6..3b7891c 100644 --- a/tests/wpunit/QueryBuilder/CRUDTest.php +++ b/tests/wpunit/QueryBuilder/CRUDTest.php @@ -229,11 +229,8 @@ public function testDeleteShouldWorkWithOrderByAndLimit() ['post_title' => 'Delete Combined D', 'post_type' => 'delete_combined_test', 'post_content' => 'Content D'], ]; - $ids = []; - foreach ($posts as $post) { - DB::table('posts')->insert($post); - $ids[] = DB::last_insert_id(); - } + $ids = $this->insert_posts($posts); + $this->assert_posts_exist($posts, $ids); // Delete the 2 oldest posts (lowest IDs) DB::table('posts') @@ -242,23 +239,13 @@ public function testDeleteShouldWorkWithOrderByAndLimit() ->limit(2) ->delete(); - // Verify the first 2 posts were deleted - $post1 = DB::table('posts') - ->where('ID', $ids[0]) - ->get(); - $post2 = DB::table('posts') - ->where('ID', $ids[1]) - ->get(); - - $this->assertNull($post1); - $this->assertNull($post2); + $foundPosts = DB::table('posts') + ->select('post_title', 'post_type', 'post_content') + ->whereIn('ID', $ids) + ->getAll(ARRAY_A); + unset($posts[0], $posts[1]); - // Verify the other 2 posts still exist - $count = DB::table('posts') - ->where('post_type', 'delete_combined_test') - ->count(); - - $this->assertEquals(2, $count); + $this->assertEquals(array_values($posts), $foundPosts); } /** @@ -276,30 +263,21 @@ public function testDeleteShouldWorkWithWhereIn() ['post_title' => 'Delete WhereIn 4', 'post_type' => 'delete_wherein_test', 'post_content' => 'Content 4'], ]; - $ids = []; - foreach ($posts as $post) { - DB::table('posts')->insert($post); - $ids[] = DB::last_insert_id(); - } + $ids = $this->insert_posts($posts); + $this->assert_posts_exist($posts, $ids); // Delete posts with specific IDs using whereIn DB::table('posts') ->whereIn('ID', [$ids[0], $ids[2]]) ->delete(); - // Verify specific posts were deleted - $post1 = DB::table('posts')->where('ID', $ids[0])->get(); - $post3 = DB::table('posts')->where('ID', $ids[2])->get(); - - $this->assertNull($post1); - $this->assertNull($post3); + $foundPosts = DB::table('posts') + ->select('post_title', 'post_type', 'post_content') + ->whereIn('ID', $ids) + ->getAll(ARRAY_A); + unset($posts[0], $posts[2]); - // Verify other posts still exist - $post2 = DB::table('posts')->where('ID', $ids[1])->get(); - $post4 = DB::table('posts')->where('ID', $ids[3])->get(); - - $this->assertNotNull($post2); - $this->assertNotNull($post4); + $this->assertEquals(array_values($posts), $foundPosts); } /** @@ -317,9 +295,8 @@ public function testDeleteShouldWorkWithWhereBetween() ['post_title' => 'Delete Between 4', 'post_type' => 'delete_between_test', 'menu_order' => 40], ]; - foreach ($posts as $post) { - DB::table('posts')->insert($post); - } + $ids = $this->insert_posts($posts); + $this->assert_posts_exist($posts, $ids); // Delete posts with menu_order between 15 and 35 DB::table('posts') @@ -327,22 +304,13 @@ public function testDeleteShouldWorkWithWhereBetween() ->whereBetween('menu_order', 15, 35) ->delete(); - // Should have deleted 2 posts (menu_order 20 and 30) - $remaining = DB::table('posts') - ->where('post_type', 'delete_between_test') - ->count(); - - $this->assertEquals(2, $remaining); - - // Verify the correct posts remain (menu_order 10 and 40) - $posts = DB::table('posts') - ->select('menu_order') - ->where('post_type', 'delete_between_test') - ->orderBy('menu_order', 'ASC') - ->getAll(); + $foundPosts = DB::table('posts') + ->select('post_title', 'post_type', 'menu_order') + ->whereIn('ID', $ids) + ->getAll(ARRAY_A); + unset($posts[1], $posts[2]); - $this->assertEquals(10, $posts[0]->menu_order); - $this->assertEquals(40, $posts[1]->menu_order); + $this->assertEquals(array_values($posts), $foundPosts); } /** @@ -360,9 +328,8 @@ public function testDeleteShouldWorkWithMultipleWhereConditions() ['post_title' => 'Delete Multi 4', 'post_type' => 'type_b', 'post_status' => 'draft'], ]; - foreach ($posts as $post) { - DB::table('posts')->insert($post); - } + $ids = $this->insert_posts($posts); + $this->assert_posts_exist($posts, $ids); // Delete only posts with type_a AND status publish DB::table('posts') @@ -371,19 +338,13 @@ public function testDeleteShouldWorkWithMultipleWhereConditions() ->where('post_title', 'Delete Multi 1') ->delete(); - // Verify only 1 post was deleted - $deleted = DB::table('posts') - ->where('post_title', 'Delete Multi 1') - ->get(); - - $this->assertNull($deleted); + $foundPosts = DB::table('posts') + ->select('post_title', 'post_type', 'post_status') + ->whereIn('ID', $ids) + ->getAll(ARRAY_A); + unset($posts[0]); - // Verify other posts still exist - $remaining = DB::table('posts') - ->whereIn('post_title', ['Delete Multi 2', 'Delete Multi 3', 'Delete Multi 4']) - ->count(); - - $this->assertEquals(3, $remaining); + $this->assertEquals(array_values($posts), $foundPosts); } /** @@ -401,9 +362,8 @@ public function testDeleteShouldWorkWithWhereLike() ['post_title' => 'Service: Widget ABC', 'post_type' => 'delete_like_test', 'post_content' => 'Content 4'], ]; - foreach ($posts as $post) { - DB::table('posts')->insert($post); - } + $ids = $this->insert_posts($posts); + $this->assert_posts_exist($posts, $ids); // Delete all posts with titles containing "Widget" DB::table('posts') @@ -411,19 +371,12 @@ public function testDeleteShouldWorkWithWhereLike() ->whereLike('post_title', '%Widget%') ->delete(); - // Should have deleted 3 posts (all with "Widget" in title) - $remaining = DB::table('posts') - ->where('post_type', 'delete_like_test') - ->count(); - - $this->assertEquals(1, $remaining); - - // Verify the correct post remains (Gadget) - $post = DB::table('posts') - ->where('post_type', 'delete_like_test') - ->get(); + $foundPosts = DB::table('posts') + ->select('post_title', 'post_type', 'post_content') + ->whereIn('ID', $ids) + ->getAll(ARRAY_A); - $this->assertStringContainsString('Gadget', $post->post_title); + $this->assertEquals([$posts[2]], $foundPosts); } /** @@ -441,9 +394,8 @@ public function testDeleteShouldWorkWithWhereLikePrefix() ['post_title' => 'Review: Meeting Notes', 'post_type' => 'delete_prefix_test', 'post_content' => 'Content 4'], ]; - foreach ($posts as $post) { - DB::table('posts')->insert($post); - } + $ids = $this->insert_posts($posts); + $this->assert_posts_exist($posts, $ids); // Delete all posts starting with "Draft:" DB::table('posts') @@ -451,19 +403,45 @@ public function testDeleteShouldWorkWithWhereLikePrefix() ->whereLike('post_title', 'Draft:%') ->delete(); - // Should have deleted 2 posts (both starting with "Draft:") - $remaining = DB::table('posts') - ->where('post_type', 'delete_prefix_test') - ->count(); + $foundPosts = DB::table('posts') + ->select('post_title', 'post_type', 'post_content') + ->whereIn('ID', $ids) + ->getAll(ARRAY_A); - $this->assertEquals(2, $remaining); + $this->assertEquals([$posts[2], $posts[3]], $foundPosts); + } - // Verify no "Draft:" posts remain - $draftPosts = DB::table('posts') - ->where('post_type', 'delete_prefix_test') - ->whereLike('post_title', 'Draft:%') - ->count(); + /** + * Inserts multiple posts into the database and returns their IDs. + * + * @param array $posts An array of associative arrays, where each associative array represents a post to insert. + * + * @return array An array of IDs corresponding to the inserted posts. + */ + private function insert_posts( array $posts ): array { + $ids = []; + foreach ($posts as $post) { + DB::table('posts')->insert($post); + $ids[] = DB::last_insert_id(); + } + + return $ids; + } + + /** + * Asserts that posts with the given IDs exist and match the specified data. + * + * @param array $posts An array of expected post data to validate against the database. + * @param array $ids An array of post IDs to check for existence in the database. + * + * @return void + */ + private function assert_posts_exist( array $posts, array $ids ) { + $foundPosts = DB::table('posts') + ->select(...array_keys($posts[0])) + ->whereIn('ID', $ids) + ->getAll(ARRAY_A); - $this->assertEquals(0, $draftPosts); + $this->assertEquals($posts, $foundPosts); } } From 1f867cdacc95b85e3092efe0f71cb4d1e457854f Mon Sep 17 00:00:00 2001 From: Volodymyr Shelmuk Date: Tue, 4 Nov 2025 20:50:50 +0200 Subject: [PATCH 5/5] Ensure assert_posts_exist doesn't get empty arrays --- tests/wpunit/QueryBuilder/CRUDTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/wpunit/QueryBuilder/CRUDTest.php b/tests/wpunit/QueryBuilder/CRUDTest.php index 3b7891c..a379de5 100644 --- a/tests/wpunit/QueryBuilder/CRUDTest.php +++ b/tests/wpunit/QueryBuilder/CRUDTest.php @@ -437,6 +437,9 @@ private function insert_posts( array $posts ): array { * @return void */ private function assert_posts_exist( array $posts, array $ids ) { + $this->assertNotEmpty($posts); + $this->assertNotEmpty($ids); + $foundPosts = DB::table('posts') ->select(...array_keys($posts[0])) ->whereIn('ID', $ids)