Skip to content

Commit 8df3bb3

Browse files
fix: honor no-op calendar query filters (#753)
Co-authored-by: homeboy-ci[bot] <266378653+homeboy-ci[bot]@users.noreply.github.com>
1 parent 82b9abb commit 8df3bb3

3 files changed

Lines changed: 141 additions & 11 deletions

File tree

inc/Abilities/EventDateQueryAbilities.php

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ class EventDateQueryAbilities {
3434
private const DEFAULT_PUBLIC_RESULTS = 50;
3535
private const TAXONOMY_CANDIDATE_BATCH = 100;
3636

37-
private static bool $registered = false;
37+
private static bool $registered = false;
38+
private ?array $prefilteredQueryArgs = null;
3839

3940
public function __construct() {
4041
if ( ! self::$registered ) {
@@ -458,7 +459,10 @@ public function executeQueryEvents( array $input ): array {
458459
* @param array $input The full ability input array.
459460
*/
460461
$base_query_args = $query_args;
461-
$query_args = (array) apply_filters( 'data_machine_events_calendar_query_args', $query_args, $input );
462+
$prefiltered = $this->consumePrefilteredQueryArgs();
463+
$query_args = is_array( $prefiltered )
464+
? $prefiltered
465+
: (array) apply_filters( 'data_machine_events_calendar_query_args', $query_args, $input );
462466

463467
if ( $query_args === $base_query_args && $this->canUseBoundedTaxonomyCandidates( $input, $tax_filters, $scope, $status, $per_page, $page ) ) {
464468
remove_filter( 'posts_clauses', $clauses_filter );
@@ -709,19 +713,21 @@ public function buildMatchingPostIdsSql( array $input ): string {
709713
* @return string SQL selecting start_date, end_date, and bucket_count.
710714
*/
711715
public function buildMatchingEventDateAggregateSql( array $input, string $start_expression, string $end_expression ): string {
712-
$selective = $this->buildSelectiveTaxonomyAggregateSql( $input, $start_expression, $end_expression );
713-
if ( '' !== $selective ) {
714-
return $selective;
715-
}
716-
717-
$request = '';
718716
$input['fields'] = 'ids';
719717
$input['per_page'] = -1;
720718
$input[ self::CAPTURE_AGGREGATE_QUERY_VAR ] = array(
721719
'start_expression' => $start_expression,
722720
'end_expression' => $end_expression,
723721
);
724722

723+
$this->prefilteredQueryArgs = null;
724+
$selective = $this->buildSelectiveTaxonomyAggregateSql( $input, $start_expression, $end_expression );
725+
if ( '' !== $selective ) {
726+
return $selective;
727+
}
728+
729+
$request = '';
730+
725731
$capture = static function ( $posts, $query ) use ( &$request ) {
726732
if ( ! $query->get( self::CAPTURE_AGGREGATE_QUERY_VAR ) ) {
727733
return $posts;
@@ -737,6 +743,7 @@ public function buildMatchingEventDateAggregateSql( array $input, string $start_
737743
$this->executeQueryEvents( $input );
738744
} finally {
739745
remove_filter( 'posts_pre_query', $capture, PHP_INT_MAX );
746+
$this->prefilteredQueryArgs = null;
740747
}
741748

742749
return $request;
@@ -755,20 +762,20 @@ private function buildSelectiveTaxonomyAggregateSql( array $input, string $start
755762

756763
$db_engine = defined( 'DB_ENGINE' ) ? strtolower( (string) constant( 'DB_ENGINE' ) ) : '';
757764
$database_type = defined( 'DATABASE_TYPE' ) ? strtolower( (string) constant( 'DATABASE_TYPE' ) ) : '';
758-
$tax_filters = is_array( $input['tax_filters'] ?? null ) ? array_filter( $input['tax_filters'] ) : array();
765+
$tax_filters = is_array( $input['tax_filters'] ?? null ) ? $input['tax_filters'] : array();
759766
if (
760767
'sqlite' === $db_engine
761768
|| 'sqlite' === $database_type
762769
|| true !== $wpdb->is_mysql
763770
|| 1 !== count( $tax_filters )
771+
|| empty( reset( $tax_filters ) )
764772
|| 'publish' !== ( $input['status'] ?? 'publish' )
765773
|| ! empty( $input['search'] )
766774
|| ! empty( $input['geo'] )
767775
|| ! empty( $input['exclude'] )
768776
|| ! empty( $input['meta_query'] )
769777
|| ! empty( $input['scope_token'] )
770778
|| ! empty( $input['time_scope'] )
771-
|| false !== has_filter( 'data_machine_events_calendar_query_args' )
772779
) {
773780
return '';
774781
}
@@ -814,6 +821,9 @@ private function buildSelectiveTaxonomyAggregateSql( array $input, string $start
814821
} elseif ( 'past' === $scope ) {
815822
$where[] = UpcomingFilter::past_where( $now );
816823
}
824+
if ( $this->hasCustomizedAggregateQueryArgs( $input, $tax_filters ) ) {
825+
return '';
826+
}
817827

818828
$placeholders = implode( ',', array_fill( 0, count( $taxonomy_ids ), '%d' ) );
819829
$table = EventDatesTable::table_name();
@@ -832,6 +842,49 @@ private function buildSelectiveTaxonomyAggregateSql( array $input, string $start
832842
return $wpdb->prepare( $sql, ...$values ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Trusted table/expression fragments; all values use placeholders.
833843
}
834844

845+
/** Determine whether registered query callbacks actually constrain this aggregate. */
846+
private function hasCustomizedAggregateQueryArgs( array $input, array $tax_filters ): bool {
847+
if ( false === has_filter( 'data_machine_events_calendar_query_args' ) ) {
848+
return false;
849+
}
850+
851+
$query_args = array(
852+
'post_type' => Event_Post_Type::POST_TYPE,
853+
'post_status' => 'publish',
854+
'posts_per_page' => -1,
855+
'paged' => max( 1, (int) ( $input['page'] ?? 1 ) ),
856+
'no_found_rows' => true,
857+
'orderby' => 'none',
858+
'fields' => 'ids',
859+
'tax_query' => array( 'relation' => 'AND' ),
860+
);
861+
$query_args[ self::CAPTURE_AGGREGATE_QUERY_VAR ] = $input[ self::CAPTURE_AGGREGATE_QUERY_VAR ];
862+
863+
foreach ( $tax_filters as $taxonomy => $term_ids ) {
864+
$query_args['tax_query'][] = array(
865+
'taxonomy' => sanitize_key( $taxonomy ),
866+
'field' => 'term_id',
867+
'terms' => array_values( array_unique( array_filter( array_map( 'absint', (array) $term_ids ) ) ) ),
868+
'operator' => 'IN',
869+
);
870+
}
871+
872+
$filtered = (array) apply_filters( 'data_machine_events_calendar_query_args', $query_args, $input );
873+
if ( $filtered === $query_args ) {
874+
return false;
875+
}
876+
877+
$this->prefilteredQueryArgs = $filtered;
878+
return true;
879+
}
880+
881+
/** Consume the one-shot aggregate fallback handoff before WP_Query can re-enter. */
882+
private function consumePrefilteredQueryArgs(): ?array {
883+
$prefiltered = $this->prefilteredQueryArgs;
884+
$this->prefilteredQueryArgs = null;
885+
return $prefiltered;
886+
}
887+
835888
/**
836889
* Execute an exact count through the canonical WP_Query constraint path.
837890
*

tests/Unit/CalendarAbilitiesTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -815,7 +815,8 @@ public function test_canonical_bucket_sql_deduplicates_taxonomy_joins(): void {
815815

816816
$this->assertSame( 1, $result['total_event_count'] );
817817
$this->assertCount( 1, $queries );
818-
$this->assertMatchesRegularExpression( '/COUNT\(DISTINCT\s+[^)]+\.ID\) AS bucket_count/i', $queries[0] );
818+
$this->assertStringContainsString( 'COUNT(*) AS bucket_count', $queries[0] );
819+
$this->assertStringContainsString( 'EXISTS (', $queries[0] );
819820
$this->assertStringContainsString( 'term_relationships', $queries[0] );
820821
}
821822

tests/Unit/EventDateQueryAbilitiesTest.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,82 @@ public function test_matching_ids_sql_preserves_consumer_constraints_without_que
467467
$this->assertStringNotContainsString( ' ORDER BY ', strtoupper( $sql ) );
468468
}
469469

470+
public function test_taxonomy_aggregate_uses_selective_sql_with_registered_noop_query_filter(): void {
471+
$venue = wp_insert_term( 'No-op aggregate venue ' . uniqid(), 'venue' );
472+
$this->assertNotWPError( $venue );
473+
$invocations = 0;
474+
$filter = static function ( array $query_args ) use ( &$invocations ): array {
475+
++$invocations;
476+
return $query_args;
477+
};
478+
add_filter( 'data_machine_events_calendar_query_args', $filter );
479+
try {
480+
$sql = ( new EventDateQueryAbilities() )->buildMatchingEventDateAggregateSql(
481+
array(
482+
'scope' => 'past',
483+
'tax_filters' => array( 'venue' => array( (int) $venue['term_id'] ) ),
484+
),
485+
'DATE(ed.start_datetime)',
486+
'DATE(ed.end_datetime)'
487+
);
488+
} finally {
489+
remove_filter( 'data_machine_events_calendar_query_args', $filter );
490+
}
491+
492+
$this->assertStringContainsString( 'EXISTS (', $sql );
493+
$this->assertStringNotContainsString( 'LEFT JOIN', $sql );
494+
$this->assertSame( 1, $invocations );
495+
}
496+
497+
public function test_taxonomy_aggregate_falls_back_when_query_filter_changes_args(): void {
498+
$venue = wp_insert_term( 'Customized aggregate venue ' . uniqid(), 'venue' );
499+
$this->assertNotWPError( $venue );
500+
$invocations = 0;
501+
$filter = static function ( array $query_args ) use ( &$invocations ): array {
502+
++$invocations;
503+
if ( 1 === $invocations ) {
504+
$query_args['post__in'] = array( 123, 456 );
505+
}
506+
return $query_args;
507+
};
508+
add_filter( 'data_machine_events_calendar_query_args', $filter );
509+
try {
510+
$sql = ( new EventDateQueryAbilities() )->buildMatchingEventDateAggregateSql(
511+
array(
512+
'scope' => 'past',
513+
'tax_filters' => array( 'venue' => array( (int) $venue['term_id'] ) ),
514+
),
515+
'DATE(ed.start_datetime)',
516+
'DATE(ed.end_datetime)'
517+
);
518+
} finally {
519+
remove_filter( 'data_machine_events_calendar_query_args', $filter );
520+
}
521+
522+
$this->assertStringContainsString( '123,456', str_replace( ' ', '', $sql ) );
523+
$this->assertStringContainsString( $GLOBALS['wpdb']->term_relationships, $sql );
524+
$this->assertSame( 1, $invocations );
525+
}
526+
527+
public function test_taxonomy_aggregate_rejects_mixed_active_and_empty_filters(): void {
528+
$venue = wp_insert_term( 'Mixed aggregate venue ' . uniqid(), 'venue' );
529+
$this->assertNotWPError( $venue );
530+
$sql = ( new EventDateQueryAbilities() )->buildMatchingEventDateAggregateSql(
531+
array(
532+
'scope' => 'past',
533+
'tax_filters' => array(
534+
'venue' => array( (int) $venue['term_id'] ),
535+
'artist' => array(),
536+
),
537+
),
538+
'DATE(ed.start_datetime)',
539+
'DATE(ed.end_datetime)'
540+
);
541+
542+
$this->assertStringNotContainsString( 'EXISTS (', $sql );
543+
$this->assertStringContainsString( '0 = 1', $sql );
544+
}
545+
470546
public function test_matching_ids_sql_handles_large_consumer_sets_without_materializing_results(): void {
471547
wp_load_alloptions();
472548
$large_set = range( 1, 17000 );

0 commit comments

Comments
 (0)