From 46c154ac108bcc2f513400503cebb6b315913092 Mon Sep 17 00:00:00 2001 From: breken-ai <312387581+breken-ai@users.noreply.github.com> Date: Fri, 25 Sep 2026 21:24:56 -0700 Subject: [PATCH] fix: skip aggregate pushdown when a WHERE clause stays local A WHERE clause the framework can't turn into a Qual (for example lower(name) = 'x' or a > b) is normally evaluated locally on each row. The aggregate upper path has no local filter, so the remote aggregate ran without that clause and returned a wrong result, e.g. SELECT count(*) FROM ft WHERE lower(name) = 'carol' counted every row. Only offer the aggregate path when every restriction clause on the foreign table was extracted as a qual. Co-Authored-By: Claude Opus 5.5 (1M context) --- supabase-wrappers/src/upper.rs | 16 +++++++++ wrappers/src/fdw/mysql_fdw/tests.rs | 50 +++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/supabase-wrappers/src/upper.rs b/supabase-wrappers/src/upper.rs index 9467d66c..be48250d 100644 --- a/supabase-wrappers/src/upper.rs +++ b/supabase-wrappers/src/upper.rs @@ -300,6 +300,22 @@ pub(super) extern "C-unwind" fn get_foreign_upper_paths< let mut state = PgBox::>::from_pg(fdw_private as _); + // Every WHERE clause on the foreign table must have been extracted as a + // qual for the FDW to push down. A clause we couldn't extract (e.g. + // `lower(name) = 'x'` or `a > b`) is normally checked locally on each + // row, but the upper path has no local filter, so the remote aggregate + // would silently run without it. + let restrictinfo = (*input_rel).baserestrictinfo; + let n_clauses = if restrictinfo.is_null() { + 0 + } else { + (*restrictinfo).length as usize + }; + if state.quals.len() != n_clauses { + debug2!("WHERE clause cannot be pushed down, skipping aggregate pushdown"); + return; + } + // Check if FDW supports any aggregates let supported = W::supported_aggregates(); if supported.is_empty() { diff --git a/wrappers/src/fdw/mysql_fdw/tests.rs b/wrappers/src/fdw/mysql_fdw/tests.rs index 09c65bb1..65b22a58 100644 --- a/wrappers/src/fdw/mysql_fdw/tests.rs +++ b/wrappers/src/fdw/mysql_fdw/tests.rs @@ -683,6 +683,56 @@ mod tests { "SELECT SUM(amount) FROM mysql_agg.orders WHERE id IN (1, 2, 3)" ); + // --- Negative: WHERE clause that can't be extracted as a qual --- + // `lower(name) = ...` stays a local filter, so the aggregate must not + // be pushed down, otherwise MySQL aggregates over rows the filter + // would have removed. + let cnt: i64 = c + .select( + "SELECT COUNT(*) FROM mysql_agg.orders WHERE lower(name) = 'carol'", + None, + &[], + ) + .unwrap() + .first() + .get_one::() + .unwrap() + .unwrap(); + assert_eq!( + cnt, 1, + "COUNT(*) WHERE lower(name) = 'carol' expected 1, got {cnt}" + ); + assert_not_pushed_down!( + c, + "SELECT COUNT(*) FROM mysql_agg.orders WHERE lower(name) = 'carol'" + ); + + // mixed: `status` is pushable, `lower(name)` is not → Eve only = 150 + let s: f64 = c + .select( + "SELECT SUM(amount) FROM mysql_agg.orders + WHERE status = 'inactive' AND lower(name) = 'eve'", + None, + &[], + ) + .unwrap() + .first() + .get_one::() + .unwrap() + .unwrap() + .to_string() + .parse() + .unwrap(); + assert!( + (s - 150.0).abs() < 0.01, + "SUM WHERE status = 'inactive' AND lower(name) = 'eve' expected 150.0, got {s}" + ); + assert_not_pushed_down!( + c, + "SELECT SUM(amount) FROM mysql_agg.orders + WHERE status = 'inactive' AND lower(name) = 'eve'" + ); + // --- Subquery table: verify aggregate pushdown through starts_with('(') branch --- // COUNT(*) over the subquery — same 5 rows let cnt: i64 = c