use rudb_common::Result;
use rudb_plan::{Node, NodeRef, Plan};
use crate::pass::{Context, Pass, top_down};
#[derive(Debug, Clone, Copy)]
pub struct LimitPushdown;
impl Pass for LimitPushdown {
fn name(&self) -> &'static str {
"limit_pushdown"
}
fn run(&self, plan: &mut Plan, _context: &Context) -> Result<()> {
push(plan);
Ok(())
}
}
pub fn push(plan: &mut Plan) {
let shared = shared(plan);
for node in top_down(plan) {
let mut at = node;
while let Some(below) = swap(plan, at, &shared) {
at = below;
}
}
}
fn swap(plan: &mut Plan, at: NodeRef, shared: &[NodeRef]) -> Option<NodeRef> {
let Node::Limit { input, count, offset } = *plan.node(at) else {
return None;
};
let Node::Project { input: under, index, exprs, names } = *plan.node(input) else {
return None;
};
if shared.contains(&input) {
return None;
}
*plan.node_mut(input) = Node::Limit { input: under, count, offset };
*plan.node_mut(at) = Node::Project { input, index, exprs, names };
Some(input)
}
fn shared(plan: &Plan) -> Vec<NodeRef> {
let mut seen = Vec::new();
let mut twice = Vec::new();
for node in top_down(plan) {
for child in plan.node(node).children().into_iter().flatten() {
if seen.contains(&child) {
twice.push(child);
} else {
seen.push(child);
}
}
}
twice
}
#[cfg(test)]
mod tests {
use rudb_plan::{JoinKind, Node, Plan, Slice};
use super::push;
fn pushed(text: &str) -> String {
let mut plan =
Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
push(&mut plan);
plan.validate().unwrap_or_else(|error| panic!("{text} did not stay valid: {error}"));
plan.to_string()
}
#[test]
fn a_limit_over_a_projection_ends_up_under_it() {
assert_eq!(
pushed(concat!(
"Limit 10 offset 0\n",
" Project #1 [#0.0::INTEGER AS a]\n",
" Get memory.main.t AS t #0 [a::INTEGER]\n",
)),
concat!(
"Project #1 [#0.0::INTEGER AS a]\n",
" Limit 10 offset 0\n",
" Get memory.main.t AS t #0 [a::INTEGER]\n",
)
);
}
#[test]
fn the_offset_and_a_limit_of_everything_come_along_too() {
assert_eq!(
pushed(concat!(
"Limit ALL offset 5\n",
" Project #1 [#0.0::INTEGER AS a]\n",
" Get memory.main.t AS t #0 [a::INTEGER]\n",
)),
concat!(
"Project #1 [#0.0::INTEGER AS a]\n",
" Limit ALL offset 5\n",
" Get memory.main.t AS t #0 [a::INTEGER]\n",
)
);
}
#[test]
fn a_limit_crosses_every_projection_above_the_one_it_started_over() {
let text = concat!(
"Limit 3 offset 0\n",
" Project #2 [#1.0::INTEGER AS a]\n",
" Project #1 [#0.0::INTEGER AS a]\n",
" Get memory.main.t AS t #0 [a::INTEGER]\n",
);
let once = pushed(text);
assert_eq!(
once,
concat!(
"Project #2 [#1.0::INTEGER AS a]\n",
" Project #1 [#0.0::INTEGER AS a]\n",
" Limit 3 offset 0\n",
" Get memory.main.t AS t #0 [a::INTEGER]\n",
)
);
assert_eq!(pushed(&once), once);
}
#[test]
fn a_limit_over_anything_that_is_not_a_projection_stays_where_it_is() {
for below in [
" Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN\n",
" Sort [#0.0::INTEGER ASC NULLS LAST]\n",
" Distinct on=[]\n",
] {
let text =
format!("Limit 10 offset 0\n{below} Get memory.main.t AS t #0 [a::INTEGER]\n");
assert_eq!(pushed(&text), text);
}
}
#[test]
fn a_projection_with_no_limit_over_it_is_left_alone() {
let text = concat!(
"Project #1 [#0.0::INTEGER AS a]\n",
" Get memory.main.t AS t #0 [a::INTEGER]\n",
);
assert_eq!(pushed(text), text);
}
#[test]
fn a_projection_something_else_is_also_reading_is_left_alone() {
let mut plan = Plan::new();
let leaf =
plan.add_node(Node::Values { index: 0, columns: Slice::EMPTY, rows: Slice::EMPTY });
let project = plan.add_node(Node::Project {
input: leaf,
index: 1,
exprs: Slice::EMPTY,
names: Slice::EMPTY,
});
let limit = plan.add_node(Node::Limit { input: project, count: Some(10), offset: 0 });
let join = plan.add_node(Node::Join {
left: limit,
right: project,
kind: JoinKind::Inner,
conditions: Slice::EMPTY,
});
plan.set_root(join);
let before = plan.to_string();
push(&mut plan);
plan.validate().expect("the plan is still well formed");
assert_eq!(plan.to_string(), before);
}
}