From 2ab1125a814815a046171f5f7ef2219bc4fc6760 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Fri, 19 Dec 2025 21:07:24 +0200 Subject: [PATCH 01/12] docs: Update documentation with changes --- CHANGELOG.md | 15 ++++++++++++ docs/query-methods.md | 54 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2bb264..f8d0bcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ All notable changes to this project will be documented in this file. This project adhere to the [Semantic Versioning](http://semver.org/) standard. +## [3.2.0] Unreleased + +* Feature - Add support for nested sub-WHERE clauses in `paginate()` and `get_total_items()` methods. This allows building complex queries like `WHERE (col1 = 'a' OR col2 = 'b') AND col3 = 'c'`. +* Tweak - Rename hooks from `tec_common_*` prefix to `stellarwp_schema_*` prefix for consistency with the StellarWP namespace. + +### Breaking Changes + +The following hooks have been renamed: +- `tec_common_custom_table_query_pre_results` → `stellarwp_schema_custom_table_query_pre_results` +- `tec_common_custom_table_query_post_results` → `stellarwp_schema_custom_table_query_post_results` +- `tec_common_custom_table_query_results` → `stellarwp_schema_custom_table_query_results` +- `tec_common_custom_table_query_where` → `stellarwp_schema_custom_table_query_where` + +[3.2.0]: https://github.com/stellarwp/schema/releases/tag/3.2.0 + ## [3.1.4] 2025-10-16 * Fix - Handle array values correctly in the `get_*` query methods. diff --git a/docs/query-methods.md b/docs/query-methods.md index 3b2e9a7..62e8906 100644 --- a/docs/query-methods.md +++ b/docs/query-methods.md @@ -271,7 +271,7 @@ $args = [ 'orderby' => 'created_at', // Column to sort by. 'order' => 'DESC', // ASC or DESC. 'offset' => 0, // Starting offset. - 'query_operator' => 'AND', // AND or OR. + 'query_operator' => 'AND', // AND or OR for top-level conditions. // Column filters. [ @@ -284,6 +284,21 @@ $args = [ 'value' => 1000, 'operator' => '<', ], + + // Sub-WHERE clauses (v3.2.0+) - group conditions with their own operator. + [ + 'query_operator' => 'OR', // Operator for this group. + [ + 'column' => 'type', + 'value' => 'classic', + 'operator' => '=', + ], + [ + 'column' => 'type', + 'value' => 'premium', + 'operator' => '=', + ], + ], ]; ``` @@ -321,6 +336,33 @@ $results = Sandwiches::paginate( 1 ); +// Sub-WHERE clauses (v3.2.0+). +// Query: WHERE (type = 'classic' OR type = 'premium') AND price_cents < 1500 +$results = Sandwiches::paginate( + [ + [ + 'query_operator' => 'OR', + [ + 'column' => 'type', + 'value' => 'classic', + 'operator' => '=', + ], + [ + 'column' => 'type', + 'value' => 'premium', + 'operator' => '=', + ], + ], + [ + 'column' => 'price_cents', + 'value' => 1500, + 'operator' => '<', + ], + ], + 20, + 1 +); + // With JOIN. $results = Sandwiches::paginate( $args, @@ -498,28 +540,30 @@ Query methods provide hooks for customization: ```php // Before query execution. -add_action( 'tec_common_custom_table_query_pre_results', function( $args, $class ) { +add_action( 'stellarwp_schema_custom_table_query_pre_results', function( $args, $class ) { // Modify args, log, etc. }, 10, 2 ); // After query execution. -add_action( 'tec_common_custom_table_query_post_results', function( $results, $args, $class ) { +add_action( 'stellarwp_schema_custom_table_query_post_results', function( $results, $args, $class ) { // Log, analyze, etc. }, 10, 3 ); // Filter results. -add_filter( 'tec_common_custom_table_query_results', function( $results, $args, $class ) { +add_filter( 'stellarwp_schema_custom_table_query_results', function( $results, $args, $class ) { // Modify results. return $results; }, 10, 3 ); // Filter WHERE clause. -add_filter( 'tec_common_custom_table_query_where', function( $where, $args, $class ) { +add_filter( 'stellarwp_schema_custom_table_query_where', function( $where, $args, $class ) { // Add custom WHERE conditions. return $where; }, 10, 3 ); ``` +> **Note (v3.2.0):** The hook names were changed from `tec_common_*` prefix to `stellarwp_schema_*` prefix. If you're upgrading from an earlier version, update your hook callbacks accordingly. + ## Performance Tips 1. **Batch Operations**: Use `insert_many()` and `update_many()` for bulk operations From 4f34cf59bb8ceb13e09c78fcd188462fb1017def Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Fri, 19 Dec 2025 21:08:45 +0200 Subject: [PATCH 02/12] feat: Introduce support for sub query clauses --- .../Traits/Custom_Table_Query_Methods.php | 95 ++++++++++++++----- 1 file changed, 73 insertions(+), 22 deletions(-) diff --git a/src/Schema/Traits/Custom_Table_Query_Methods.php b/src/Schema/Traits/Custom_Table_Query_Methods.php index 095c065..f76f137 100644 --- a/src/Schema/Traits/Custom_Table_Query_Methods.php +++ b/src/Schema/Traits/Custom_Table_Query_Methods.php @@ -468,7 +468,7 @@ public static function paginate( array $args, int $per_page = 20, int $page = 1, * @param array $args The query arguments. * @param class-string $class The class name. */ - do_action( 'tec_common_custom_table_query_pre_results', $args, static::class ); + do_action( 'stellarwp_schema_custom_table_query_pre_results', $args, static::class ); $database = Config::get_db(); @@ -498,7 +498,7 @@ public static function paginate( array $args, int $per_page = 20, int $page = 1, * @param array $args The query arguments. * @param class-string $class The class name. */ - do_action( 'tec_common_custom_table_query_post_results', $results, $args, static::class ); + do_action( 'stellarwp_schema_custom_table_query_post_results', $results, $args, static::class ); /** * Filters the results of the query. @@ -509,13 +509,14 @@ public static function paginate( array $args, int $per_page = 20, int $page = 1, * @param array $args The query arguments. * @param class-string $class The class name. */ - return apply_filters( 'tec_common_custom_table_query_results', $results, $args, static::class ); + return apply_filters( 'stellarwp_schema_custom_table_query_results', $results, $args, static::class ); } /** * Builds a WHERE clause from the provided arguments. * * @since 3.0.0 + * @since 3.2.0 Now sub where clauses are supported. * * @param array $args The query arguments. * @@ -552,12 +553,58 @@ protected static function build_where_from_args( array $args = [] ): string { $columns = static::get_columns()->get_names(); + $sub_wheres = self::build_sub_wheres_from_args( + array_filter( $args,static fn( $arg ) => is_array( $arg ) ), + $columns, + $joined_prefix + ); + + $where = array_merge( $where, $sub_wheres ); + + /** + * Filters the WHERE clause. + * + * @since 3.0.0 + * + * @param array $where The WHERE clause parts. + * @param array $args The query arguments. + * @param class-string $class The class name. + */ + $where = apply_filters( 'stellarwp_schema_custom_table_query_where', array_filter( $where ), $args, static::class ); + + if ( empty( $where ) ) { + return ''; + } + + return 'WHERE ' . implode( " {$query_operator} ", $where ); + } + + /** + * Builds the sub WHERE clauses from the provided arguments. + * + * @since 3.2.0 + * + * @param array $args The query arguments. + * @param array $columns The columns to select. + * @param string $joined_prefix The prefix to use for the joined columns. + * + * @return array The sub WHERE clauses. + */ + private static function build_sub_wheres_from_args( array $args = [], array $columns = [], string $joined_prefix = '' ): array { + $sub_wheres = []; + foreach ( $args as $arg ) { if ( ! is_array( $arg ) ) { continue; } if ( empty( $arg['column'] ) ) { + if ( ! empty( $arg[0]['column'] ) ) { + $sub_wheres[] = [ + 'queries' => self::build_sub_wheres_from_args( $arg, $columns, $joined_prefix ), + 'operator' => ! empty( $arg['query_operator'] ) && in_array( strtoupper( $arg['query_operator'] ), [ 'AND', 'OR' ], true ) ? strtoupper( $arg['query_operator'] ) : 'AND', + ]; + } continue; } @@ -589,34 +636,38 @@ protected static function build_where_from_args( array $args = [] ): string { $query = "{$joined_prefix}{$column} {$operator} {$placeholder}"; if ( is_array( $value ) ) { - $where[] = $database::prepare( $query, ...$value ); + $sub_wheres[] = $database::prepare( $query, ...$value ); continue; } if ( 'NULL' === $placeholder ) { - $where[] = $query; + $sub_wheres[] = $query; continue; } - $where[] = $database::prepare( $query, $value ); - } - - /** - * Filters the WHERE clause. - * - * @since 3.0.0 - * - * @param array $where The WHERE clause parts. - * @param array $args The query arguments. - * @param class-string $class The class name. - */ - $where = apply_filters( 'tec_common_custom_table_query_where', array_filter( $where ), $args, static::class ); - - if ( empty( $where ) ) { - return ''; + $sub_wheres[] = $database::prepare( $query, $value ); } - return 'WHERE ' . implode( " {$query_operator} ", $where ); + return array_filter( + array_map( + static function ( $sub_where ) { + if ( ! is_array( $sub_where ) ) { + return $sub_where; + } + + if ( empty( $sub_where['queries'] ) ) { + return ''; + } + + if ( ! isset( $sub_where['operator'] ) || ! in_array( strtoupper( $sub_where['operator'] ), [ 'AND', 'OR' ], true ) ) { + $sub_where['operator'] = 'AND'; + } + + return '(' . implode( " {$sub_where['operator']} ", $sub_where['queries'] ) . ')'; + }, + $sub_wheres + ) + ); } /** From 4f879e77e4ec427678e2c287462313777f4ebd24 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Fri, 19 Dec 2025 21:10:25 +0200 Subject: [PATCH 03/12] tests: Cover the added functionalities with tests --- .../Traits/Custom_Table_Query_MethodsTest.php | 269 ++++++++++++++++++ 1 file changed, 269 insertions(+) diff --git a/tests/wpunit/Traits/Custom_Table_Query_MethodsTest.php b/tests/wpunit/Traits/Custom_Table_Query_MethodsTest.php index 52d4276..227d386 100644 --- a/tests/wpunit/Traits/Custom_Table_Query_MethodsTest.php +++ b/tests/wpunit/Traits/Custom_Table_Query_MethodsTest.php @@ -236,6 +236,275 @@ public function should_handle_scalar_values_in_queries() { $this->assertEquals( 'Scalar Test', $result['name'] ); } + /** + * @test + */ + public function should_paginate_with_simple_where_clause() { + $table = $this->get_query_test_table(); + Register::table( $table ); + + // Insert test data + $table::insert( [ 'name' => 'Item 1', 'slug' => 'item-1', 'status' => 1 ] ); + $table::insert( [ 'name' => 'Item 2', 'slug' => 'item-2', 'status' => 1 ] ); + $table::insert( [ 'name' => 'Item 3', 'slug' => 'item-3', 'status' => 0 ] ); + $table::insert( [ 'name' => 'Item 4', 'slug' => 'item-4', 'status' => 1 ] ); + + // Query with simple where clause + $results = $table::paginate( + [ + [ + 'column' => 'status', + 'value' => 1, + 'operator' => '=', + ], + ], + 10, + 1 + ); + + $this->assertCount( 3, $results ); + } + + /** + * @test + */ + public function should_paginate_with_sub_where_clauses_using_or() { + $table = $this->get_query_test_table(); + Register::table( $table ); + + // Insert test data + $table::insert( [ 'name' => 'Alpha', 'slug' => 'alpha', 'status' => 1 ] ); + $table::insert( [ 'name' => 'Beta', 'slug' => 'beta', 'status' => 0 ] ); + $table::insert( [ 'name' => 'Gamma', 'slug' => 'gamma', 'status' => 1 ] ); + $table::insert( [ 'name' => 'Delta', 'slug' => 'delta', 'status' => 0 ] ); + + // Query: WHERE (slug = 'alpha' OR slug = 'beta') + $results = $table::paginate( + [ + [ + 'query_operator' => 'OR', + [ + 'column' => 'slug', + 'value' => 'alpha', + 'operator' => '=', + ], + [ + 'column' => 'slug', + 'value' => 'beta', + 'operator' => '=', + ], + ], + ], + 10, + 1 + ); + + $this->assertCount( 2, $results ); + $slugs = array_column( $results, 'slug' ); + $this->assertContains( 'alpha', $slugs ); + $this->assertContains( 'beta', $slugs ); + } + + /** + * @test + */ + public function should_paginate_with_nested_sub_where_clauses() { + $table = $this->get_query_test_table(); + Register::table( $table ); + + // Insert test data + $table::insert( [ 'name' => 'Active Alpha', 'slug' => 'alpha', 'status' => 1 ] ); + $table::insert( [ 'name' => 'Inactive Alpha', 'slug' => 'alpha-inactive', 'status' => 0 ] ); + $table::insert( [ 'name' => 'Active Beta', 'slug' => 'beta', 'status' => 1 ] ); + $table::insert( [ 'name' => 'Inactive Beta', 'slug' => 'beta-inactive', 'status' => 0 ] ); + $table::insert( [ 'name' => 'Active Gamma', 'slug' => 'gamma', 'status' => 1 ] ); + + // Query: WHERE (slug = 'alpha' OR slug = 'beta') AND status = 1 + $results = $table::paginate( + [ + [ + 'query_operator' => 'OR', + [ + 'column' => 'slug', + 'value' => 'alpha', + 'operator' => '=', + ], + [ + 'column' => 'slug', + 'value' => 'beta', + 'operator' => '=', + ], + ], + [ + 'column' => 'status', + 'value' => 1, + 'operator' => '=', + ], + ], + 10, + 1 + ); + + $this->assertCount( 2, $results ); + $slugs = array_column( $results, 'slug' ); + $this->assertContains( 'alpha', $slugs ); + $this->assertContains( 'beta', $slugs ); + + // Verify all results have status = 1 + foreach ( $results as $result ) { + $this->assertEquals( 1, $result['status'] ); + } + } + + /** + * @test + */ + public function should_paginate_with_multiple_sub_where_groups() { + $table = $this->get_query_test_table(); + Register::table( $table ); + + // Insert test data + $table::insert( [ 'name' => 'A', 'slug' => 'a', 'status' => 1 ] ); + $table::insert( [ 'name' => 'B', 'slug' => 'b', 'status' => 0 ] ); + $table::insert( [ 'name' => 'C', 'slug' => 'c', 'status' => 1 ] ); + $table::insert( [ 'name' => 'D', 'slug' => 'd', 'status' => 0 ] ); + + // Query with OR at the top level: WHERE (slug = 'a') OR (slug = 'b') + $results = $table::paginate( + [ + 'query_operator' => 'OR', + [ + 'column' => 'slug', + 'value' => 'a', + 'operator' => '=', + ], + [ + 'column' => 'slug', + 'value' => 'b', + 'operator' => '=', + ], + ], + 10, + 1 + ); + + $this->assertCount( 2, $results ); + } + + /** + * @test + */ + public function should_count_total_items_with_sub_where_clauses() { + $table = $this->get_query_test_table(); + Register::table( $table ); + + // Insert test data + $table::insert( [ 'name' => 'X', 'slug' => 'x', 'status' => 1 ] ); + $table::insert( [ 'name' => 'Y', 'slug' => 'y', 'status' => 0 ] ); + $table::insert( [ 'name' => 'Z', 'slug' => 'z', 'status' => 1 ] ); + + // Count with sub-where: WHERE (slug = 'x' OR slug = 'y') + $total = $table::get_total_items( + [ + [ + 'query_operator' => 'OR', + [ + 'column' => 'slug', + 'value' => 'x', + 'operator' => '=', + ], + [ + 'column' => 'slug', + 'value' => 'y', + 'operator' => '=', + ], + ], + ] + ); + + $this->assertEquals( 2, $total ); + } + + /** + * @test + */ + public function should_handle_sub_where_with_different_operators() { + $table = $this->get_query_test_table(); + Register::table( $table ); + + // Insert test data + $table::insert( [ 'name' => 'First', 'slug' => 'first', 'status' => 1 ] ); + $table::insert( [ 'name' => 'Second', 'slug' => 'second', 'status' => 2 ] ); + $table::insert( [ 'name' => 'Third', 'slug' => 'third', 'status' => 3 ] ); + $table::insert( [ 'name' => 'Fourth', 'slug' => 'fourth', 'status' => 4 ] ); + + // Query: WHERE (status > 1 AND status < 4) + $results = $table::paginate( + [ + [ + 'query_operator' => 'AND', + [ + 'column' => 'status', + 'value' => 1, + 'operator' => '>', + ], + [ + 'column' => 'status', + 'value' => 4, + 'operator' => '<', + ], + ], + ], + 10, + 1 + ); + + $this->assertCount( 2, $results ); + $statuses = array_column( $results, 'status' ); + $this->assertContains( 2, $statuses ); + $this->assertContains( 3, $statuses ); + } + + /** + * @test + */ + public function should_use_stellarwp_schema_hook_prefix() { + $table = $this->get_query_test_table(); + Register::table( $table ); + + $table::insert( [ 'name' => 'Hook Test', 'slug' => 'hook-test', 'status' => 1 ] ); + + $pre_results_fired = false; + $post_results_fired = false; + $results_filter_fired = false; + $where_filter_fired = false; + + add_action( 'stellarwp_schema_custom_table_query_pre_results', function() use ( &$pre_results_fired ) { + $pre_results_fired = true; + } ); + + add_action( 'stellarwp_schema_custom_table_query_post_results', function() use ( &$post_results_fired ) { + $post_results_fired = true; + } ); + + add_filter( 'stellarwp_schema_custom_table_query_results', function( $results ) use ( &$results_filter_fired ) { + $results_filter_fired = true; + return $results; + } ); + + add_filter( 'stellarwp_schema_custom_table_query_where', function( $where ) use ( &$where_filter_fired ) { + $where_filter_fired = true; + return $where; + } ); + + $table::paginate( [], 10, 1 ); + + $this->assertTrue( $pre_results_fired, 'stellarwp_schema_custom_table_query_pre_results action should fire' ); + $this->assertTrue( $post_results_fired, 'stellarwp_schema_custom_table_query_post_results action should fire' ); + $this->assertTrue( $results_filter_fired, 'stellarwp_schema_custom_table_query_results filter should fire' ); + $this->assertTrue( $where_filter_fired, 'stellarwp_schema_custom_table_query_where filter should fire' ); + } + /** * Get a test table for query method testing. */ From ee946f250d298a621c4b610e4d3dea528c14361e Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Mon, 22 Dec 2025 18:19:47 +0200 Subject: [PATCH 04/12] Update src/Schema/Traits/Custom_Table_Query_Methods.php Co-authored-by: theAverageDev (Luca Tumedei) --- src/Schema/Traits/Custom_Table_Query_Methods.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Schema/Traits/Custom_Table_Query_Methods.php b/src/Schema/Traits/Custom_Table_Query_Methods.php index f76f137..879b521 100644 --- a/src/Schema/Traits/Custom_Table_Query_Methods.php +++ b/src/Schema/Traits/Custom_Table_Query_Methods.php @@ -554,7 +554,7 @@ protected static function build_where_from_args( array $args = [] ): string { $columns = static::get_columns()->get_names(); $sub_wheres = self::build_sub_wheres_from_args( - array_filter( $args,static fn( $arg ) => is_array( $arg ) ), + array_filter( $args, 'is_array' ), $columns, $joined_prefix ); From edf3f87fe124a430b7b9520e96cee065a5e1a46d Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 24 Dec 2025 13:21:29 +0200 Subject: [PATCH 05/12] feat: add a filter to allow complete query modifications for paginate method --- .../Traits/Custom_Table_Query_Methods.php | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/Schema/Traits/Custom_Table_Query_Methods.php b/src/Schema/Traits/Custom_Table_Query_Methods.php index f76f137..e377c0f 100644 --- a/src/Schema/Traits/Custom_Table_Query_Methods.php +++ b/src/Schema/Traits/Custom_Table_Query_Methods.php @@ -472,13 +472,43 @@ public static function paginate( array $args, int $per_page = 20, int $page = 1, $database = Config::get_db(); - $results = $database::get_results( + /** + * Filters the query used to paginate the results. + * + * @since 3.2.0 + * + * @param string $query The query to use. + * @param array $args The query arguments. + * @param int $per_page The number of items to display per page. + * @param int $page The current page number. + * @param array $columns The columns to select. + * @param string $join_table The table to join. + * @param string $join_condition The condition to join on. + * @param array $selectable_joined_columns The columns from the joined table to select. + * @param int $offset The offset to use. + * @param string $where The WHERE clause to use. + */ + $query = apply_filters( + 'stellarwp_schema_custom_table_paginate_query', $database::prepare( "SELECT {$formatted_columns}{$secondary_columns} FROM %i a {$join} {$where} ORDER BY a.{$orderby} {$order} LIMIT %d, %d", static::table_name( true ), $offset, $per_page ), + $args, + $per_page, + $page, + $columns, + $join_table, + $join_condition, + $selectable_joined_columns, + $offset, + $where, + ); + + $results = $database::get_results( + $query, ARRAY_A ); From ed3b561e52aaa2694a744705f72bd30677883147 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 24 Dec 2025 13:21:43 +0200 Subject: [PATCH 06/12] docs: update docs with the added filter --- CHANGELOG.md | 1 + docs/query-methods.md | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8d0bcd..18e66f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. This projec ## [3.2.0] Unreleased * Feature - Add support for nested sub-WHERE clauses in `paginate()` and `get_total_items()` methods. This allows building complex queries like `WHERE (col1 = 'a' OR col2 = 'b') AND col3 = 'c'`. +* Feature - Add new `stellarwp_schema_custom_table_paginate_query` filter to allow modification of the paginate query before execution. * Tweak - Rename hooks from `tec_common_*` prefix to `stellarwp_schema_*` prefix for consistency with the StellarWP namespace. ### Breaking Changes diff --git a/docs/query-methods.md b/docs/query-methods.md index 62e8906..2de0e55 100644 --- a/docs/query-methods.md +++ b/docs/query-methods.md @@ -560,6 +560,14 @@ add_filter( 'stellarwp_schema_custom_table_query_where', function( $where, $args // Add custom WHERE conditions. return $where; }, 10, 3 ); + +// Filter the paginate query before execution (v3.2.0+). +add_filter( 'stellarwp_schema_custom_table_paginate_query', function( $query ) { + // Modify the SQL query string before execution. + // Useful for debugging, logging, or query modifications. + error_log( "Paginate query: {$query}" ); + return $query; +} ); ``` > **Note (v3.2.0):** The hook names were changed from `tec_common_*` prefix to `stellarwp_schema_*` prefix. If you're upgrading from an earlier version, update your hook callbacks accordingly. @@ -577,4 +585,4 @@ add_filter( 'stellarwp_schema_custom_table_query_where', function( $where, $args - [Column System](columns.md) - [Index System](indexes.md) - [Table Schemas](schemas-table.md) -- [Migrating from v2 to v3](migrating-from-v2-to-v3.md) \ No newline at end of file +- [Migrating from v2 to v3](migrating-from-v2-to-v3.md) From c404715bc5aebadde0e054394d985657c69432ce Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 24 Dec 2025 13:24:44 +0200 Subject: [PATCH 07/12] chore: include snapshot assertion lib --- composer.json | 3 +- composer.lock | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 49be587..3cc3c5b 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,8 @@ "symfony/event-dispatcher-contracts": "^2.5.1", "symfony/string": "^5.4", "szepeviktor/phpstan-wordpress": "^1.1", - "php-stubs/wp-cli-stubs": "^2.11" + "php-stubs/wp-cli-stubs": "^2.11", + "lucatume/codeception-snapshot-assertions": "^0.5.0" }, "minimum-stability": "stable", "autoload": { diff --git a/composer.lock b/composer.lock index 2c9f40c..c5d2519 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4520621c62ecebc167f526b56e02c36f", + "content-hash": "f0a1786f04bcbc622161f202cbba9d18", "packages": [ { "name": "psr/container", @@ -2054,6 +2054,55 @@ }, "time": "2021-04-17T13:49:01+00:00" }, + { + "name": "gajus/dindent", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/gajus/dindent.git", + "reference": "d81c3a6f78fbe1ab26f5e753098bbbef6b6a9f3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/gajus/dindent/zipball/d81c3a6f78fbe1ab26f5e753098bbbef6b6a9f3c", + "reference": "d81c3a6f78fbe1ab26f5e753098bbbef6b6a9f3c", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "satooshi/php-coveralls": "dev-master" + }, + "type": "library", + "autoload": { + "psr-4": { + "Gajus\\Dindent\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Gajus Kuizinas", + "email": "gk@anuary.com" + } + ], + "description": "HTML indentation library for development and testing.", + "homepage": "https://github.com/gajus/dindent", + "keywords": [ + "format", + "html", + "indent" + ], + "support": { + "issues": "https://github.com/gajus/dindent/issues", + "source": "https://github.com/gajus/dindent/tree/master" + }, + "time": "2014-10-08T10:03:04+00:00" + }, { "name": "gettext/gettext", "version": "v4.8.12", @@ -3041,6 +3090,51 @@ ], "time": "2024-09-21T08:32:55+00:00" }, + { + "name": "lucatume/codeception-snapshot-assertions", + "version": "0.5.0", + "source": { + "type": "git", + "url": "https://github.com/lucatume/codeception-snapshot-assertions.git", + "reference": "4583b0e9337d0ddd82d82710e4467b06f2915ba9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lucatume/codeception-snapshot-assertions/zipball/4583b0e9337d0ddd82d82710e4467b06f2915ba9", + "reference": "4583b0e9337d0ddd82d82710e4467b06f2915ba9", + "shasum": "" + }, + "require": { + "codeception/codeception": "^4.0", + "ext-dom": "*", + "gajus/dindent": "^2.0" + }, + "require-dev": { + "squizlabs/php_codesniffer": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "tad\\Codeception\\SnapshotAssertions\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "theAverageDev (Luca Tumedei)", + "email": "luca@theaveragedev.com" + } + ], + "description": "Snapshot assertions sugar for Codeception.", + "support": { + "issues": "https://github.com/lucatume/codeception-snapshot-assertions/issues", + "source": "https://github.com/lucatume/codeception-snapshot-assertions/tree/0.5.0" + }, + "time": "2025-07-01T07:40:18+00:00" + }, { "name": "lucatume/di52", "version": "4.0.1", From 8020d592e8d85867d84711dbd02ba6952bf0fd4a Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Fri, 26 Dec 2025 20:14:57 +0200 Subject: [PATCH 08/12] tweak: better check for array of sub-wheres --- src/Schema/Traits/Custom_Table_Query_Methods.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Schema/Traits/Custom_Table_Query_Methods.php b/src/Schema/Traits/Custom_Table_Query_Methods.php index bb4e1ce..bf22c01 100644 --- a/src/Schema/Traits/Custom_Table_Query_Methods.php +++ b/src/Schema/Traits/Custom_Table_Query_Methods.php @@ -629,7 +629,8 @@ private static function build_sub_wheres_from_args( array $args = [], array $col } if ( empty( $arg['column'] ) ) { - if ( ! empty( $arg[0]['column'] ) ) { + $check_for_sub_query = array_filter( array_values( $arg ), static fn( $value ) => ! empty( $value['column'] ) ); + if ( ! empty( $check_for_sub_query ) ) { $sub_wheres[] = [ 'queries' => self::build_sub_wheres_from_args( $arg, $columns, $joined_prefix ), 'operator' => ! empty( $arg['query_operator'] ) && in_array( strtoupper( $arg['query_operator'] ), [ 'AND', 'OR' ], true ) ? strtoupper( $arg['query_operator'] ) : 'AND', From 5fa89101dbaf201aa7405241c9db5a54903662df Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Fri, 26 Dec 2025 20:45:22 +0200 Subject: [PATCH 09/12] feature: introduced new `stellarwp_schema_custom_table_total_items_query` filter --- CHANGELOG.md | 1 + docs/query-methods.md | 8 ++++++++ .../Traits/Custom_Table_Query_Methods.php | 20 +++++++++++++++++-- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18e66f1..10ba6a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. This projec * Feature - Add support for nested sub-WHERE clauses in `paginate()` and `get_total_items()` methods. This allows building complex queries like `WHERE (col1 = 'a' OR col2 = 'b') AND col3 = 'c'`. * Feature - Add new `stellarwp_schema_custom_table_paginate_query` filter to allow modification of the paginate query before execution. +* Feature - Add new `stellarwp_schema_custom_table_total_items_query` filter to allow modification of the total items count query before execution. * Tweak - Rename hooks from `tec_common_*` prefix to `stellarwp_schema_*` prefix for consistency with the StellarWP namespace. ### Breaking Changes diff --git a/docs/query-methods.md b/docs/query-methods.md index 2de0e55..fdfb4b5 100644 --- a/docs/query-methods.md +++ b/docs/query-methods.md @@ -568,6 +568,14 @@ add_filter( 'stellarwp_schema_custom_table_paginate_query', function( $query ) { error_log( "Paginate query: {$query}" ); return $query; } ); + +// Filter the total items count query before execution (v3.2.0+). +add_filter( 'stellarwp_schema_custom_table_total_items_query', function( $query ) { + // Modify the SQL count query string before execution. + // Useful for debugging, logging, or query modifications. + error_log( "Total items query: {$query}" ); + return $query; +} ); ``` > **Note (v3.2.0):** The hook names were changed from `tec_common_*` prefix to `stellarwp_schema_*` prefix. If you're upgrading from an earlier version, update your hook callbacks accordingly. diff --git a/src/Schema/Traits/Custom_Table_Query_Methods.php b/src/Schema/Traits/Custom_Table_Query_Methods.php index bf22c01..4e88682 100644 --- a/src/Schema/Traits/Custom_Table_Query_Methods.php +++ b/src/Schema/Traits/Custom_Table_Query_Methods.php @@ -324,12 +324,26 @@ public static function get_total_items( array $args = [] ): int { $database = Config::get_db(); $where = static::build_where_from_args( $args ); - return (int) $database::get_var( + /** + * Filters the query used to get the total number of items in the table. + * + * @since 3.2.0 + * + * @param string $query The query to use. + * @param array $args The query arguments. + * @param string $where The WHERE clause to use. + */ + $query = apply_filters( + 'stellarwp_schema_custom_table_total_items_query', $database::prepare( "SELECT COUNT(*) FROM %i a {$where}", static::table_name( true ) - ) + ), + $args, + $where, ); + + return (int) $database::get_var( $query ); } /** @@ -635,6 +649,8 @@ private static function build_sub_wheres_from_args( array $args = [], array $col 'queries' => self::build_sub_wheres_from_args( $arg, $columns, $joined_prefix ), 'operator' => ! empty( $arg['query_operator'] ) && in_array( strtoupper( $arg['query_operator'] ), [ 'AND', 'OR' ], true ) ? strtoupper( $arg['query_operator'] ) : 'AND', ]; + } else { + _doing_it_wrong( __METHOD__, 'A sub where clause must contain a column.', '3.2.0' ); } continue; } From bce6161d131f4e39ef5d12d0fc85e8e4e9970003 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Fri, 26 Dec 2025 21:20:05 +0200 Subject: [PATCH 10/12] adding more test coverage --- .../Traits/Query_Snapshot_Assertions.php | 91 +++ .../Traits/Custom_Table_Query_MethodsTest.php | 591 ++++++++++++++++-- ...query_with_paginate_filter__0.snapshot.php | 1 + ...ems_with_sub_where_clauses__0.snapshot.php | 1 + ...nesting_with_all_operators__0.snapshot.php | 1 + ...ted_sub_wheres_four_levels__0.snapshot.php | 1 + ...ndle_empty_sub_where_group__0.snapshot.php | 1 + ...e_with_different_operators__0.snapshot.php | 1 + ...ere_with_missing_value_key__0.snapshot.php | 1 + ..._where_with_missing_column__0.snapshot.php | 1 + ...e_with_invalid_column_name__0.snapshot.php | 1 + ...re_with_missing_column_key__0.snapshot.php | 1 + ...invalid_operator_to_equals__0.snapshot.php | 1 + ...uery_operator_in_sub_where__0.snapshot.php | 1 + ..._multiple_sub_where_groups__0.snapshot.php | 1 + ...h_nested_sub_where_clauses__0.snapshot.php | 1 + ...e_with_simple_where_clause__0.snapshot.php | 1 + ...sub_where_clauses_using_or__0.snapshot.php | 1 + ...query_for_nested_sub_where__0.snapshot.php | 1 + ...ect_query_for_or_sub_where__0.snapshot.php | 1 + ...query_for_simple_sub_where__0.snapshot.php | 1 + 21 files changed, 666 insertions(+), 35 deletions(-) create mode 100644 tests/_support/Traits/Query_Snapshot_Assertions.php create mode 100644 tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_capture_query_with_paginate_filter__0.snapshot.php create mode 100644 tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_count_total_items_with_sub_where_clauses__0.snapshot.php create mode 100644 tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_handle_complex_four_level_nesting_with_all_operators__0.snapshot.php create mode 100644 tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_handle_deeply_nested_sub_wheres_four_levels__0.snapshot.php create mode 100644 tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_handle_empty_sub_where_group__0.snapshot.php create mode 100644 tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_handle_sub_where_with_different_operators__0.snapshot.php create mode 100644 tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_handle_sub_where_with_missing_value_key__0.snapshot.php create mode 100644 tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_ignore_nested_sub_where_with_missing_column__0.snapshot.php create mode 100644 tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_ignore_sub_where_with_invalid_column_name__0.snapshot.php create mode 100644 tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_ignore_sub_where_with_missing_column_key__0.snapshot.php create mode 100644 tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_normalize_invalid_operator_to_equals__0.snapshot.php create mode 100644 tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_normalize_invalid_query_operator_in_sub_where__0.snapshot.php create mode 100644 tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_paginate_with_multiple_sub_where_groups__0.snapshot.php create mode 100644 tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_paginate_with_nested_sub_where_clauses__0.snapshot.php create mode 100644 tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_paginate_with_simple_where_clause__0.snapshot.php create mode 100644 tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_paginate_with_sub_where_clauses_using_or__0.snapshot.php create mode 100644 tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_produce_correct_query_for_nested_sub_where__0.snapshot.php create mode 100644 tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_produce_correct_query_for_or_sub_where__0.snapshot.php create mode 100644 tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_produce_correct_query_for_simple_sub_where__0.snapshot.php diff --git a/tests/_support/Traits/Query_Snapshot_Assertions.php b/tests/_support/Traits/Query_Snapshot_Assertions.php new file mode 100644 index 0000000..b1e6e02 --- /dev/null +++ b/tests/_support/Traits/Query_Snapshot_Assertions.php @@ -0,0 +1,91 @@ +captured_query = ''; + + add_filter( 'stellarwp_schema_custom_table_paginate_query', function( $query ) { + $this->captured_query = $query; + return $query; + }, 10, 1 ); + + add_filter( 'stellarwp_schema_custom_table_total_items_query', function( $query ) { + $this->captured_query = $query; + return $query; + }, 10, 1 ); + } + + /** + * Tears down the query capture filter. + * + * @after + * + * @return void + */ + protected function tear_down_query_capture(): void { + $this->captured_query = ''; + + remove_all_filters( 'stellarwp_schema_custom_table_paginate_query' ); + remove_all_filters( 'stellarwp_schema_custom_table_total_items_query' ); + } + + /** + * Gets the captured query. + * + * @return string + */ + protected function get_captured_query(): string { + return $this->captured_query; + } + + /** + * Asserts that the captured query matches a stored snapshot. + * + * @return void + */ + protected function assertCapturedQueryMatchesSnapshot(): void { + $this->assertNotEmpty( $this->captured_query ); + $this->assertMatchesCodeSnapshot( $this->captured_query ); + } + + /** + * Asserts that the captured query contains the expected pattern. + * + * @param string $expected_pattern The expected pattern. + * @param string $message Optional message. + * + * @return void + */ + protected function assertQueryContains( string $expected_pattern, string $message = '' ): void { + $this->assertStringContainsString( + $expected_pattern, + $this->captured_query, + $message ?: 'Query does not contain expected pattern' + ); + } +} diff --git a/tests/wpunit/Traits/Custom_Table_Query_MethodsTest.php b/tests/wpunit/Traits/Custom_Table_Query_MethodsTest.php index 227d386..55edd43 100644 --- a/tests/wpunit/Traits/Custom_Table_Query_MethodsTest.php +++ b/tests/wpunit/Traits/Custom_Table_Query_MethodsTest.php @@ -5,6 +5,7 @@ use StellarWP\Schema\Register; use StellarWP\Schema\Tests\SchemaTestCase; use StellarWP\Schema\Tests\Traits\Table_Fixtures; +use StellarWP\Schema\Tests\Traits\Query_Snapshot_Assertions; use StellarWP\Schema\Columns\Integer_Column; use StellarWP\Schema\Columns\String_Column; use StellarWP\Schema\Columns\ID; @@ -16,6 +17,7 @@ class Custom_Table_Query_MethodsTest extends SchemaTestCase { use Table_Fixtures; + use Query_Snapshot_Assertions; /** * @before @@ -85,24 +87,18 @@ public function should_get_all_by_with_array_values() { 'status' => 1, ] ); - $id1 = DB::last_insert_id(); - $table::insert( [ 'name' => 'Test 2', 'slug' => 'test-2', 'status' => 1, ] ); - $id2 = DB::last_insert_id(); - $table::insert( [ 'name' => 'Test 3', 'slug' => 'test-3', 'status' => 0, ] ); - $id3 = DB::last_insert_id(); - // Get all by status using array (simulating IN operator scenario) $results = $table::get_all_by( 'status', [ 1, 0 ], 'IN' ); @@ -131,24 +127,18 @@ public function should_handle_empty_array() { 'status' => 1, ] ); - $id1 = DB::last_insert_id(); - $table::insert( [ 'name' => 'Test 2', 'slug' => 'test-2', 'status' => 1, ] ); - $id2 = DB::last_insert_id(); - $table::insert( [ 'name' => 'Test 3', 'slug' => 'test-3', 'status' => 0, ] ); - $id3 = DB::last_insert_id(); - $results = $table::get_all_by( 'status', [], 'IN' ); $this->assertEmpty( $results ); @@ -263,6 +253,7 @@ public function should_paginate_with_simple_where_clause() { ); $this->assertCount( 3, $results ); + $this->assertCapturedQueryMatchesSnapshot(); } /** @@ -303,6 +294,7 @@ public function should_paginate_with_sub_where_clauses_using_or() { $slugs = array_column( $results, 'slug' ); $this->assertContains( 'alpha', $slugs ); $this->assertContains( 'beta', $slugs ); + $this->assertCapturedQueryMatchesSnapshot(); } /** @@ -354,6 +346,7 @@ public function should_paginate_with_nested_sub_where_clauses() { foreach ( $results as $result ) { $this->assertEquals( 1, $result['status'] ); } + $this->assertCapturedQueryMatchesSnapshot(); } /** @@ -389,6 +382,7 @@ public function should_paginate_with_multiple_sub_where_groups() { ); $this->assertCount( 2, $results ); + $this->assertCapturedQueryMatchesSnapshot(); } /** @@ -423,6 +417,7 @@ public function should_count_total_items_with_sub_where_clauses() { ); $this->assertEquals( 2, $total ); + $this->assertCapturedQueryMatchesSnapshot(); } /** @@ -463,6 +458,7 @@ public function should_handle_sub_where_with_different_operators() { $statuses = array_column( $results, 'status' ); $this->assertContains( 2, $statuses ); $this->assertContains( 3, $statuses ); + $this->assertCapturedQueryMatchesSnapshot(); } /** @@ -474,35 +470,560 @@ public function should_use_stellarwp_schema_hook_prefix() { $table::insert( [ 'name' => 'Hook Test', 'slug' => 'hook-test', 'status' => 1 ] ); - $pre_results_fired = false; - $post_results_fired = false; - $results_filter_fired = false; - $where_filter_fired = false; + $action_count_before = did_action( 'stellarwp_schema_custom_table_query_pre_results' ); + $action_count_after = did_action( 'stellarwp_schema_custom_table_query_post_results' ); + $filter_count_before = did_filter( 'stellarwp_schema_custom_table_query_results' ); + $filter_count_after = did_filter( 'stellarwp_schema_custom_table_query_where' ); - add_action( 'stellarwp_schema_custom_table_query_pre_results', function() use ( &$pre_results_fired ) { - $pre_results_fired = true; - } ); + $table::paginate( [], 10, 1 ); - add_action( 'stellarwp_schema_custom_table_query_post_results', function() use ( &$post_results_fired ) { - $post_results_fired = true; - } ); + $this->assertSame( $action_count_before + 1, did_action( 'stellarwp_schema_custom_table_query_pre_results' ), 'stellarwp_schema_custom_table_query_pre_results action should fire' ); + $this->assertSame( $action_count_after + 1, did_action( 'stellarwp_schema_custom_table_query_post_results' ), 'stellarwp_schema_custom_table_query_post_results action should fire' ); + $this->assertSame( $filter_count_before + 1, did_filter( 'stellarwp_schema_custom_table_query_results' ), 'stellarwp_schema_custom_table_query_results filter should fire' ); + $this->assertSame( $filter_count_after + 1, did_filter( 'stellarwp_schema_custom_table_query_where' ), 'stellarwp_schema_custom_table_query_where filter should fire' ); + } + + /** + * @test + */ + public function should_ignore_sub_where_with_missing_column_key() { + $table = $this->get_query_test_table(); + Register::table( $table ); - add_filter( 'stellarwp_schema_custom_table_query_results', function( $results ) use ( &$results_filter_fired ) { - $results_filter_fired = true; - return $results; - } ); + $table::insert( [ 'name' => 'Test 1', 'slug' => 'test-1', 'status' => 1 ] ); + $table::insert( [ 'name' => 'Test 2', 'slug' => 'test-2', 'status' => 0 ] ); - add_filter( 'stellarwp_schema_custom_table_query_where', function( $where ) use ( &$where_filter_fired ) { - $where_filter_fired = true; - return $where; - } ); + $doing_it_wrong_called = false; + uopz_set_return( '_doing_it_wrong', function() use ( &$doing_it_wrong_called ) { + $doing_it_wrong_called = true; + }, true ); - $table::paginate( [], 10, 1 ); + // Malformed: missing 'column' key. + $results = $table::paginate( + [ + [ + 'value' => 'test-1', + 'operator' => '=', + ], + ], + 10, + 1 + ); + + uopz_unset_return( '_doing_it_wrong' ); + + $this->assertTrue( $doing_it_wrong_called, '_doing_it_wrong should be called' ); + + // Should return all rows since the malformed filter is ignored. + $this->assertCount( 2, $results ); + + // Query should not contain WHERE clause. + $this->assertStringNotContainsString( 'WHERE', $this->get_captured_query() ); + $this->assertCapturedQueryMatchesSnapshot(); + } + + /** + * @test + */ + public function should_handle_sub_where_with_missing_value_key() { + $table = $this->get_query_test_table(); + Register::table( $table ); + + $table::insert( [ 'name' => 'Test 1', 'slug' => 'test-1', 'status' => 1 ] ); + $table::insert( [ 'name' => '', 'slug' => 'test-2', 'status' => 0 ] ); + + $doing_it_wrong_called = false; + + uopz_set_return( '_doing_it_wrong', function () use ( &$doing_it_wrong_called ) { + $doing_it_wrong_called = true; + }, true ); + + // Missing 'value' key - should default to checking column is not empty. + $results = $table::paginate( + [ + [ + 'column' => 'name', + ], + ], + 10, + 1 + ); + + uopz_unset_return( '_doing_it_wrong' ); + + $this->assertFalse( $doing_it_wrong_called, '_doing_it_wrong should not be called' ); + + // Should return only the row with non-empty name. + $this->assertCount( 1, $results ); + $this->assertEquals( 'Test 1', $results[0]['name'] ); + + // Query should contain WHERE clause with != ''. + $this->assertQueryContains( "a.name != ''" ); + $this->assertCapturedQueryMatchesSnapshot(); + } + + /** + * @test + */ + public function should_normalize_invalid_operator_to_equals() { + $table = $this->get_query_test_table(); + Register::table( $table ); + + $table::insert( [ 'name' => 'Test 1', 'slug' => 'test-1', 'status' => 1 ] ); + $table::insert( [ 'name' => 'Test 2', 'slug' => 'test-2', 'status' => 0 ] ); + + // Invalid operator should be normalized to '='. + $results = $table::paginate( + [ + [ + 'column' => 'slug', + 'value' => 'test-1', + 'operator' => 'INVALID_OP', + ], + ], + 10, + 1 + ); + + $this->assertCount( 1, $results ); + $this->assertEquals( 'test-1', $results[0]['slug'] ); + + // Should use '=' operator. + $this->assertQueryContains( "a.slug = 'test-1'" ); + $this->assertCapturedQueryMatchesSnapshot(); + } + + /** + * @test + */ + public function should_ignore_sub_where_with_invalid_column_name() { + $table = $this->get_query_test_table(); + Register::table( $table ); + + $table::insert( [ 'name' => 'Test 1', 'slug' => 'test-1', 'status' => 1 ] ); + $table::insert( [ 'name' => 'Test 2', 'slug' => 'test-2', 'status' => 0 ] ); + + // Column 'nonexistent' does not exist. + $results = $table::paginate( + [ + [ + 'column' => 'nonexistent_column', + 'value' => 'test-1', + 'operator' => '=', + ], + ], + 10, + 1 + ); + + // Should return all rows since invalid column filter is ignored. + $this->assertCount( 2, $results ); + + // Query should not contain WHERE clause. + $this->assertStringNotContainsString( 'WHERE', $this->get_captured_query() ); + $this->assertCapturedQueryMatchesSnapshot(); + } + + /** + * @test + */ + public function should_ignore_nested_sub_where_with_missing_column() { + $table = $this->get_query_test_table(); + Register::table( $table ); + + $table::insert( [ 'name' => 'Alpha', 'slug' => 'alpha', 'status' => 1 ] ); + $table::insert( [ 'name' => 'Beta', 'slug' => 'beta', 'status' => 0 ] ); + + // Nested sub-where with one valid and one invalid entry. + $results = $table::paginate( + [ + [ + 'query_operator' => 'OR', + [ + 'column' => 'slug', + 'value' => 'alpha', + 'operator' => '=', + ], + [ + // Missing 'column' key - should be ignored. + 'value' => 'beta', + 'operator' => '=', + ], + ], + ], + 10, + 1 + ); + + // Should return only alpha since the invalid sub-where is ignored. + $this->assertCount( 1, $results ); + $this->assertEquals( 'alpha', $results[0]['slug'] ); + $this->assertCapturedQueryMatchesSnapshot(); + } + + /** + * @test + */ + public function should_normalize_invalid_query_operator_in_sub_where() { + $table = $this->get_query_test_table(); + Register::table( $table ); + + $table::insert( [ 'name' => 'Test 1', 'slug' => 'test-1', 'status' => 1 ] ); + $table::insert( [ 'name' => 'Test 2', 'slug' => 'test-2', 'status' => 1 ] ); + $table::insert( [ 'name' => 'Test 3', 'slug' => 'test-3', 'status' => 0 ] ); + + // Invalid query_operator should be normalized to 'AND'. + $results = $table::paginate( + [ + [ + 'query_operator' => 'INVALID', + [ + 'column' => 'slug', + 'value' => 'test-1', + 'operator' => '=', + ], + [ + 'column' => 'status', + 'value' => 1, + 'operator' => '=', + ], + ], + ], + 10, + 1 + ); + + // With AND operator, only 'test-1' matches both conditions. + $this->assertCount( 1, $results ); + + // Should use 'AND' operator (the default). + $this->assertQueryContains( "AND" ); + $this->assertCapturedQueryMatchesSnapshot(); + } + + /** + * @test + */ + public function should_handle_deeply_nested_sub_wheres_four_levels() { + $table = $this->get_query_test_table(); + Register::table( $table ); + + // Insert test data covering various combinations. + $table::insert( [ 'name' => 'A1', 'slug' => 'a1', 'status' => 1 ] ); + $table::insert( [ 'name' => 'A2', 'slug' => 'a2', 'status' => 1 ] ); + $table::insert( [ 'name' => 'B1', 'slug' => 'b1', 'status' => 0 ] ); + $table::insert( [ 'name' => 'B2', 'slug' => 'b2', 'status' => 0 ] ); + $table::insert( [ 'name' => 'C1', 'slug' => 'c1', 'status' => 1 ] ); + $table::insert( [ 'name' => 'C2', 'slug' => 'c2', 'status' => 0 ] ); + + // Level 4 deep nesting: + // WHERE ( + // ( + // (slug = 'a1' OR slug = 'a2') + // AND + // (status = 1) + // ) + // OR + // (slug = 'b1') + // ) + $results = $table::paginate( + [ + [ + 'query_operator' => 'OR', + // Level 2 + [ + 'query_operator' => 'AND', + // Level 3 + [ + 'query_operator' => 'OR', + // Level 4 + [ + 'column' => 'slug', + 'value' => 'a1', + 'operator' => '=', + ], + [ + 'column' => 'slug', + 'value' => 'a2', + 'operator' => '=', + ], + ], + [ + 'column' => 'status', + 'value' => 1, + 'operator' => '=', + ], + ], + // OR with this + [ + 'column' => 'slug', + 'value' => 'b1', + 'operator' => '=', + ], + ], + ], + 10, + 1 + ); + + // Should match: a1 (status=1), a2 (status=1), b1 (any status). + $this->assertCount( 3, $results ); + $slugs = array_column( $results, 'slug' ); + $this->assertContains( 'a1', $slugs ); + $this->assertContains( 'a2', $slugs ); + $this->assertContains( 'b1', $slugs ); + + // Verify the query structure has proper nesting. + $query = $this->get_captured_query(); + $this->assertStringContainsString( 'WHERE', $query ); + // Should contain nested parentheses. + $this->assertStringContainsString( '((', $query ); + $this->assertCapturedQueryMatchesSnapshot(); + } + + /** + * @test + */ + public function should_handle_complex_four_level_nesting_with_all_operators() { + $table = $this->get_query_test_table(); + Register::table( $table ); + + $table::insert( [ 'name' => 'Item 1', 'slug' => 'item-1', 'status' => 1 ] ); + $table::insert( [ 'name' => 'Item 2', 'slug' => 'item-2', 'status' => 2 ] ); + $table::insert( [ 'name' => 'Item 3', 'slug' => 'item-3', 'status' => 3 ] ); + $table::insert( [ 'name' => 'Item 4', 'slug' => 'item-4', 'status' => 4 ] ); + $table::insert( [ 'name' => 'Item 5', 'slug' => 'item-5', 'status' => 5 ] ); + + // Complex query: + // WHERE ( + // ( + // (status >= 2 AND status <= 4) + // OR + // (slug = 'item-1') + // ) + // AND + // (status != 3) + // ) + $results = $table::paginate( + [ + [ + 'query_operator' => 'AND', + // Level 2 + [ + 'query_operator' => 'OR', + // Level 3 + [ + 'query_operator' => 'AND', + // Level 4 + [ + 'column' => 'status', + 'value' => 2, + 'operator' => '>=', + ], + [ + 'column' => 'status', + 'value' => 4, + 'operator' => '<=', + ], + ], + [ + 'column' => 'slug', + 'value' => 'item-1', + 'operator' => '=', + ], + ], + [ + 'column' => 'status', + 'value' => 3, + 'operator' => '!=', + ], + ], + ], + 10, + 1 + ); + + // Should match: item-1 (status=1, not 3), item-2 (status=2, in range, not 3), item-4 (status=4, in range, not 3). + $this->assertCount( 3, $results ); + $slugs = array_column( $results, 'slug' ); + $this->assertContains( 'item-1', $slugs ); + $this->assertContains( 'item-2', $slugs ); + $this->assertContains( 'item-4', $slugs ); + $this->assertNotContains( 'item-3', $slugs ); // Excluded by != 3. + $this->assertNotContains( 'item-5', $slugs ); // Not in range and not item-1. + $this->assertCapturedQueryMatchesSnapshot(); + } + + /** + * @test + */ + public function should_capture_query_with_paginate_filter() { + $table = $this->get_query_test_table(); + Register::table( $table ); + + $table::insert( [ 'name' => 'Test', 'slug' => 'test', 'status' => 1 ] ); + + $table::paginate( + [ + [ + 'column' => 'status', + 'value' => 1, + 'operator' => '=', + ], + ], + 10, + 1 + ); + + $query = $this->get_captured_query(); + + // Verify query structure. + $this->assertNotEmpty( $query ); + $this->assertStringContainsString( 'SELECT', $query ); + $this->assertStringContainsString( 'FROM', $query ); + $this->assertStringContainsString( 'WHERE', $query ); + $this->assertStringContainsString( 'a.status = 1', $query ); + $this->assertStringContainsString( 'LIMIT', $query ); + $this->assertCapturedQueryMatchesSnapshot(); + } + + /** + * @test + */ + public function should_produce_correct_query_for_simple_sub_where() { + $table = $this->get_query_test_table(); + Register::table( $table ); + + $table::insert( [ 'name' => 'Test', 'slug' => 'test', 'status' => 1 ] ); + + $table::paginate( + [ + 'orderby' => 'id', + 'order' => 'ASC', + [ + 'column' => 'slug', + 'value' => 'test', + 'operator' => '=', + ], + ], + 20, + 1 + ); + + $this->assertQueryContains( "WHERE a.slug = 'test'" ); + $this->assertQueryContains( "ORDER BY a.id ASC" ); + $this->assertQueryContains( "LIMIT 0, 20" ); + $this->assertCapturedQueryMatchesSnapshot(); + } + + /** + * @test + */ + public function should_produce_correct_query_for_or_sub_where() { + $table = $this->get_query_test_table(); + Register::table( $table ); + + $table::insert( [ 'name' => 'A', 'slug' => 'a', 'status' => 1 ] ); + $table::insert( [ 'name' => 'B', 'slug' => 'b', 'status' => 0 ] ); + + $table::paginate( + [ + [ + 'query_operator' => 'OR', + [ + 'column' => 'slug', + 'value' => 'a', + 'operator' => '=', + ], + [ + 'column' => 'slug', + 'value' => 'b', + 'operator' => '=', + ], + ], + ], + 10, + 1 + ); + + // Verify the query has OR grouped in parentheses. + $this->assertQueryContains( "(a.slug = 'a' OR a.slug = 'b')" ); + $this->assertCapturedQueryMatchesSnapshot(); + } + + /** + * @test + */ + public function should_produce_correct_query_for_nested_sub_where() { + $table = $this->get_query_test_table(); + Register::table( $table ); + + $table::insert( [ 'name' => 'Test', 'slug' => 'test', 'status' => 1 ] ); + + // WHERE ((slug = 'a' OR slug = 'b') AND status = 1) + $table::paginate( + [ + [ + 'query_operator' => 'OR', + [ + 'column' => 'slug', + 'value' => 'a', + 'operator' => '=', + ], + [ + 'column' => 'slug', + 'value' => 'b', + 'operator' => '=', + ], + ], + [ + 'column' => 'status', + 'value' => 1, + 'operator' => '=', + ], + ], + 10, + 1 + ); + + // Verify the query structure. + $query = $this->get_captured_query(); + $this->assertStringContainsString( "(a.slug = 'a' OR a.slug = 'b')", $query ); + $this->assertStringContainsString( "a.status = 1", $query ); + $this->assertStringContainsString( " AND ", $query ); + $this->assertCapturedQueryMatchesSnapshot(); + } + + /** + * @test + */ + public function should_handle_empty_sub_where_group() { + $table = $this->get_query_test_table(); + Register::table( $table ); + + $table::insert( [ 'name' => 'Test 1', 'slug' => 'test-1', 'status' => 1 ] ); + $table::insert( [ 'name' => 'Test 2', 'slug' => 'test-2', 'status' => 0 ] ); + + // Empty sub-where group should be ignored. + $results = $table::paginate( + [ + [ + 'query_operator' => 'OR', + // Empty - no conditions. + ], + [ + 'column' => 'status', + 'value' => 1, + 'operator' => '=', + ], + ], + 10, + 1 + ); - $this->assertTrue( $pre_results_fired, 'stellarwp_schema_custom_table_query_pre_results action should fire' ); - $this->assertTrue( $post_results_fired, 'stellarwp_schema_custom_table_query_post_results action should fire' ); - $this->assertTrue( $results_filter_fired, 'stellarwp_schema_custom_table_query_results filter should fire' ); - $this->assertTrue( $where_filter_fired, 'stellarwp_schema_custom_table_query_where filter should fire' ); + $this->assertCount( 1, $results ); + $this->assertEquals( 'test-1', $results[0]['slug'] ); + $this->assertCapturedQueryMatchesSnapshot(); } /** diff --git a/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_capture_query_with_paginate_filter__0.snapshot.php b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_capture_query_with_paginate_filter__0.snapshot.php new file mode 100644 index 0000000..b271c62 --- /dev/null +++ b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_capture_query_with_paginate_filter__0.snapshot.php @@ -0,0 +1 @@ +SELECT a.* FROM `wp_query_test` a WHERE a.status = 1 ORDER BY a.id ASC LIMIT 0, 10 \ No newline at end of file diff --git a/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_count_total_items_with_sub_where_clauses__0.snapshot.php b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_count_total_items_with_sub_where_clauses__0.snapshot.php new file mode 100644 index 0000000..4c4d4c7 --- /dev/null +++ b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_count_total_items_with_sub_where_clauses__0.snapshot.php @@ -0,0 +1 @@ +SELECT COUNT(*) FROM `wp_query_test` a WHERE (a.slug = 'x' OR a.slug = 'y') \ No newline at end of file diff --git a/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_handle_complex_four_level_nesting_with_all_operators__0.snapshot.php b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_handle_complex_four_level_nesting_with_all_operators__0.snapshot.php new file mode 100644 index 0000000..4ac222e --- /dev/null +++ b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_handle_complex_four_level_nesting_with_all_operators__0.snapshot.php @@ -0,0 +1 @@ +SELECT a.* FROM `wp_query_test` a WHERE (((a.status >= 2 AND a.status <= 4) OR a.slug = 'item-1') AND a.status != 3) ORDER BY a.id ASC LIMIT 0, 10 \ No newline at end of file diff --git a/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_handle_deeply_nested_sub_wheres_four_levels__0.snapshot.php b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_handle_deeply_nested_sub_wheres_four_levels__0.snapshot.php new file mode 100644 index 0000000..5cfbb1d --- /dev/null +++ b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_handle_deeply_nested_sub_wheres_four_levels__0.snapshot.php @@ -0,0 +1 @@ +SELECT a.* FROM `wp_query_test` a WHERE (((a.slug = 'a1' OR a.slug = 'a2') AND a.status = 1) OR a.slug = 'b1') ORDER BY a.id ASC LIMIT 0, 10 \ No newline at end of file diff --git a/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_handle_empty_sub_where_group__0.snapshot.php b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_handle_empty_sub_where_group__0.snapshot.php new file mode 100644 index 0000000..b271c62 --- /dev/null +++ b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_handle_empty_sub_where_group__0.snapshot.php @@ -0,0 +1 @@ +SELECT a.* FROM `wp_query_test` a WHERE a.status = 1 ORDER BY a.id ASC LIMIT 0, 10 \ No newline at end of file diff --git a/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_handle_sub_where_with_different_operators__0.snapshot.php b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_handle_sub_where_with_different_operators__0.snapshot.php new file mode 100644 index 0000000..7f21f82 --- /dev/null +++ b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_handle_sub_where_with_different_operators__0.snapshot.php @@ -0,0 +1 @@ +SELECT a.* FROM `wp_query_test` a WHERE (a.status > 1 AND a.status < 4) ORDER BY a.id ASC LIMIT 0, 10 \ No newline at end of file diff --git a/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_handle_sub_where_with_missing_value_key__0.snapshot.php b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_handle_sub_where_with_missing_value_key__0.snapshot.php new file mode 100644 index 0000000..a0225f5 --- /dev/null +++ b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_handle_sub_where_with_missing_value_key__0.snapshot.php @@ -0,0 +1 @@ +SELECT a.* FROM `wp_query_test` a WHERE a.name != '' ORDER BY a.id ASC LIMIT 0, 10 \ No newline at end of file diff --git a/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_ignore_nested_sub_where_with_missing_column__0.snapshot.php b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_ignore_nested_sub_where_with_missing_column__0.snapshot.php new file mode 100644 index 0000000..7436784 --- /dev/null +++ b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_ignore_nested_sub_where_with_missing_column__0.snapshot.php @@ -0,0 +1 @@ +SELECT a.* FROM `wp_query_test` a WHERE (a.slug = 'alpha') ORDER BY a.id ASC LIMIT 0, 10 \ No newline at end of file diff --git a/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_ignore_sub_where_with_invalid_column_name__0.snapshot.php b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_ignore_sub_where_with_invalid_column_name__0.snapshot.php new file mode 100644 index 0000000..08b78be --- /dev/null +++ b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_ignore_sub_where_with_invalid_column_name__0.snapshot.php @@ -0,0 +1 @@ +SELECT a.* FROM `wp_query_test` a ORDER BY a.id ASC LIMIT 0, 10 \ No newline at end of file diff --git a/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_ignore_sub_where_with_missing_column_key__0.snapshot.php b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_ignore_sub_where_with_missing_column_key__0.snapshot.php new file mode 100644 index 0000000..08b78be --- /dev/null +++ b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_ignore_sub_where_with_missing_column_key__0.snapshot.php @@ -0,0 +1 @@ +SELECT a.* FROM `wp_query_test` a ORDER BY a.id ASC LIMIT 0, 10 \ No newline at end of file diff --git a/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_normalize_invalid_operator_to_equals__0.snapshot.php b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_normalize_invalid_operator_to_equals__0.snapshot.php new file mode 100644 index 0000000..0d3874d --- /dev/null +++ b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_normalize_invalid_operator_to_equals__0.snapshot.php @@ -0,0 +1 @@ +SELECT a.* FROM `wp_query_test` a WHERE a.slug = 'test-1' ORDER BY a.id ASC LIMIT 0, 10 \ No newline at end of file diff --git a/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_normalize_invalid_query_operator_in_sub_where__0.snapshot.php b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_normalize_invalid_query_operator_in_sub_where__0.snapshot.php new file mode 100644 index 0000000..7c33a65 --- /dev/null +++ b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_normalize_invalid_query_operator_in_sub_where__0.snapshot.php @@ -0,0 +1 @@ +SELECT a.* FROM `wp_query_test` a WHERE (a.slug = 'test-1' AND a.status = 1) ORDER BY a.id ASC LIMIT 0, 10 \ No newline at end of file diff --git a/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_paginate_with_multiple_sub_where_groups__0.snapshot.php b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_paginate_with_multiple_sub_where_groups__0.snapshot.php new file mode 100644 index 0000000..1a4fe36 --- /dev/null +++ b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_paginate_with_multiple_sub_where_groups__0.snapshot.php @@ -0,0 +1 @@ +SELECT a.* FROM `wp_query_test` a WHERE a.slug = 'a' OR a.slug = 'b' ORDER BY a.id ASC LIMIT 0, 10 \ No newline at end of file diff --git a/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_paginate_with_nested_sub_where_clauses__0.snapshot.php b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_paginate_with_nested_sub_where_clauses__0.snapshot.php new file mode 100644 index 0000000..74e579f --- /dev/null +++ b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_paginate_with_nested_sub_where_clauses__0.snapshot.php @@ -0,0 +1 @@ +SELECT a.* FROM `wp_query_test` a WHERE (a.slug = 'alpha' OR a.slug = 'beta') AND a.status = 1 ORDER BY a.id ASC LIMIT 0, 10 \ No newline at end of file diff --git a/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_paginate_with_simple_where_clause__0.snapshot.php b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_paginate_with_simple_where_clause__0.snapshot.php new file mode 100644 index 0000000..b271c62 --- /dev/null +++ b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_paginate_with_simple_where_clause__0.snapshot.php @@ -0,0 +1 @@ +SELECT a.* FROM `wp_query_test` a WHERE a.status = 1 ORDER BY a.id ASC LIMIT 0, 10 \ No newline at end of file diff --git a/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_paginate_with_sub_where_clauses_using_or__0.snapshot.php b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_paginate_with_sub_where_clauses_using_or__0.snapshot.php new file mode 100644 index 0000000..260b13d --- /dev/null +++ b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_paginate_with_sub_where_clauses_using_or__0.snapshot.php @@ -0,0 +1 @@ +SELECT a.* FROM `wp_query_test` a WHERE (a.slug = 'alpha' OR a.slug = 'beta') ORDER BY a.id ASC LIMIT 0, 10 \ No newline at end of file diff --git a/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_produce_correct_query_for_nested_sub_where__0.snapshot.php b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_produce_correct_query_for_nested_sub_where__0.snapshot.php new file mode 100644 index 0000000..bb756d0 --- /dev/null +++ b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_produce_correct_query_for_nested_sub_where__0.snapshot.php @@ -0,0 +1 @@ +SELECT a.* FROM `wp_query_test` a WHERE (a.slug = 'a' OR a.slug = 'b') AND a.status = 1 ORDER BY a.id ASC LIMIT 0, 10 \ No newline at end of file diff --git a/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_produce_correct_query_for_or_sub_where__0.snapshot.php b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_produce_correct_query_for_or_sub_where__0.snapshot.php new file mode 100644 index 0000000..987ca20 --- /dev/null +++ b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_produce_correct_query_for_or_sub_where__0.snapshot.php @@ -0,0 +1 @@ +SELECT a.* FROM `wp_query_test` a WHERE (a.slug = 'a' OR a.slug = 'b') ORDER BY a.id ASC LIMIT 0, 10 \ No newline at end of file diff --git a/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_produce_correct_query_for_simple_sub_where__0.snapshot.php b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_produce_correct_query_for_simple_sub_where__0.snapshot.php new file mode 100644 index 0000000..84db9d6 --- /dev/null +++ b/tests/wpunit/Traits/__snapshots__/Custom_Table_Query_MethodsTest__should_produce_correct_query_for_simple_sub_where__0.snapshot.php @@ -0,0 +1 @@ +SELECT a.* FROM `wp_query_test` a WHERE a.slug = 'test' ORDER BY a.id ASC LIMIT 0, 20 \ No newline at end of file From a4d836b6f84077f86bd5107019df6f7368c60c38 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Mon, 5 Jan 2026 14:49:53 +0200 Subject: [PATCH 11/12] test: use actions and filters instead of uopz --- .../Traits/Custom_Table_Query_MethodsTest.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/wpunit/Traits/Custom_Table_Query_MethodsTest.php b/tests/wpunit/Traits/Custom_Table_Query_MethodsTest.php index 55edd43..bbbb003 100644 --- a/tests/wpunit/Traits/Custom_Table_Query_MethodsTest.php +++ b/tests/wpunit/Traits/Custom_Table_Query_MethodsTest.php @@ -493,10 +493,12 @@ public function should_ignore_sub_where_with_missing_column_key() { $table::insert( [ 'name' => 'Test 1', 'slug' => 'test-1', 'status' => 1 ] ); $table::insert( [ 'name' => 'Test 2', 'slug' => 'test-2', 'status' => 0 ] ); + add_filter( 'doing_it_wrong_trigger_error', '__return_false' ); + $doing_it_wrong_called = false; - uopz_set_return( '_doing_it_wrong', function() use ( &$doing_it_wrong_called ) { + add_action( 'doing_it_wrong_run', function() use ( &$doing_it_wrong_called ) { $doing_it_wrong_called = true; - }, true ); + } ); // Malformed: missing 'column' key. $results = $table::paginate( @@ -510,8 +512,6 @@ public function should_ignore_sub_where_with_missing_column_key() { 1 ); - uopz_unset_return( '_doing_it_wrong' ); - $this->assertTrue( $doing_it_wrong_called, '_doing_it_wrong should be called' ); // Should return all rows since the malformed filter is ignored. @@ -534,9 +534,11 @@ public function should_handle_sub_where_with_missing_value_key() { $doing_it_wrong_called = false; - uopz_set_return( '_doing_it_wrong', function () use ( &$doing_it_wrong_called ) { + add_filter( 'doing_it_wrong_trigger_error', '__return_false' ); + + add_action( 'doing_it_wrong_run', function () use ( &$doing_it_wrong_called ) { $doing_it_wrong_called = true; - }, true ); + } ); // Missing 'value' key - should default to checking column is not empty. $results = $table::paginate( @@ -549,8 +551,6 @@ public function should_handle_sub_where_with_missing_value_key() { 1 ); - uopz_unset_return( '_doing_it_wrong' ); - $this->assertFalse( $doing_it_wrong_called, '_doing_it_wrong should not be called' ); // Should return only the row with non-empty name. From 73893a4ebd977e147138d5c4a86960a8ed0231c0 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Mon, 5 Jan 2026 14:52:33 +0200 Subject: [PATCH 12/12] docs: updated release date --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10ba6a4..993a48f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. This project adhere to the [Semantic Versioning](http://semver.org/) standard. -## [3.2.0] Unreleased +## [3.2.0] 2026-01-05 * Feature - Add support for nested sub-WHERE clauses in `paginate()` and `get_total_items()` methods. This allows building complex queries like `WHERE (col1 = 'a' OR col2 = 'b') AND col3 = 'c'`. * Feature - Add new `stellarwp_schema_custom_table_paginate_query` filter to allow modification of the paginate query before execution.