use rudb_common::{LogicalType, Result};
use rudb_plan::{ConjunctionOp, Expr, ExprRef, JoinKind, Node, NodeRef, Plan};
use crate::pass::{Context, Pass};
use crate::tables::{TableSet, Tables, produced};
use crate::{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| substitute(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) = partition(plan, pending, index, &keys);
let moved = down.into_iter().map(|part| substitute(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::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::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 } => {
let below = (produced(plan, left), produced(plan, right));
let kind = nulls::narrow(plan, written, &pending, (&below.0, &below.1));
let held = plan.expr_list(conditions).to_vec();
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 { (over, Vec::new()) } 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() {
conditions
} else {
let all: Vec<ExprRef> =
plan.expr_list(conditions).to_vec().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,
})
};
filter(plan, above, stay)
}
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 rebuilt_left = node(plan, left, to_left, tables);
let rebuilt_right = node(plan, right, to_right, tables);
let above = if rebuilt_left == left && rebuilt_right == right {
at
} else {
plan.add_node(Node::CrossProduct { left: rebuilt_left, right: rebuilt_right })
};
filter(plan, above, over)
}
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::Get { .. } | Node::Values { .. } | Node::TableFunction { .. } | Node::Dummy => {
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 => (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 filter(plan: &mut Plan, input: NodeRef, parts: Vec<ExprRef>) -> NodeRef {
let parts: Vec<ExprRef> = parts.into_iter().filter(|&part| !always(plan, part)).collect();
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 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),
None => false,
};
});
answer
}
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_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 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_does_not_become_a_join_while_the_join_is_the_slower_operator() {
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 = "\
Filter (#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN
CrossProduct
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_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);
}
}