use crate::sqlselect::Expr;
use serde_json::Value;
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
}
pub fn to_nql_predicate(e: &Expr, binding: &str, strict_qual: bool) -> Option<String> {
match e {
Expr::Column { qual, name } => match qual {
Some(q) if q.eq_ignore_ascii_case(binding) => Some(name.clone()),
Some(_) => None,
None if strict_qual => None,
None => Some(name.clone()),
},
Expr::Literal(v) => nql_literal(v),
Expr::Binary { op, left, right } => {
let o = op.to_ascii_uppercase();
let l = to_nql_predicate(left, binding, strict_qual)?;
let r = to_nql_predicate(right, binding, strict_qual)?;
match o.as_str() {
"=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" => Some(format!("{} {} {}", l, o, r)),
"<>" => Some(format!("{} != {}", l, r)),
"AND" => Some(format!("({} AND {})", l, r)),
"OR" => Some(format!("({} OR {})", l, r)),
_ => None,
}
}
Expr::InList { expr, list, negated: false } => {
let l = to_nql_predicate(expr, binding, strict_qual)?;
let mut items = Vec::with_capacity(list.len());
for it in list {
items.push(to_nql_predicate(it, binding, strict_qual)?);
}
if items.is_empty() {
return None;
}
Some(format!("{} IN ({})", l, items.join(", ")))
}
_ => None,
}
}
fn nql_literal(v: &Value) -> Option<String> {
match v {
Value::String(s) => Some(format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))),
Value::Number(n) => Some(n.to_string()),
Value::Bool(b) => Some(if *b { "TRUE".into() } else { "FALSE".into() }),
_ => None,
}
}
pub fn nql_prefilter(
where_: Option<&Expr>,
binding: &str,
bindings: &[String],
nullable: &[String],
) -> Option<String> {
if nullable.iter().any(|n| n.eq_ignore_ascii_case(binding)) {
return None;
}
let w = where_?;
let strict = bindings.len() > 1;
let mut parts = vec![];
conjuncts(w, &mut parts);
let kept: Vec<String> = parts
.iter()
.filter_map(|p| to_nql_predicate(p, binding, strict))
.collect();
if kept.is_empty() {
None
} else {
Some(kept.join(" AND "))
}
}
#[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));
}
fn pre(sql: &str, binding: &str) -> Option<String> {
let sel = parse(sql).expect("parses");
let bindings: Vec<String> = sel
.from
.iter()
.map(|t| t.binding())
.chain(sel.joins.iter().map(|j| j.table.binding()))
.collect();
let nullable = super::nullable_bindings(&sel);
super::nql_prefilter(sel.where_.as_ref(), binding, &bindings, &nullable)
}
#[test]
fn the_predicate_reaches_the_scan_in_nqls_spelling() {
assert_eq!(pre("SELECT 1 FROM orders WHERE status = 'paid'", "orders").as_deref(),
Some("status = \"paid\""));
assert_eq!(pre("SELECT 1 FROM orders WHERE total <> 5", "orders").as_deref(),
Some("total != 5"));
assert_eq!(pre("SELECT 1 FROM orders WHERE total >= 100", "orders").as_deref(),
Some("total >= 100"));
assert_eq!(pre("SELECT 1 FROM orders WHERE status LIKE 'pa%'", "orders").as_deref(),
Some("status LIKE \"pa%\""));
assert_eq!(pre("SELECT 1 FROM orders WHERE status IN ('paid','open')", "orders").as_deref(),
Some("status IN (\"paid\", \"open\")"));
assert_eq!(pre("SELECT 1 FROM orders WHERE a = 1 OR b = 2", "orders").as_deref(),
Some("(a = 1 OR b = 2)"));
assert_eq!(pre("SELECT 1 FROM orders WHERE s = 'a\"b'", "orders").as_deref(),
Some("s = \"a\\\"b\""));
}
#[test]
fn anything_that_could_drop_a_row_sql_keeps_is_refused() {
for sql in [
"SELECT 1 FROM orders WHERE NOT (status = 'paid')",
"SELECT 1 FROM orders WHERE status IS NULL",
"SELECT 1 FROM orders WHERE status IS NOT NULL",
"SELECT 1 FROM orders WHERE status NOT IN ('paid')",
"SELECT 1 FROM orders WHERE status = NULL",
"SELECT 1 FROM orders WHERE total + 1 > 5",
"SELECT 1 FROM orders WHERE lower(status) = 'paid'",
"SELECT 1 FROM orders WHERE total::text = '5'",
] {
assert_eq!(pre(sql, "orders"), None, "{}", sql);
}
}
#[test]
fn a_conjunction_pushes_the_part_it_can_and_keeps_the_rest_above() {
assert_eq!(pre("SELECT 1 FROM orders WHERE status = 'paid' AND lower(x) = 'y'", "orders")
.as_deref(),
Some("status = \"paid\""));
assert_eq!(pre("SELECT 1 FROM orders WHERE status = 'paid' OR lower(x) = 'y'", "orders"),
None);
}
#[test]
fn another_relations_predicate_never_reaches_this_scan() {
let sql = "SELECT 1 FROM orders o JOIN drivers d ON o.driver = d._id \
WHERE o.status = 'paid' AND d.name = 'Bob'";
assert_eq!(pre(sql, "o").as_deref(), Some("status = \"paid\""));
assert_eq!(pre(sql, "d").as_deref(), Some("name = \"Bob\""));
assert_eq!(pre("SELECT 1 FROM a JOIN b ON a.x = b.x WHERE v > 5", "a"), None);
assert_eq!(pre("SELECT 1 FROM orders WHERE v > 5", "orders").as_deref(), Some("v > 5"));
}
#[test]
fn the_nullable_side_of_an_outer_join_is_never_pre_filtered() {
let sql = "SELECT 1 FROM a LEFT JOIN b ON a.x = b.x WHERE b.v = 5";
assert_eq!(pre(sql, "b"), None);
}
}