use crate::sqlselect::Expr;
use std::collections::HashMap;
const PURE_FUNCS: &[&str] = &[
"lower", "upper", "length", "char_length", "character_length", "coalesce",
"nullif", "int2", "int4", "int8", "text", "quote_ident", "format_type",
"array_to_string", "current_schema", "current_database", "current_catalog",
"current_user", "session_user", "user", "version", "pg_get_userbyid",
"pg_table_is_visible", "pg_type_is_visible", "pg_function_is_visible",
"pg_encoding_to_char", "pg_get_expr", "pg_get_indexdef",
"pg_get_constraintdef",
];
#[derive(Debug, Clone, Default)]
pub struct Pushdown {
pub per_binding: HashMap<String, Vec<Expr>>,
pub refusals: Vec<String>,
}
impl Pushdown {
pub fn for_binding(&self, binding: &str) -> Option<&Vec<Expr>> {
self.per_binding.get(&binding.to_ascii_lowercase())
}
pub fn pushed_count(&self) -> usize {
self.per_binding.values().map(|v| v.len()).sum()
}
}
fn conjuncts<'a>(e: &'a Expr, out: &mut Vec<&'a Expr>) {
match e {
Expr::Binary { op, left, right } if op == "AND" => {
conjuncts(left, out);
conjuncts(right, out);
}
other => out.push(other),
}
}
enum Reads {
One(String),
Constant,
Refused(&'static str),
}
fn reads(e: &Expr, known: &[String]) -> Reads {
let mut seen: Vec<String> = vec![];
let mut why: Option<&'static str> = None;
walk(e, known, &mut seen, &mut why);
if let Some(w) = why {
return Reads::Refused(w);
}
match seen.len() {
0 => Reads::Constant,
1 => Reads::One(seen.pop().expect("one")),
_ => Reads::Refused("spans more than one relation"),
}
}
fn walk(e: &Expr, known: &[String], seen: &mut Vec<String>, why: &mut Option<&'static str>) {
match e {
Expr::Column { qual, .. } => match qual {
Some(q) => {
let lower = q.to_ascii_lowercase();
if !known.iter().any(|b| b.eq_ignore_ascii_case(q)) {
*why = Some("references an unknown relation");
} else if !seen.contains(&lower) {
seen.push(lower);
}
}
None => *why = Some("unqualified column cannot be attributed to a relation"),
},
Expr::Literal(_) => {}
Expr::Star | Expr::QualifiedStar(_) => *why = Some("contains `*`"),
Expr::Func { name, args } => {
if !PURE_FUNCS.iter().any(|f| f.eq_ignore_ascii_case(name)) {
*why = Some("calls a function not known to be pure");
}
for a in args {
walk(a, known, seen, why);
}
}
Expr::Agg { .. } => *why = Some("contains an aggregate"),
Expr::Case { operand, whens, else_ } => {
if let Some(o) = operand {
walk(o, known, seen, why);
}
for (w, t) in whens {
walk(w, known, seen, why);
walk(t, known, seen, why);
}
if let Some(x) = else_ {
walk(x, known, seen, why);
}
}
Expr::Binary { left, right, .. } => {
walk(left, known, seen, why);
walk(right, known, seen, why);
}
Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
walk(expr, known, seen, why)
}
Expr::InList { expr, list, .. } => {
walk(expr, known, seen, why);
for i in list {
walk(i, known, seen, why);
}
}
Expr::Index { expr, index } => {
walk(expr, known, seen, why);
walk(index, known, seen, why);
}
Expr::ArrayLit(items) => {
for i in items {
walk(i, known, seen, why);
}
}
Expr::Subquery(_) | Expr::Exists { .. } | Expr::ArrayQuery(_) | Expr::InSubquery { .. } => {
*why = Some("contains a subquery")
}
Expr::Quantified { left, right, .. } => {
walk(left, known, seen, why);
walk(right, known, seen, why);
}
}
}
pub fn nullable_bindings(sel: &crate::sqlselect::Select) -> Vec<String> {
use crate::sqlselect::JoinKind;
let mut out: Vec<String> = vec![];
let mut accumulated: Vec<String> = sel
.from
.iter()
.map(|t| t.binding().to_ascii_lowercase())
.collect();
for j in &sel.joins {
let rb = j.table.binding().to_ascii_lowercase();
if matches!(j.kind, JoinKind::Left | JoinKind::Full) && !out.contains(&rb) {
out.push(rb.clone());
}
if matches!(j.kind, JoinKind::Right | JoinKind::Full) {
for a in &accumulated {
if !out.contains(a) {
out.push(a.clone());
}
}
}
accumulated.push(rb);
}
out
}
pub fn plan(
where_: Option<&Expr>,
bindings: &[String],
nullable: &[String],
) -> Pushdown {
let mut out = Pushdown::default();
let Some(w) = where_ else { return out };
if bindings.len() < 2 {
return out;
}
let mut parts = vec![];
conjuncts(w, &mut parts);
for p in parts {
match reads(p, bindings) {
Reads::One(b) if nullable.iter().any(|n| n.eq_ignore_ascii_case(&b)) => {
out.refusals.push(format!(
"Filter retained above join: predicate references nullable \
side of an outer join ({b})"
));
}
Reads::One(b) => out.per_binding.entry(b).or_default().push(p.clone()),
Reads::Constant => out
.refusals
.push("Filter retained above join: predicate reads no column".into()),
Reads::Refused(why) => out
.refusals
.push(format!("Filter retained above join: {why}")),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sqlselect::parse;
fn plan_for(sql: &str) -> Pushdown {
let sel = parse(sql).expect("parses");
let mut b = vec![];
if let Some(f) = &sel.from {
b.push(f.binding());
}
for j in &sel.joins {
b.push(j.table.binding());
}
let nullable = nullable_bindings(&sel);
plan(sel.where_.as_ref(), &b, &nullable)
}
#[test]
fn a_single_relation_predicate_is_pushed_to_that_relation() {
let p = plan_for("SELECT 1 FROM a JOIN b ON a.x = b.x WHERE a.v > 5");
assert_eq!(p.pushed_count(), 1);
assert_eq!(p.for_binding("a").map(|v| v.len()), Some(1));
assert!(p.for_binding("b").is_none());
assert!(p.refusals.is_empty(), "{:?}", p.refusals);
}
#[test]
fn conjuncts_are_pushed_to_their_own_relations_independently() {
let p = plan_for(
"SELECT 1 FROM a JOIN b ON a.x = b.x WHERE a.v > 5 AND b.w < 2 AND a.z = 'q'",
);
assert_eq!(p.pushed_count(), 3);
assert_eq!(p.for_binding("a").map(|v| v.len()), Some(2));
assert_eq!(p.for_binding("b").map(|v| v.len()), Some(1));
}
#[test]
fn a_predicate_on_the_nullable_side_of_a_left_join_is_REFUSED() {
let p = plan_for("SELECT 1 FROM a LEFT JOIN b ON a.x = b.x WHERE b.w = 5");
assert_eq!(p.pushed_count(), 0);
assert!(p.refusals[0].contains("nullable side"), "{:?}", p.refusals);
}
#[test]
fn the_non_nullable_side_of_a_left_join_is_still_pushed() {
let p = plan_for("SELECT 1 FROM a LEFT JOIN b ON a.x = b.x WHERE a.v > 5");
assert_eq!(p.for_binding("a").map(|v| v.len()), Some(1));
assert!(p.refusals.is_empty(), "{:?}", p.refusals);
}
#[test]
fn a_right_join_makes_the_LEFT_side_nullable_including_the_from_relation() {
let p = plan_for("SELECT 1 FROM a RIGHT JOIN b ON a.x = b.x WHERE a.v > 5");
assert_eq!(p.pushed_count(), 0, "a is synthesised by the RIGHT join");
assert!(p.refusals[0].contains("nullable side"), "{:?}", p.refusals);
let p = plan_for("SELECT 1 FROM a RIGHT JOIN b ON a.x = b.x WHERE b.w > 5");
assert_eq!(p.for_binding("b").map(|v| v.len()), Some(1));
}
#[test]
fn a_full_join_makes_both_sides_nullable() {
for w in ["a.v > 5", "b.w > 5"] {
let p = plan_for(&format!("SELECT 1 FROM a FULL JOIN b ON a.x = b.x WHERE {w}"));
assert_eq!(p.pushed_count(), 0, "{w}");
}
}
#[test]
fn a_later_right_join_retroactively_protects_earlier_relations() {
let sel = parse(
"SELECT 1 FROM a JOIN b ON a.x = b.x RIGHT JOIN c ON b.y = c.y \
WHERE a.v > 1 AND b.w > 1 AND c.z > 1",
)
.expect("parses");
let nullable = nullable_bindings(&sel);
assert!(nullable.contains(&"a".to_string()), "{nullable:?}");
assert!(nullable.contains(&"b".to_string()), "{nullable:?}");
assert!(!nullable.contains(&"c".to_string()), "c is never synthesised");
let p = plan_for(
"SELECT 1 FROM a JOIN b ON a.x = b.x RIGHT JOIN c ON b.y = c.y \
WHERE a.v > 1 AND b.w > 1 AND c.z > 1",
);
assert_eq!(p.pushed_count(), 1, "only c");
assert_eq!(p.for_binding("c").map(|v| v.len()), Some(1));
assert_eq!(p.refusals.len(), 2);
}
#[test]
fn an_all_inner_query_can_push_everything() {
let p = plan_for(
"SELECT 1 FROM a JOIN b ON a.x = b.x JOIN c ON b.y = c.y \
WHERE a.v > 1 AND b.w > 1 AND c.z > 1",
);
assert_eq!(p.pushed_count(), 3);
assert!(p.refusals.is_empty());
assert!(nullable_bindings(&parse(
"SELECT 1 FROM a JOIN b ON a.x = b.x JOIN c ON b.y = c.y"
).unwrap()).is_empty());
}
#[test]
fn a_predicate_spanning_two_relations_is_refused_with_a_reason() {
let p = plan_for("SELECT 1 FROM a JOIN b ON a.x = b.x WHERE a.v > b.w");
assert_eq!(p.pushed_count(), 0);
assert_eq!(p.refusals.len(), 1);
assert!(p.refusals[0].contains("spans more than one relation"), "{:?}", p.refusals);
}
#[test]
fn or_is_never_split() {
let p = plan_for("SELECT 1 FROM a JOIN b ON a.x = b.x WHERE a.v > 5 OR b.w < 2");
assert_eq!(p.pushed_count(), 0);
assert_eq!(p.refusals.len(), 1);
}
#[test]
fn an_or_of_one_relation_is_also_refused_today() {
let p = plan_for("SELECT 1 FROM a JOIN b ON a.x = b.x WHERE a.v > 5 OR a.v < 1");
assert_eq!(p.pushed_count(), 1, "one conjunct, one relation");
}
#[test]
fn an_unqualified_column_is_refused() {
let p = plan_for("SELECT 1 FROM a JOIN b ON a.x = b.x WHERE v > 5");
assert_eq!(p.pushed_count(), 0);
assert!(p.refusals[0].contains("unqualified"), "{:?}", p.refusals);
}
#[test]
fn a_constant_predicate_is_refused_as_pointless() {
let p = plan_for("SELECT 1 FROM a JOIN b ON a.x = b.x WHERE 1 = 1");
assert_eq!(p.pushed_count(), 0);
assert!(p.refusals[0].contains("reads no column"), "{:?}", p.refusals);
}
#[test]
fn a_volatile_function_is_refused_because_the_allowlist_is_fail_safe() {
let sel = parse("SELECT 1 FROM a JOIN b ON a.x = b.x").expect("parses");
let _ = sel;
let pred = Expr::Binary {
op: "=".into(),
left: Box::new(Expr::Func {
name: "random".into(),
args: vec![Expr::Column { qual: Some("a".into()), name: "v".into() }],
}),
right: Box::new(Expr::Literal(serde_json::json!(1))),
};
let p = plan(Some(&pred), &["a".into(), "b".into()], &[]);
assert_eq!(p.pushed_count(), 0);
assert!(p.refusals[0].contains("not known to be pure"), "{:?}", p.refusals);
}
#[test]
fn pure_functions_and_postfix_operators_are_pushable() {
for (w, want) in [
("lower(a.name) = 'x'", 1),
("a.v IS NULL", 1),
("a.v IS NOT NULL", 1),
("a.v IN (1, 2, 3)", 1),
("a.v NOT IN (1, 2)", 1),
("a.v BETWEEN 1 AND 9", 2),
("a.v NOT BETWEEN 1 AND 9", 1),
("coalesce(a.v, 0) > 1", 1),
("a.v::text = '5'", 1),
("NOT (a.v = 3)", 1),
("CASE WHEN a.v > 1 THEN true ELSE false END", 1),
] {
let p = plan_for(&format!("SELECT 1 FROM a JOIN b ON a.x = b.x WHERE {w}"));
assert_eq!(
p.pushed_count(), want,
"{w} should push {want}: {:?}", p.refusals
);
assert!(p.refusals.is_empty(), "{w}: {:?}", p.refusals);
}
}
#[test]
fn nothing_is_pushed_without_a_join_because_there_is_nothing_to_push_below() {
let p = plan_for("SELECT 1 FROM a WHERE a.v > 5");
assert_eq!(p.pushed_count(), 0);
assert!(p.refusals.is_empty());
}
#[test]
fn an_unknown_relation_is_left_to_the_evaluator_to_report() {
let p = plan_for("SELECT 1 FROM a JOIN b ON a.x = b.x WHERE zz.v > 5");
assert_eq!(p.pushed_count(), 0);
assert!(p.refusals[0].contains("unknown relation"), "{:?}", p.refusals);
}
#[test]
fn a_binding_is_matched_case_insensitively() {
let p = plan_for("SELECT 1 FROM a JOIN b ON a.x = b.x WHERE A.v > 5");
assert_eq!(p.for_binding("a").map(|v| v.len()), Some(1));
assert_eq!(p.for_binding("A").map(|v| v.len()), Some(1));
}
}