use rudb_common::{LogicalType, Result};
use rudb_plan::{
BuildSide, CompareOp, ConjunctionOp, Expr, ExprRef, JoinKind, Node, NodeRef, Plan,
};
use crate::pass::{Context, Pass};
use crate::tables::{TableSet, Tables, produced};
use crate::{fold, nulls, transitive, walk};
#[derive(Debug, Clone, Copy)]
pub struct FilterPushdown;
impl Pass for FilterPushdown {
fn name(&self) -> &'static str {
"filter_pushdown"
}
fn run(&self, plan: &mut Plan, _context: &Context) -> Result<()> {
push(plan);
Ok(())
}
}
pub fn push(plan: &mut Plan) {
let mut tables = Tables::new();
let root = node(plan, plan.root(), Vec::new(), &mut tables);
plan.set_root(root);
}
fn node(plan: &mut Plan, at: NodeRef, pending: Vec<ExprRef>, tables: &mut Tables) -> NodeRef {
match *plan.node(at) {
Node::Filter { input, predicate } => {
let mut parts = pending;
split(plan, predicate, &mut parts);
transitive::within(plan, &mut parts);
node(plan, input, parts, tables)
}
Node::Project { input, index, exprs, names } => {
let held = plan.expr_list(exprs).to_vec();
let (down, stay) = partition(plan, pending, index, &held);
let moved = down.into_iter().map(|part| substituted(plan, part, index, &held));
let moved: Vec<ExprRef> = moved.collect();
let rebuilt = node(plan, input, moved, tables);
let above = if rebuilt == input {
at
} else {
plan.add_node(Node::Project { input: rebuilt, index, exprs, names })
};
filter(plan, above, stay)
}
Node::Aggregate { input, index, groups, aggregates } => {
let keys = plan.expr_list(groups).to_vec();
let (down, stay) = if keys.is_empty() {
(Vec::new(), pending)
} else {
partition(plan, pending, index, &keys)
};
let moved = down.into_iter().map(|part| substituted(plan, part, index, &keys));
let moved: Vec<ExprRef> = moved.collect();
let rebuilt = node(plan, input, moved, tables);
let above = if rebuilt == input {
at
} else {
plan.add_node(Node::Aggregate { input: rebuilt, index, groups, aggregates })
};
filter(plan, above, stay)
}
Node::Window { input, index, partition, order, frame, expressions } => {
let rebuilt = node(plan, input, Vec::new(), tables);
let above = if rebuilt == input {
at
} else {
plan.add_node(Node::Window {
input: rebuilt,
index,
partition,
order,
frame,
expressions,
})
};
filter(plan, above, pending)
}
Node::Sort { input, keys } => {
let rebuilt = node(plan, input, pending, tables);
if rebuilt == input { at } else { plan.add_node(Node::Sort { input: rebuilt, keys }) }
}
Node::Distinct { input, on } => {
let whole_row = plan.expr_list(on).is_empty();
let (down, stay) =
if whole_row { (pending, Vec::new()) } else { (Vec::new(), pending) };
let rebuilt = node(plan, input, down, tables);
let above = if rebuilt == input {
at
} else {
plan.add_node(Node::Distinct { input: rebuilt, on })
};
filter(plan, above, stay)
}
Node::Fetch { input, index, args, columns, row } => {
let rebuilt = node(plan, input, Vec::new(), tables);
let above = if rebuilt == input {
at
} else {
plan.add_node(Node::Fetch { input: rebuilt, index, args, columns, row })
};
filter(plan, above, pending)
}
Node::TableFetch { input, index, catalog, schema, table, columns, row } => {
let rebuilt = node(plan, input, Vec::new(), tables);
let above = if rebuilt == input {
at
} else {
plan.add_node(Node::TableFetch {
input: rebuilt,
index,
catalog,
schema,
table,
columns,
row,
})
};
filter(plan, above, pending)
}
Node::Limit { input, count, offset } => {
let rebuilt = node(plan, input, Vec::new(), tables);
let above = if rebuilt == input {
at
} else {
plan.add_node(Node::Limit { input: rebuilt, count, offset })
};
filter(plan, above, pending)
}
Node::TopN { input, keys, count, offset } => {
let rebuilt = node(plan, input, Vec::new(), tables);
let above = if rebuilt == input {
at
} else {
plan.add_node(Node::TopN { input: rebuilt, keys, count, offset })
};
filter(plan, above, pending)
}
Node::Join { left, right, kind: written, conditions, build } => {
let below = (produced(plan, left), produced(plan, right));
let kind = nulls::narrow(plan, written, &pending, (&below.0, &below.1));
let mut held = Vec::new();
for &condition in plan.expr_list(conditions) {
split(plan, condition, &mut held);
}
let (extra_left, extra_right) =
transitive::across(plan, tables, kind, &held, &pending, (&below.0, &below.1));
let (mut to_left, mut to_right, over) =
sides(plan, tables, pending, &below, kept(kind));
to_left.extend(extra_left);
to_right.extend(extra_right);
let (added, stay) = if kind == JoinKind::Inner {
onto(plan, tables, &held, over, &below)
} else {
(Vec::new(), over)
};
let rebuilt_left = node(plan, left, to_left, tables);
let rebuilt_right = node(plan, right, to_right, tables);
let rebuilt_conditions = if added.is_empty() && held == plan.expr_list(conditions) {
conditions
} else {
let all: Vec<ExprRef> = held.into_iter().chain(added).collect();
plan.add_expr_list(&all)
};
let above = if rebuilt_left == left
&& rebuilt_right == right
&& rebuilt_conditions == conditions
&& kind == written
{
at
} else {
plan.add_node(Node::Join {
left: rebuilt_left,
right: rebuilt_right,
kind,
conditions: rebuilt_conditions,
build,
})
};
filter(plan, above, stay)
}
Node::DependentJoin { left, right, kind, conditions } => {
let rebuilt_left = node(plan, left, Vec::new(), tables);
let rebuilt_right = node(plan, right, Vec::new(), tables);
let above = if rebuilt_left == left && rebuilt_right == right {
at
} else {
plan.add_node(Node::DependentJoin {
left: rebuilt_left,
right: rebuilt_right,
kind,
conditions,
})
};
filter(plan, above, pending)
}
Node::CrossProduct { left, right } => {
let below = (produced(plan, left), produced(plan, right));
let (to_left, to_right, over) = sides(plan, tables, pending, &below, (true, true));
let (conditions, stay) = joined(plan, tables, over, &below);
let rebuilt_left = node(plan, left, to_left, tables);
let rebuilt_right = node(plan, right, to_right, tables);
let above = if conditions.is_empty() {
if rebuilt_left == left && rebuilt_right == right {
at
} else {
plan.add_node(Node::CrossProduct { left: rebuilt_left, right: rebuilt_right })
}
} else {
let conditions = plan.add_expr_list(&conditions);
plan.add_node(Node::Join {
left: rebuilt_left,
right: rebuilt_right,
kind: JoinKind::Inner,
conditions,
build: BuildSide::default(),
})
};
filter(plan, above, stay)
}
Node::SetOp { left, right, kind, all, index } => {
let rebuilt_left = node(plan, left, Vec::new(), tables);
let rebuilt_right = node(plan, right, Vec::new(), tables);
let above = if rebuilt_left == left && rebuilt_right == right {
at
} else {
plan.add_node(Node::SetOp {
left: rebuilt_left,
right: rebuilt_right,
kind,
all,
index,
})
};
filter(plan, above, pending)
}
Node::MaterializedCte { definition, body, name, cte, columns } => {
let rebuilt_definition = node(plan, definition, Vec::new(), tables);
let rebuilt_body = node(plan, body, pending, tables);
if rebuilt_definition == definition && rebuilt_body == body {
at
} else {
plan.add_node(Node::MaterializedCte {
definition: rebuilt_definition,
body: rebuilt_body,
name,
cte,
columns,
})
}
}
Node::Get { .. }
| Node::Values { .. }
| Node::TableFunction { .. }
| Node::Dummy
| Node::CteScan { .. } => filter(plan, at, pending),
}
}
pub(crate) fn kept(kind: JoinKind) -> (bool, bool) {
match kind {
JoinKind::Inner => (true, true),
JoinKind::Left | JoinKind::Semi | JoinKind::Anti | JoinKind::Single | JoinKind::Mark => {
(true, false)
}
JoinKind::Right => (false, true),
JoinKind::Full | JoinKind::Positional => (false, false),
}
}
fn sides(
plan: &Plan,
tables: &mut Tables,
pending: Vec<ExprRef>,
below: &(TableSet, TableSet),
kept: (bool, bool),
) -> (Vec<ExprRef>, Vec<ExprRef>, Vec<ExprRef>) {
let (below_left, below_right) = (&below.0, &below.1);
let mut to_left = Vec::new();
let mut to_right = Vec::new();
let mut over = Vec::new();
for part in pending {
let read = tables.of(plan, part);
if kept.0 && read.is_subset_of(below_left) {
to_left.push(part);
} else if kept.1 && read.is_subset_of(below_right) {
to_right.push(part);
} else {
over.push(part);
}
}
(to_left, to_right, over)
}
fn answered(plan: &Plan, tables: &mut Tables, part: ExprRef, below: &(TableSet, TableSet)) -> bool {
let Expr::Compare { op: CompareOp::Equal | CompareOp::NotDistinctFrom, left, right } =
*plan.expr(part)
else {
return false;
};
if !matches!((plan.expr(left), plan.expr(right)), (Expr::Column(_), Expr::Column(_))) {
return false;
}
if plan.expr_type(left) != plan.expr_type(right) || !plan.expr_type(left).is_keyed() {
return false;
}
let (one, other) = (tables.of(plan, left), tables.of(plan, right));
(one.is_subset_of(&below.0) && other.is_subset_of(&below.1))
|| (one.is_subset_of(&below.1) && other.is_subset_of(&below.0))
}
fn onto(
plan: &Plan,
tables: &mut Tables,
held: &[ExprRef],
over: Vec<ExprRef>,
below: &(TableSet, TableSet),
) -> (Vec<ExprRef>, Vec<ExprRef>) {
let (fit, unfit): (Vec<ExprRef>, Vec<ExprRef>) =
over.into_iter().partition(|&part| answered(plan, tables, part, below));
let lookup = !(held.is_empty() && fit.is_empty())
&& held.iter().all(|&part| answered(plan, tables, part, below));
if lookup {
return (fit, unfit);
}
(fit.into_iter().chain(unfit).collect(), Vec::new())
}
fn joined(
plan: &Plan,
tables: &mut Tables,
over: Vec<ExprRef>,
below: &(TableSet, TableSet),
) -> (Vec<ExprRef>, Vec<ExprRef>) {
over.into_iter().partition(|&part| answered(plan, tables, part, below))
}
fn filter(plan: &mut Plan, input: NodeRef, parts: Vec<ExprRef>) -> NodeRef {
let parts: Vec<ExprRef> = parts.into_iter().filter(|&part| !always(plan, part)).collect();
if let Some(&decided) = parts.iter().find(|&&part| never(plan, part)) {
return plan.add_node(Node::Filter { input, predicate: decided });
}
let mut parts = parts;
let mut held = 0;
while held < parts.len() {
let part = parts[held];
let earlier = parts[..held].iter().any(|&other| walk::same(plan, other, part));
if earlier && !walk::volatile(plan, part) {
parts.remove(held);
} else {
held += 1;
}
}
let predicate = match parts.len() {
0 => return input,
1 => parts[0],
_ => {
let children = plan.add_expr_list(&parts);
let conjunction = Expr::Conjunction { op: ConjunctionOp::And, children };
plan.add_expr(conjunction, LogicalType::Boolean)
}
};
plan.add_node(Node::Filter { input, predicate })
}
fn always(plan: &Plan, predicate: ExprRef) -> bool {
let Expr::Constant(value) = *plan.expr(predicate) else {
return false;
};
plan.value(value).as_bool() == Some(true)
}
fn never(plan: &Plan, predicate: ExprRef) -> bool {
let Expr::Constant(value) = *plan.expr(predicate) else {
return false;
};
plan.value(value).as_bool() != Some(true)
}
fn split(plan: &Plan, predicate: ExprRef, into: &mut Vec<ExprRef>) {
if let Expr::Conjunction { op: ConjunctionOp::And, children } = *plan.expr(predicate) {
for &child in plan.expr_list(children) {
split(plan, child, into);
}
} else {
into.push(predicate);
}
}
fn partition(
plan: &Plan,
pending: Vec<ExprRef>,
index: u32,
held: &[ExprRef],
) -> (Vec<ExprRef>, Vec<ExprRef>) {
let mut down = Vec::new();
let mut stay = Vec::new();
for part in pending {
if substitutable(plan, part, index, held) {
down.push(part);
} else {
stay.push(part);
}
}
(down, stay)
}
fn substitutable(plan: &Plan, expr: ExprRef, index: u32, held: &[ExprRef]) -> bool {
let mut answer = true;
walk::columns(plan, expr, &mut |binding| {
answer &= binding.table == index;
answer &= match held.get(binding.column as usize) {
Some(&source) => !walk::volatile(plan, source) && walk::elementwise(plan, source),
None => false,
};
});
answer
}
fn substituted(plan: &mut Plan, expr: ExprRef, index: u32, held: &[ExprRef]) -> ExprRef {
let substituted = substitute(plan, expr, index, held);
fold::rewritten(plan, substituted)
}
fn substitute(plan: &mut Plan, expr: ExprRef, index: u32, held: &[ExprRef]) -> ExprRef {
if let Expr::Column(binding) = *plan.expr(expr) {
return if binding.table == index { held[binding.column as usize] } else { expr };
}
walk::rebuild(plan, expr, &mut |plan, child| substitute(plan, child, index, held))
}
#[cfg(test)]
mod tests {
use super::FilterPushdown;
use crate::pass::{Context, Pass};
use rudb_plan::Plan;
fn pushed(text: &str) -> String {
let mut plan =
Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
FilterPushdown
.run(&mut plan, &Context::new())
.unwrap_or_else(|error| panic!("{text} did not push: {error}"));
plan.validate().unwrap_or_else(|error| panic!("{text} pushed to a bad plan: {error}"));
plan.to_string()
}
#[test]
fn a_filter_over_a_projection_lands_under_it() {
let before = "\
Filter (#1.0::INTEGER > 1::INTEGER)::BOOLEAN
Project #1 [#0.0::INTEGER AS x]
Get memory.main.t AS t #0 [a::INTEGER]
";
let after = "\
Project #1 [#0.0::INTEGER AS x]
Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN
Get memory.main.t AS t #0 [a::INTEGER]
";
assert_eq!(pushed(before), after);
}
#[test]
fn a_computed_column_is_rewritten_into_what_it_is_computed_from() {
let before = "\
Filter (#1.0::INTEGER > 2::INTEGER)::BOOLEAN
Project #1 [\"+\"(#0.0::INTEGER, 1::INTEGER)::INTEGER AS y]
Get memory.main.t AS t #0 [a::INTEGER]
";
let after = "\
Project #1 [\"+\"(#0.0::INTEGER, 1::INTEGER)::INTEGER AS y]
Filter (\"+\"(#0.0::INTEGER, 1::INTEGER)::INTEGER > 2::INTEGER)::BOOLEAN
Get memory.main.t AS t #0 [a::INTEGER]
";
assert_eq!(pushed(before), after);
}
#[test]
fn a_projection_that_is_not_the_same_twice_keeps_its_filter_above_it() {
let text = "\
Filter (#1.0::DOUBLE > 0.5::DOUBLE)::BOOLEAN
Project #1 [random()::DOUBLE AS r]
Get memory.main.t AS t #0 [a::INTEGER]
";
assert_eq!(pushed(text), text);
}
#[test]
fn a_predicate_over_a_group_key_goes_under_the_grouping() {
let before = "\
Filter (#1.0::INTEGER > 1::INTEGER)::BOOLEAN
Aggregate #1 groups=[#0.0::INTEGER] aggregates=[count_star()::BIGINT]
Get memory.main.t AS t #0 [a::INTEGER]
";
let after = "\
Aggregate #1 groups=[#0.0::INTEGER] aggregates=[count_star()::BIGINT]
Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN
Get memory.main.t AS t #0 [a::INTEGER]
";
assert_eq!(pushed(before), after);
}
#[test]
fn a_conjunction_is_split_so_the_half_about_a_group_key_moves_on_its_own() {
let before = "\
Filter ((#1.0::INTEGER > 1::INTEGER)::BOOLEAN AND (#1.1::BIGINT > 2::BIGINT)::BOOLEAN)::BOOLEAN
Aggregate #1 groups=[#0.0::INTEGER] aggregates=[count_star()::BIGINT]
Get memory.main.t AS t #0 [a::INTEGER]
";
let after = "\
Filter (#1.1::BIGINT > 2::BIGINT)::BOOLEAN
Aggregate #1 groups=[#0.0::INTEGER] aggregates=[count_star()::BIGINT]
Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN
Get memory.main.t AS t #0 [a::INTEGER]
";
assert_eq!(pushed(before), after);
}
#[test]
fn a_predicate_that_substitution_turns_into_a_constant_is_folded_on_the_way_down() {
let before = "\
Filter CAST(#1.0::\"NULL\")::BOOLEAN
Project #1 [NULL::\"NULL\" AS a]
Get memory.main.t AS t #0 [a::INTEGER]
";
let after = "\
Project #1 [NULL::\"NULL\" AS a]
Filter NULL::BOOLEAN
Get memory.main.t AS t #0 [a::INTEGER]
";
assert_eq!(pushed(before), after);
}
#[test]
fn nothing_crosses_an_aggregate_that_has_no_group_keys() {
let text = "\
Filter FALSE::BOOLEAN
Aggregate #1 groups=[] aggregates=[count_star()::BIGINT]
Get memory.main.t AS t #0 [a::INTEGER]
";
assert_eq!(pushed(text), text);
}
#[test]
fn a_filter_that_keeps_nothing_leaves_the_rest_of_the_conjunction_out() {
let before = "\
Filter (FALSE::BOOLEAN AND (#0.0::INTEGER > 1::INTEGER)::BOOLEAN)::BOOLEAN
Get memory.main.t AS t #0 [a::INTEGER]
";
let after = "\
Filter FALSE::BOOLEAN
Get memory.main.t AS t #0 [a::INTEGER]
";
assert_eq!(pushed(before), after);
}
#[test]
fn the_same_predicate_twice_comes_out_once() {
let before = "\
Filter ((#0.0::INTEGER > 1::INTEGER)::BOOLEAN AND (#0.0::INTEGER > 1::INTEGER)::BOOLEAN)::BOOLEAN
Get memory.main.t AS t #0 [a::INTEGER]
";
let after = "\
Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN
Get memory.main.t AS t #0 [a::INTEGER]
";
assert_eq!(pushed(before), after);
}
#[test]
fn two_volatile_predicates_that_read_the_same_stay_two() {
let text = "\
Filter ((random()::DOUBLE > 0.5::DOUBLE)::BOOLEAN AND (random()::DOUBLE > 0.5::DOUBLE)::BOOLEAN)::BOOLEAN
Get memory.main.t AS t #0 [a::INTEGER]
";
assert_eq!(pushed(text), text);
}
#[test]
fn two_filters_that_keep_nothing_come_out_as_one() {
let before = "\
Filter NULL::BOOLEAN
Project #1 [#0.0::INTEGER AS a]
Filter NULL::BOOLEAN
Get memory.main.t AS t #0 [a::INTEGER]
";
let after = "\
Project #1 [#0.0::INTEGER AS a]
Filter NULL::BOOLEAN
Get memory.main.t AS t #0 [a::INTEGER]
";
assert_eq!(pushed(before), after);
}
#[test]
fn a_filter_crosses_a_sort() {
let before = "\
Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN
Sort [#0.0::INTEGER ASC NULLS LAST]
Get memory.main.t AS t #0 [a::INTEGER]
";
let after = "\
Sort [#0.0::INTEGER ASC NULLS LAST]
Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN
Get memory.main.t AS t #0 [a::INTEGER]
";
assert_eq!(pushed(before), after);
}
#[test]
fn a_filter_does_not_cross_a_limit() {
let text = "\
Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN
Limit 2 offset 0
Get memory.main.t AS t #0 [a::INTEGER]
";
assert_eq!(pushed(text), text);
}
#[test]
fn a_filter_crosses_a_plain_distinct_and_not_a_distinct_on() {
let before = "\
Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN
Distinct on=[]
Get memory.main.t AS t #0 [a::INTEGER]
";
let after = "\
Distinct on=[]
Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN
Get memory.main.t AS t #0 [a::INTEGER]
";
assert_eq!(pushed(before), after);
let on = "\
Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN
Distinct on=[#0.0::INTEGER]
Get memory.main.t AS t #0 [a::INTEGER]
";
assert_eq!(pushed(on), on);
}
#[test]
fn two_filters_in_a_row_become_one() {
let before = "\
Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN
Filter (#0.1::VARCHAR = 'a'::VARCHAR)::BOOLEAN
Get memory.main.t AS t #0 [a::INTEGER, b::VARCHAR]
";
let after = "\
Filter ((#0.0::INTEGER > 1::INTEGER)::BOOLEAN AND (#0.1::VARCHAR = 'a'::VARCHAR)::BOOLEAN)::BOOLEAN
Get memory.main.t AS t #0 [a::INTEGER, b::VARCHAR]
";
assert_eq!(pushed(before), after);
}
#[test]
fn a_predicate_over_one_side_of_an_inner_join_goes_into_that_side() {
let before = "\
Filter ((#0.0::INTEGER = 1::INTEGER)::BOOLEAN AND (#1.0::INTEGER = 2::INTEGER)::BOOLEAN)::BOOLEAN
Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]
Get memory.main.t AS a #0 [a::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER]
";
let after = "\
Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]
Filter ((#0.0::INTEGER = 1::INTEGER)::BOOLEAN AND (#0.0::INTEGER = 2::INTEGER)::BOOLEAN)::BOOLEAN
Get memory.main.t AS a #0 [a::INTEGER]
Filter ((#1.0::INTEGER = 2::INTEGER)::BOOLEAN AND (#1.0::INTEGER = 1::INTEGER)::BOOLEAN)::BOOLEAN
Get memory.main.t AS b #1 [a::INTEGER]
";
assert_eq!(pushed(before), after);
}
#[test]
fn a_predicate_over_both_sides_of_an_inner_join_becomes_a_condition_of_it() {
let before = "\
Filter (#0.1::INTEGER = #1.1::INTEGER)::BOOLEAN
Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]
Get memory.main.t AS a #0 [a::INTEGER, b::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER, b::INTEGER]
";
let after = "\
Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN, (#0.1::INTEGER = #1.1::INTEGER)::BOOLEAN]
Get memory.main.t AS a #0 [a::INTEGER, b::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER, b::INTEGER]
";
assert_eq!(pushed(before), after);
}
#[test]
fn an_and_written_inside_a_join_condition_becomes_two_conditions() {
let before = "\
Join INNER on=[((#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN AND (#0.1::INTEGER = #1.1::INTEGER)::BOOLEAN)::BOOLEAN]
Get memory.main.t AS a #0 [a::INTEGER, b::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER, b::INTEGER]
";
let after = "\
Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN, (#0.1::INTEGER = #1.1::INTEGER)::BOOLEAN]
Get memory.main.t AS a #0 [a::INTEGER, b::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER, b::INTEGER]
";
assert_eq!(pushed(before), after);
}
#[test]
fn an_or_written_inside_a_join_condition_stays_one_condition() {
let before = "\
Join LEFT on=[((#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN OR (#0.1::INTEGER = #1.1::INTEGER)::BOOLEAN)::BOOLEAN]
Get memory.main.t AS a #0 [a::INTEGER, b::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER, b::INTEGER]
";
assert_eq!(pushed(before), before);
}
#[test]
fn splitting_a_join_condition_leaves_an_outer_join_the_kind_it_was() {
let before = "\
Join LEFT on=[((#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN AND (#0.1::INTEGER = #1.1::INTEGER)::BOOLEAN)::BOOLEAN]
Get memory.main.t AS a #0 [a::INTEGER, b::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER, b::INTEGER]
";
let after = "\
Join LEFT on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN, (#0.1::INTEGER = #1.1::INTEGER)::BOOLEAN]
Get memory.main.t AS a #0 [a::INTEGER, b::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER, b::INTEGER]
";
assert_eq!(pushed(before), after);
}
#[test]
fn only_the_kept_side_of_an_outer_join_takes_a_predicate() {
let before = "\
Filter ((#0.0::INTEGER = 1::INTEGER)::BOOLEAN AND (#1.0::INTEGER IS NOT DISTINCT FROM NULL::\"NULL\")::BOOLEAN)::BOOLEAN
Join LEFT on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]
Get memory.main.t AS a #0 [a::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER]
";
let after = "\
Filter (#1.0::INTEGER IS NOT DISTINCT FROM NULL::\"NULL\")::BOOLEAN
Join LEFT on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]
Filter (#0.0::INTEGER = 1::INTEGER)::BOOLEAN
Get memory.main.t AS a #0 [a::INTEGER]
Filter (#1.0::INTEGER = 1::INTEGER)::BOOLEAN
Get memory.main.t AS b #1 [a::INTEGER]
";
assert_eq!(pushed(before), after);
}
#[test]
fn a_predicate_over_both_sides_of_an_outer_join_stays_above_it() {
let text = "\
Filter (#0.0::INTEGER IS NOT DISTINCT FROM #1.0::INTEGER)::BOOLEAN
Join LEFT on=[(#0.1::INTEGER = #1.1::INTEGER)::BOOLEAN]
Get memory.main.t AS a #0 [a::INTEGER, b::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER, b::INTEGER]
";
assert_eq!(pushed(text), text);
}
#[test]
fn a_full_outer_join_takes_nothing_and_neither_does_a_positional_one() {
let full = "\
Filter (#0.0::INTEGER IS NOT DISTINCT FROM NULL::\"NULL\")::BOOLEAN
Join FULL on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]
Get memory.main.t AS a #0 [a::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER]
";
assert_eq!(pushed(full), full);
let positional = "\
Filter (#0.0::INTEGER = 1::INTEGER)::BOOLEAN
Join POSITIONAL on=[]
Get memory.main.t AS a #0 [a::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER]
";
assert_eq!(pushed(positional), positional);
}
#[test]
fn a_left_join_whose_padded_rows_are_all_filtered_out_is_an_inner_join() {
let before = "\
Filter (#1.0::INTEGER = 2::INTEGER)::BOOLEAN
Join LEFT on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]
Get memory.main.t AS a #0 [a::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER]
";
let after = "\
Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]
Filter (#0.0::INTEGER = 2::INTEGER)::BOOLEAN
Get memory.main.t AS a #0 [a::INTEGER]
Filter (#1.0::INTEGER = 2::INTEGER)::BOOLEAN
Get memory.main.t AS b #1 [a::INTEGER]
";
assert_eq!(pushed(before), after);
}
#[test]
fn is_not_null_over_the_padded_side_is_the_same_rewrite() {
let before = "\
Filter (#1.0::INTEGER IS DISTINCT FROM NULL::\"NULL\")::BOOLEAN
Join LEFT on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]
Get memory.main.t AS a #0 [a::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER]
";
let after = "\
Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]
Filter (#0.0::INTEGER IS DISTINCT FROM NULL::\"NULL\")::BOOLEAN
Get memory.main.t AS a #0 [a::INTEGER]
Filter (#1.0::INTEGER IS DISTINCT FROM NULL::\"NULL\")::BOOLEAN
Get memory.main.t AS b #1 [a::INTEGER]
";
assert_eq!(pushed(before), after);
}
#[test]
fn a_full_outer_join_becomes_the_one_sided_join_the_predicate_left_of_it() {
let before = "\
Filter (#0.0::INTEGER = 1::INTEGER)::BOOLEAN
Join FULL on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]
Get memory.main.t AS a #0 [a::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER]
";
let after = "\
Join LEFT on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]
Filter (#0.0::INTEGER = 1::INTEGER)::BOOLEAN
Get memory.main.t AS a #0 [a::INTEGER]
Filter (#1.0::INTEGER = 1::INTEGER)::BOOLEAN
Get memory.main.t AS b #1 [a::INTEGER]
";
assert_eq!(pushed(before), after);
}
#[test]
fn a_predicate_over_one_side_of_a_cross_product_goes_into_that_side() {
let before = "\
Filter (#0.0::INTEGER > 5::INTEGER)::BOOLEAN
CrossProduct
Get memory.main.t AS a #0 [a::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER]
";
let after = "\
CrossProduct
Filter (#0.0::INTEGER > 5::INTEGER)::BOOLEAN
Get memory.main.t AS a #0 [a::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER]
";
assert_eq!(pushed(before), after);
}
#[test]
fn a_cross_product_under_an_equality_becomes_an_inner_join() {
let before = "\
Filter ((#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN AND (#0.1::INTEGER > 5::INTEGER)::BOOLEAN)::BOOLEAN
CrossProduct
Get memory.main.t AS a #0 [a::INTEGER, b::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER, b::INTEGER]
";
let after = "\
Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]
Filter (#0.1::INTEGER > 5::INTEGER)::BOOLEAN
Get memory.main.t AS a #0 [a::INTEGER, b::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER, b::INTEGER]
";
assert_eq!(pushed(before), after);
}
#[test]
fn a_cross_product_under_a_null_safe_equality_becomes_an_inner_join_too() {
let before = "\
Filter (#0.0::INTEGER IS NOT DISTINCT FROM #1.0::INTEGER)::BOOLEAN
CrossProduct
Get memory.main.t AS a #0 [a::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER]
";
let after = "\
Join INNER on=[(#0.0::INTEGER IS NOT DISTINCT FROM #1.0::INTEGER)::BOOLEAN]
Get memory.main.t AS a #0 [a::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER]
";
assert_eq!(pushed(before), after);
}
#[test]
fn a_cross_product_under_a_comparison_that_is_not_an_equality_stays_a_cross_product() {
let before = "\
Filter (#0.0::INTEGER < #1.0::INTEGER)::BOOLEAN
CrossProduct
Get memory.main.t AS a #0 [a::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER]
";
assert_eq!(pushed(before), before);
}
#[test]
fn an_equality_between_a_column_and_an_expression_does_not_make_a_join() {
let before = "\
Filter (#0.0::INTEGER = \"+\"(#1.0::INTEGER, 1::INTEGER)::INTEGER)::BOOLEAN
CrossProduct
Get memory.main.t AS a #0 [a::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER]
";
assert_eq!(pushed(before), before);
}
#[test]
fn the_equalities_over_a_cross_product_become_conditions_and_the_rest_stays_above() {
let before = "\
Filter ((#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN AND (#0.1::INTEGER < #1.1::INTEGER)::BOOLEAN)::BOOLEAN
CrossProduct
Get memory.main.t AS a #0 [a::INTEGER, b::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER, b::INTEGER]
";
let after = "\
Filter (#0.1::INTEGER < #1.1::INTEGER)::BOOLEAN
Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]
Get memory.main.t AS a #0 [a::INTEGER, b::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER, b::INTEGER]
";
assert_eq!(pushed(before), after);
}
#[test]
fn a_predicate_a_lookup_cannot_answer_does_not_join_a_join_that_uses_one() {
let before = "\
Filter (#0.1::INTEGER < #1.1::INTEGER)::BOOLEAN
Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]
Get memory.main.t AS a #0 [a::INTEGER, b::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER, b::INTEGER]
";
assert_eq!(pushed(before), before);
}
#[test]
fn a_predicate_over_both_sides_of_a_join_no_lookup_answers_becomes_a_condition() {
let before = "\
Filter (#0.1::INTEGER < #1.1::INTEGER)::BOOLEAN
Join INNER on=[(#0.0::INTEGER < #1.0::INTEGER)::BOOLEAN]
Get memory.main.t AS a #0 [a::INTEGER, b::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER, b::INTEGER]
";
let after = "\
Join INNER on=[(#0.0::INTEGER < #1.0::INTEGER)::BOOLEAN, (#0.1::INTEGER < #1.1::INTEGER)::BOOLEAN]
Get memory.main.t AS a #0 [a::INTEGER, b::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER, b::INTEGER]
";
assert_eq!(pushed(before), after);
}
#[test]
fn a_predicate_over_a_projection_above_a_join_reaches_the_side_it_reads() {
let before = "\
Filter (#2.0::INTEGER > 5::INTEGER)::BOOLEAN
Project #2 [#1.0::INTEGER AS x]
Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]
Get memory.main.t AS a #0 [a::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER]
";
let after = "\
Project #2 [#1.0::INTEGER AS x]
Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]
Filter (#0.0::INTEGER > 5::INTEGER)::BOOLEAN
Get memory.main.t AS a #0 [a::INTEGER]
Filter (#1.0::INTEGER > 5::INTEGER)::BOOLEAN
Get memory.main.t AS b #1 [a::INTEGER]
";
assert_eq!(pushed(before), after);
}
#[test]
fn a_filter_inside_one_side_of_a_join_still_moves_down_that_side() {
let before = "\
Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]
Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN
Sort [#0.0::INTEGER ASC NULLS LAST]
Get memory.main.t AS a #0 [a::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER]
";
let after = "\
Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]
Sort [#0.0::INTEGER ASC NULLS LAST]
Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN
Get memory.main.t AS a #0 [a::INTEGER]
Get memory.main.t AS b #1 [a::INTEGER]
";
assert_eq!(pushed(before), after);
}
#[test]
fn a_plan_it_has_already_moved_is_left_where_it_put_it() {
let before = "\
Filter (#1.0::INTEGER > 1::INTEGER)::BOOLEAN
Project #1 [#0.0::INTEGER AS x]
Get memory.main.t AS t #0 [a::INTEGER]
";
let once = pushed(before);
assert_eq!(pushed(&once), once);
}
}