1use crate::sqlselect::Expr;
85use std::collections::HashMap;
86
87const PURE_FUNCS: &[&str] = &[
93 "lower", "upper", "length", "char_length", "character_length", "coalesce",
94 "nullif", "int2", "int4", "int8", "text", "quote_ident", "format_type",
95 "array_to_string", "current_schema", "current_database", "current_catalog",
96 "current_user", "session_user", "user", "version", "pg_get_userbyid",
97 "pg_table_is_visible", "pg_type_is_visible", "pg_function_is_visible",
98 "pg_encoding_to_char", "pg_get_expr", "pg_get_indexdef",
99 "pg_get_constraintdef",
100];
101
102#[derive(Debug, Clone, Default)]
104pub struct Pushdown {
105 pub per_binding: HashMap<String, Vec<Expr>>,
107 pub refusals: Vec<String>,
109}
110
111impl Pushdown {
112 pub fn for_binding(&self, binding: &str) -> Option<&Vec<Expr>> {
113 self.per_binding.get(&binding.to_ascii_lowercase())
114 }
115
116 pub fn pushed_count(&self) -> usize {
117 self.per_binding.values().map(|v| v.len()).sum()
118 }
119}
120
121fn conjuncts<'a>(e: &'a Expr, out: &mut Vec<&'a Expr>) {
126 match e {
127 Expr::Binary { op, left, right } if op == "AND" => {
128 conjuncts(left, out);
129 conjuncts(right, out);
130 }
131 other => out.push(other),
132 }
133}
134
135enum Reads {
137 One(String),
139 Constant,
142 Refused(&'static str),
143}
144
145fn reads(e: &Expr, known: &[String]) -> Reads {
146 let mut seen: Vec<String> = vec![];
147 let mut why: Option<&'static str> = None;
148 walk(e, known, &mut seen, &mut why);
149 if let Some(w) = why {
150 return Reads::Refused(w);
151 }
152 match seen.len() {
153 0 => Reads::Constant,
154 1 => Reads::One(seen.pop().expect("one")),
155 _ => Reads::Refused("spans more than one relation"),
156 }
157}
158
159fn walk(e: &Expr, known: &[String], seen: &mut Vec<String>, why: &mut Option<&'static str>) {
160 match e {
161 Expr::Column { qual, .. } => match qual {
162 Some(q) => {
163 let lower = q.to_ascii_lowercase();
164 if !known.iter().any(|b| b.eq_ignore_ascii_case(q)) {
165 *why = Some("references an unknown relation");
168 } else if !seen.contains(&lower) {
169 seen.push(lower);
170 }
171 }
172 None => *why = Some("unqualified column cannot be attributed to a relation"),
176 },
177 Expr::Literal(_) => {}
178 Expr::Star | Expr::QualifiedStar(_) => *why = Some("contains `*`"),
179 Expr::Func { name, args } => {
180 if !PURE_FUNCS.iter().any(|f| f.eq_ignore_ascii_case(name)) {
181 *why = Some("calls a function not known to be pure");
182 }
183 for a in args {
184 walk(a, known, seen, why);
185 }
186 }
187 Expr::Agg { .. } => *why = Some("contains an aggregate"),
191 Expr::Case { operand, whens, else_ } => {
192 if let Some(o) = operand {
193 walk(o, known, seen, why);
194 }
195 for (w, t) in whens {
196 walk(w, known, seen, why);
197 walk(t, known, seen, why);
198 }
199 if let Some(x) = else_ {
200 walk(x, known, seen, why);
201 }
202 }
203 Expr::Binary { left, right, .. } => {
204 walk(left, known, seen, why);
205 walk(right, known, seen, why);
206 }
207 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
208 walk(expr, known, seen, why)
209 }
210 Expr::InList { expr, list, .. } => {
211 walk(expr, known, seen, why);
212 for i in list {
213 walk(i, known, seen, why);
214 }
215 }
216 Expr::Index { expr, index } => {
217 walk(expr, known, seen, why);
218 walk(index, known, seen, why);
219 }
220 Expr::ArrayLit(items) => {
221 for i in items {
222 walk(i, known, seen, why);
223 }
224 }
225 Expr::Subquery(_) | Expr::Exists { .. } | Expr::ArrayQuery(_) | Expr::InSubquery { .. } => {
229 *why = Some("contains a subquery")
230 }
231 Expr::Quantified { left, right, .. } => {
232 walk(left, known, seen, why);
233 walk(right, known, seen, why);
234 }
235 }
236}
237
238pub fn nullable_bindings(sel: &crate::sqlselect::Select) -> Vec<String> {
244 use crate::sqlselect::JoinKind;
245 let mut out: Vec<String> = vec![];
246 let mut accumulated: Vec<String> = sel
247 .from
248 .iter()
249 .map(|t| t.binding().to_ascii_lowercase())
250 .collect();
251
252 for j in &sel.joins {
253 let rb = j.table.binding().to_ascii_lowercase();
254 if matches!(j.kind, JoinKind::Left | JoinKind::Full) && !out.contains(&rb) {
257 out.push(rb.clone());
258 }
259 if matches!(j.kind, JoinKind::Right | JoinKind::Full) {
263 for a in &accumulated {
264 if !out.contains(a) {
265 out.push(a.clone());
266 }
267 }
268 }
269 accumulated.push(rb);
270 }
271 out
272}
273
274pub fn plan(
280 where_: Option<&Expr>,
281 bindings: &[String],
282 nullable: &[String],
283) -> Pushdown {
284 let mut out = Pushdown::default();
285 let Some(w) = where_ else { return out };
286
287 if bindings.len() < 2 {
290 return out;
291 }
292
293 let mut parts = vec![];
294 conjuncts(w, &mut parts);
295 for p in parts {
296 match reads(p, bindings) {
297 Reads::One(b) if nullable.iter().any(|n| n.eq_ignore_ascii_case(&b)) => {
298 out.refusals.push(format!(
301 "Filter retained above join: predicate references nullable \
302 side of an outer join ({b})"
303 ));
304 }
305 Reads::One(b) => out.per_binding.entry(b).or_default().push(p.clone()),
306 Reads::Constant => out
307 .refusals
308 .push("Filter retained above join: predicate reads no column".into()),
309 Reads::Refused(why) => out
310 .refusals
311 .push(format!("Filter retained above join: {why}")),
312 }
313 }
314 out
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320 use crate::sqlselect::parse;
321
322 fn plan_for(sql: &str) -> Pushdown {
323 let sel = parse(sql).expect("parses");
324 let mut b = vec![];
325 if let Some(f) = &sel.from {
326 b.push(f.binding());
327 }
328 for j in &sel.joins {
329 b.push(j.table.binding());
330 }
331 let nullable = nullable_bindings(&sel);
332 plan(sel.where_.as_ref(), &b, &nullable)
333 }
334
335 #[test]
336 fn a_single_relation_predicate_is_pushed_to_that_relation() {
337 let p = plan_for("SELECT 1 FROM a JOIN b ON a.x = b.x WHERE a.v > 5");
338 assert_eq!(p.pushed_count(), 1);
339 assert_eq!(p.for_binding("a").map(|v| v.len()), Some(1));
340 assert!(p.for_binding("b").is_none());
341 assert!(p.refusals.is_empty(), "{:?}", p.refusals);
342 }
343
344 #[test]
345 fn conjuncts_are_pushed_to_their_own_relations_independently() {
346 let p = plan_for(
347 "SELECT 1 FROM a JOIN b ON a.x = b.x WHERE a.v > 5 AND b.w < 2 AND a.z = 'q'",
348 );
349 assert_eq!(p.pushed_count(), 3);
350 assert_eq!(p.for_binding("a").map(|v| v.len()), Some(2));
351 assert_eq!(p.for_binding("b").map(|v| v.len()), Some(1));
352 }
353
354 #[test]
355 fn a_predicate_on_the_nullable_side_of_a_left_join_is_REFUSED() {
356 let p = plan_for("SELECT 1 FROM a LEFT JOIN b ON a.x = b.x WHERE b.w = 5");
362 assert_eq!(p.pushed_count(), 0);
363 assert!(p.refusals[0].contains("nullable side"), "{:?}", p.refusals);
364 }
365
366 #[test]
367 fn the_non_nullable_side_of_a_left_join_is_still_pushed() {
368 let p = plan_for("SELECT 1 FROM a LEFT JOIN b ON a.x = b.x WHERE a.v > 5");
372 assert_eq!(p.for_binding("a").map(|v| v.len()), Some(1));
373 assert!(p.refusals.is_empty(), "{:?}", p.refusals);
374 }
375
376 #[test]
377 fn a_right_join_makes_the_LEFT_side_nullable_including_the_from_relation() {
378 let p = plan_for("SELECT 1 FROM a RIGHT JOIN b ON a.x = b.x WHERE a.v > 5");
379 assert_eq!(p.pushed_count(), 0, "a is synthesised by the RIGHT join");
380 assert!(p.refusals[0].contains("nullable side"), "{:?}", p.refusals);
381 let p = plan_for("SELECT 1 FROM a RIGHT JOIN b ON a.x = b.x WHERE b.w > 5");
383 assert_eq!(p.for_binding("b").map(|v| v.len()), Some(1));
384 }
385
386 #[test]
387 fn a_full_join_makes_both_sides_nullable() {
388 for w in ["a.v > 5", "b.w > 5"] {
389 let p = plan_for(&format!("SELECT 1 FROM a FULL JOIN b ON a.x = b.x WHERE {w}"));
390 assert_eq!(p.pushed_count(), 0, "{w}");
391 }
392 }
393
394 #[test]
395 fn a_later_right_join_retroactively_protects_earlier_relations() {
396 let sel = parse(
401 "SELECT 1 FROM a JOIN b ON a.x = b.x RIGHT JOIN c ON b.y = c.y \
402 WHERE a.v > 1 AND b.w > 1 AND c.z > 1",
403 )
404 .expect("parses");
405 let nullable = nullable_bindings(&sel);
406 assert!(nullable.contains(&"a".to_string()), "{nullable:?}");
407 assert!(nullable.contains(&"b".to_string()), "{nullable:?}");
408 assert!(!nullable.contains(&"c".to_string()), "c is never synthesised");
409
410 let p = plan_for(
411 "SELECT 1 FROM a JOIN b ON a.x = b.x RIGHT JOIN c ON b.y = c.y \
412 WHERE a.v > 1 AND b.w > 1 AND c.z > 1",
413 );
414 assert_eq!(p.pushed_count(), 1, "only c");
415 assert_eq!(p.for_binding("c").map(|v| v.len()), Some(1));
416 assert_eq!(p.refusals.len(), 2);
417 }
418
419 #[test]
420 fn an_all_inner_query_can_push_everything() {
421 let p = plan_for(
422 "SELECT 1 FROM a JOIN b ON a.x = b.x JOIN c ON b.y = c.y \
423 WHERE a.v > 1 AND b.w > 1 AND c.z > 1",
424 );
425 assert_eq!(p.pushed_count(), 3);
426 assert!(p.refusals.is_empty());
427 assert!(nullable_bindings(&parse(
428 "SELECT 1 FROM a JOIN b ON a.x = b.x JOIN c ON b.y = c.y"
429 ).unwrap()).is_empty());
430 }
431
432 #[test]
433 fn a_predicate_spanning_two_relations_is_refused_with_a_reason() {
434 let p = plan_for("SELECT 1 FROM a JOIN b ON a.x = b.x WHERE a.v > b.w");
435 assert_eq!(p.pushed_count(), 0);
436 assert_eq!(p.refusals.len(), 1);
437 assert!(p.refusals[0].contains("spans more than one relation"), "{:?}", p.refusals);
438 }
439
440 #[test]
441 fn or_is_never_split() {
442 let p = plan_for("SELECT 1 FROM a JOIN b ON a.x = b.x WHERE a.v > 5 OR b.w < 2");
445 assert_eq!(p.pushed_count(), 0);
446 assert_eq!(p.refusals.len(), 1);
447 }
448
449 #[test]
450 fn an_or_of_one_relation_is_also_refused_today() {
451 let p = plan_for("SELECT 1 FROM a JOIN b ON a.x = b.x WHERE a.v > 5 OR a.v < 1");
455 assert_eq!(p.pushed_count(), 1, "one conjunct, one relation");
456 }
457
458 #[test]
459 fn an_unqualified_column_is_refused() {
460 let p = plan_for("SELECT 1 FROM a JOIN b ON a.x = b.x WHERE v > 5");
461 assert_eq!(p.pushed_count(), 0);
462 assert!(p.refusals[0].contains("unqualified"), "{:?}", p.refusals);
463 }
464
465 #[test]
466 fn a_constant_predicate_is_refused_as_pointless() {
467 let p = plan_for("SELECT 1 FROM a JOIN b ON a.x = b.x WHERE 1 = 1");
468 assert_eq!(p.pushed_count(), 0);
469 assert!(p.refusals[0].contains("reads no column"), "{:?}", p.refusals);
470 }
471
472 #[test]
473 fn a_volatile_function_is_refused_because_the_allowlist_is_fail_safe() {
474 let sel = parse("SELECT 1 FROM a JOIN b ON a.x = b.x").expect("parses");
475 let _ = sel;
476 let pred = Expr::Binary {
477 op: "=".into(),
478 left: Box::new(Expr::Func {
479 name: "random".into(),
480 args: vec![Expr::Column { qual: Some("a".into()), name: "v".into() }],
481 }),
482 right: Box::new(Expr::Literal(serde_json::json!(1))),
483 };
484 let p = plan(Some(&pred), &["a".into(), "b".into()], &[]);
485 assert_eq!(p.pushed_count(), 0);
486 assert!(p.refusals[0].contains("not known to be pure"), "{:?}", p.refusals);
487 }
488
489 #[test]
490 fn pure_functions_and_postfix_operators_are_pushable() {
491 for (w, want) in [
495 ("lower(a.name) = 'x'", 1),
496 ("a.v IS NULL", 1),
497 ("a.v IS NOT NULL", 1),
498 ("a.v IN (1, 2, 3)", 1),
499 ("a.v NOT IN (1, 2)", 1),
500 ("a.v BETWEEN 1 AND 9", 2),
501 ("a.v NOT BETWEEN 1 AND 9", 1),
502 ("coalesce(a.v, 0) > 1", 1),
503 ("a.v::text = '5'", 1),
504 ("NOT (a.v = 3)", 1),
505 ("CASE WHEN a.v > 1 THEN true ELSE false END", 1),
506 ] {
507 let p = plan_for(&format!("SELECT 1 FROM a JOIN b ON a.x = b.x WHERE {w}"));
508 assert_eq!(
509 p.pushed_count(), want,
510 "{w} should push {want}: {:?}", p.refusals
511 );
512 assert!(p.refusals.is_empty(), "{w}: {:?}", p.refusals);
513 }
514 }
515
516 #[test]
517 fn nothing_is_pushed_without_a_join_because_there_is_nothing_to_push_below() {
518 let p = plan_for("SELECT 1 FROM a WHERE a.v > 5");
519 assert_eq!(p.pushed_count(), 0);
520 assert!(p.refusals.is_empty());
522 }
523
524 #[test]
525 fn an_unknown_relation_is_left_to_the_evaluator_to_report() {
526 let p = plan_for("SELECT 1 FROM a JOIN b ON a.x = b.x WHERE zz.v > 5");
527 assert_eq!(p.pushed_count(), 0);
528 assert!(p.refusals[0].contains("unknown relation"), "{:?}", p.refusals);
529 }
530
531 #[test]
532 fn a_binding_is_matched_case_insensitively() {
533 let p = plan_for("SELECT 1 FROM a JOIN b ON a.x = b.x WHERE A.v > 5");
536 assert_eq!(p.for_binding("a").map(|v| v.len()), Some(1));
537 assert_eq!(p.for_binding("A").map(|v| v.len()), Some(1));
538 }
539}