pub mod hash_index;
use std::rc::Rc;
use crate::ast::{
BinaryOp, Consumer, Expr, Follow, FollowDestination, OpNode, SelectItem, Subquery, Where,
};
pub use hash_index::{HashIndex, RowIndex, index_key, intersect_positions};
#[derive(Clone, Debug)]
pub struct Correlation {
pub local: Expr,
pub outer: Expr,
pub path: Vec<String>,
pub index: usize,
}
#[derive(Clone, Debug)]
pub struct BlockPlan {
pub node: Rc<OpNode>,
pub depth: usize,
pub correlated: Vec<Correlation>,
pub residual: Option<Where>,
pub receiver_stable: bool,
pub invariant: bool,
pub from_cardinality: bool,
}
#[derive(Clone, Copy, Debug)]
pub struct RuleContext {
pub binding_count: usize,
}
pub type Rule = fn(&BlockPlan, &RuleContext) -> Option<BlockPlan>;
pub fn logical_block(node: &OpNode, depth: usize) -> BlockPlan {
BlockPlan {
residual: node.sub.r#where.clone(),
node: Rc::new(node.clone()),
depth,
correlated: Vec::new(),
receiver_stable: false,
invariant: false,
from_cardinality: false,
}
}
pub fn conjunction(mut parts: Vec<Where>) -> Option<Where> {
match parts.len() {
0 => None,
1 => parts.pop(),
_ => Some(Where::And { parts }),
}
}
pub fn conjuncts(w: &Where) -> Vec<&Where> {
match w {
Where::And { parts } => parts.iter().collect(),
other => vec![other],
}
}
pub fn residual_without(plan: &BlockPlan, answered: &Correlation) -> Option<Where> {
plan.node.sub.r#where.as_ref().and_then(|w| {
conjunction(
conjuncts(w)
.into_iter()
.enumerate()
.filter(|(i, _)| *i != answered.index)
.map(|(_, part)| part.clone())
.collect(),
)
})
}
pub fn lookup_order(plan: &BlockPlan) -> Vec<&Correlation> {
let (varying, literal): (Vec<&Correlation>, Vec<&Correlation>) = plan
.correlated
.iter()
.partition(|c| !matches!(c.outer, Expr::Lit(_)));
varying.into_iter().chain(literal).collect()
}
const MAX_PASSES: usize = 8;
pub fn optimize_block(node: &OpNode, depth: usize, ctx: &RuleContext, rules: &[Rule]) -> BlockPlan {
let mut plan = logical_block(node, depth);
for _ in 0..MAX_PASSES {
let mut changed = false;
for rule in rules {
if let Some(next) = rule(&plan, ctx) {
plan = next;
changed = true;
}
}
if !changed {
break;
}
}
plan
}
pub const DEFAULT_RULES: &[Rule] = &[
correlated_equality_probe,
stable_receiver,
invariant_block,
cardinality_only,
];
pub fn correlated_equality_probe(plan: &BlockPlan, ctx: &RuleContext) -> Option<BlockPlan> {
let sub = &plan.node.sub;
if !plan.correlated.is_empty() || !sub.from.is_empty() || sub.follow.is_some() {
return None;
}
let w = sub.r#where.as_ref()?;
if where_has_lifts(w) {
return None;
}
let mut correlated = Vec::new();
let mut residual = Vec::new();
let mut prefix_raise_free = true;
for (index, part) in conjuncts(w).into_iter().enumerate() {
let eq = if prefix_raise_free {
as_correlation(part, index, ctx)
} else {
None
};
match eq {
Some(c) => correlated.push(c),
None => {
residual.push(part.clone());
prefix_raise_free = prefix_raise_free && where_raise_free(part, ctx);
}
}
}
if correlated.is_empty() {
return None;
}
Some(BlockPlan {
correlated,
residual: conjunction(residual),
..plan.clone()
})
}
fn as_correlation(w: &Where, index: usize, ctx: &RuleContext) -> Option<Correlation> {
let Where::Scalar {
expr:
Expr::Binary {
op: BinaryOp::Eq,
left,
right,
},
} = w
else {
return None;
};
pair(left, right, index, ctx).or_else(|| pair(right, left, index, ctx))
}
fn pair(local: &Expr, outer: &Expr, index: usize, ctx: &RuleContext) -> Option<Correlation> {
let path = local_path(local)?;
if expr_reads_current_scope(outer) || !expr_raise_free(outer, ctx) {
return None;
}
Some(Correlation {
local: local.clone(),
outer: outer.clone(),
path,
index,
})
}
pub fn stable_receiver(plan: &BlockPlan, _ctx: &RuleContext) -> Option<BlockPlan> {
if plan.receiver_stable || expr_reads_scope_in(&plan.node.receiver, plan.depth, plan.depth) {
return None;
}
Some(BlockPlan {
receiver_stable: true,
..plan.clone()
})
}
pub fn invariant_block(plan: &BlockPlan, _ctx: &RuleContext) -> Option<BlockPlan> {
if plan.invariant
|| op_has_lifts(&plan.node)
|| op_reads_scope_in(&plan.node, plan.depth, plan.depth)
{
return None;
}
Some(BlockPlan {
invariant: true,
..plan.clone()
})
}
pub fn cardinality_only(plan: &BlockPlan, _ctx: &RuleContext) -> Option<BlockPlan> {
let node = &plan.node;
if plan.from_cardinality
|| !matches!(node.op, Consumer::Exists | Consumer::None | Consumer::Count)
|| node.distinct
|| node.sub.order_by.is_some()
|| node.sub.follow.is_some()
|| (node.sub.r#where.is_some() && plan.residual.is_some())
{
return None;
}
Some(BlockPlan {
from_cardinality: true,
..plan.clone()
})
}
pub fn expr_reads_scope_in(e: &Expr, at: usize, up_to: usize) -> bool {
match e {
Expr::Lit(_) | Expr::Binding { .. } => false,
Expr::Ident { .. } => at >= 1 && at <= up_to,
Expr::Outer { levels, .. } => {
let d = at.saturating_sub(*levels);
*levels <= at && d >= 1 && d <= up_to
}
Expr::Member { recv, .. } => expr_reads_scope_in(recv, at, up_to),
Expr::Index { recv, index } => {
expr_reads_scope_in(recv, at, up_to) || expr_reads_scope_in(index, at, up_to)
}
Expr::Call { recv, args, .. } => {
recv.as_deref()
.is_some_and(|r| expr_reads_scope_in(r, at, up_to))
|| args.iter().any(|a| expr_reads_scope_in(a, at, up_to))
}
Expr::Unary { expr, .. } => expr_reads_scope_in(expr, at, up_to),
Expr::Binary { left, right, .. }
| Expr::Logical { left, right, .. }
| Expr::In { left, right } => {
expr_reads_scope_in(left, at, up_to) || expr_reads_scope_in(right, at, up_to)
}
Expr::Range { lo, hi, .. } => {
lo.as_deref()
.is_some_and(|x| expr_reads_scope_in(x, at, up_to))
|| hi
.as_deref()
.is_some_and(|x| expr_reads_scope_in(x, at, up_to))
}
}
}
pub fn where_reads_scope_in(w: &Where, at: usize, up_to: usize) -> bool {
match w {
Where::And { parts } | Where::Or { parts } => {
parts.iter().any(|p| where_reads_scope_in(p, at, up_to))
}
Where::Not { expr } => where_reads_scope_in(expr, at, up_to),
Where::Scalar { expr } => expr_reads_scope_in(expr, at, up_to),
Where::Op(op) => op_reads_scope_in(op, at, up_to),
}
}
pub fn op_reads_scope_in(op: &OpNode, at: usize, up_to: usize) -> bool {
expr_reads_scope_in(&op.receiver, at, up_to) || sub_reads_scope_in(&op.sub, at + 1, up_to)
}
fn sub_reads_scope_in(sub: &Subquery, at: usize, up_to: usize) -> bool {
sub.from.iter().any(|e| expr_reads_scope_in(e, at, up_to))
|| sub
.r#where
.as_ref()
.is_some_and(|w| where_reads_scope_in(w, at, up_to))
|| sub.select.iter().any(|item| match item {
SelectItem::Field { expr, .. } => expr_reads_scope_in(expr, at, up_to),
SelectItem::Collect { op, .. } => op_reads_scope_in(op, at, up_to),
})
|| sub.order_by.as_ref().is_some_and(|specs| {
specs
.iter()
.any(|o| expr_reads_scope_in(&o.expr, at, up_to))
})
|| sub
.limit
.as_ref()
.is_some_and(|e| expr_reads_scope_in(e, at, up_to))
|| sub
.offset
.as_ref()
.is_some_and(|e| expr_reads_scope_in(e, at, up_to))
|| sub
.follow
.as_ref()
.is_some_and(|f| follow_reads_scope_in(f, at, up_to))
}
fn follow_reads_scope_in(f: &Follow, at: usize, up_to: usize) -> bool {
f.destinations.iter().any(|d| match d {
FollowDestination::Relation(e) => expr_reads_scope_in(e, at, up_to),
FollowDestination::Block(op) => op_reads_scope_in(op, at, up_to),
}) || f
.r#where
.as_ref()
.is_some_and(|e| expr_reads_scope_in(e, at + 1, up_to))
|| f.frontier
.as_ref()
.is_some_and(|e| expr_reads_scope_in(e, at, up_to))
|| f.by
.as_ref()
.is_some_and(|e| expr_reads_scope_in(e, at, up_to))
}
pub fn expr_reads_current_scope(e: &Expr) -> bool {
expr_reads_scope_in(e, 1, 1)
}
pub fn expr_raise_free(e: &Expr, ctx: &RuleContext) -> bool {
match e {
Expr::Lit(_) | Expr::Ident { .. } | Expr::Outer { .. } => true,
Expr::Binding { index } => *index < ctx.binding_count,
Expr::Call { .. } => false,
Expr::Member { recv, .. } => expr_raise_free(recv, ctx),
Expr::Index { recv, index } => expr_raise_free(recv, ctx) && expr_raise_free(index, ctx),
Expr::Unary { expr, .. } => expr_raise_free(expr, ctx),
Expr::Binary { left, right, .. }
| Expr::Logical { left, right, .. }
| Expr::In { left, right } => expr_raise_free(left, ctx) && expr_raise_free(right, ctx),
Expr::Range { lo, hi, .. } => {
lo.as_deref().is_none_or(|x| expr_raise_free(x, ctx))
&& hi.as_deref().is_none_or(|x| expr_raise_free(x, ctx))
}
}
}
pub fn where_raise_free(w: &Where, ctx: &RuleContext) -> bool {
match w {
Where::And { parts } | Where::Or { parts } => {
parts.iter().all(|p| where_raise_free(p, ctx))
}
Where::Not { expr } => where_raise_free(expr, ctx),
Where::Scalar { expr } => expr_raise_free(expr, ctx),
Where::Op(op) => op_raise_free(op, ctx),
}
}
pub fn op_raise_free(op: &OpNode, ctx: &RuleContext) -> bool {
let sub = &op.sub;
matches!(op.op, Consumer::Exists | Consumer::None | Consumer::Count)
&& sub.follow.is_none()
&& sub.limit.is_none()
&& sub.offset.is_none()
&& !op.distinct
&& expr_raise_free(&op.receiver, ctx)
&& sub.from.iter().all(|e| expr_raise_free(e, ctx))
&& sub
.r#where
.as_ref()
.is_none_or(|w| where_raise_free(w, ctx))
&& sub
.order_by
.as_ref()
.is_none_or(|specs| specs.iter().all(|o| expr_raise_free(&o.expr, ctx)))
&& !op_has_lifts(op)
}
pub fn op_has_lifts(op: &OpNode) -> bool {
sub_has_lifts(&op.sub)
}
fn sub_has_lifts(sub: &Subquery) -> bool {
sub.select.iter().any(|item| match item {
SelectItem::Field { lift, .. } => *lift > 0,
SelectItem::Collect { op, .. } => op_has_lifts(op),
}) || sub.r#where.as_ref().is_some_and(where_has_lifts)
|| sub.follow.as_ref().is_some_and(|f| {
f.destinations
.iter()
.any(|d| matches!(d, FollowDestination::Block(op) if op_has_lifts(op)))
})
}
pub fn where_has_lifts(w: &Where) -> bool {
match w {
Where::And { parts } | Where::Or { parts } => parts.iter().any(where_has_lifts),
Where::Not { expr } => where_has_lifts(expr),
Where::Scalar { .. } => false,
Where::Op(op) => op_has_lifts(op),
}
}
pub const SCOPE_INTRINSICS: &[&str] =
&["$key", "$depth", "$stop", "$leaf", "$frontier", "$ordinal"];
pub fn local_path(e: &Expr) -> Option<Vec<String>> {
match e {
Expr::Ident { name } if name == "$value" => Some(Vec::new()),
Expr::Ident { name } if SCOPE_INTRINSICS.contains(&name.as_str()) => None,
Expr::Ident { name } => Some(vec![name.clone()]),
Expr::Member { recv, name } => {
let mut path = local_path(recv)?;
path.push(name.clone());
Some(path)
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse_string;
fn block(src: &str) -> OpNode {
let q = parse_string(src).expect("parses");
match q.select.into_iter().next() {
Some(SelectItem::Collect { op, .. }) => *op,
_ => match q.r#where {
Some(Where::Op(op)) => *op,
other => panic!("no block in {src}: {other:?}"),
},
}
}
fn plan(src: &str) -> BlockPlan {
optimize_block(
&block(src),
1,
&RuleContext { binding_count: 0 },
DEFAULT_RULES,
)
}
#[test]
fn hoists_a_correlated_equality_and_keeps_the_residual_in_order() {
let p = plan("select c: ^cs first { where total > 1 && id == ^cid && !closed } from os");
assert_eq!(p.correlated.len(), 1);
assert_eq!(p.correlated[0].path, ["id"]);
assert!(p.receiver_stable);
assert!(!p.invariant, "reads ^cid");
match p.residual {
Some(Where::And { parts }) => assert_eq!(parts.len(), 2),
other => panic!("residual should keep two conjuncts: {other:?}"),
}
}
#[test]
fn a_raising_conjunct_left_of_the_equality_blocks_it() {
let p = plan("select c: ^cs first { where f(x) && id == ^cid } from os");
assert!(p.correlated.is_empty());
let p = plan("select c: ^cs first { where ^^ys exists { limit 1 } && id == ^cid } from os");
assert!(p.correlated.is_empty(), "a bound can raise");
let p = plan("select c: ^cs first { where id == ^cid && f(x) } from os");
assert_eq!(p.correlated.len(), 1, "right of the equality is fine");
}
#[test]
fn the_outer_side_must_not_read_the_row_nor_raise() {
assert!(
plan("select c: ^cs first { where a == b } from os")
.correlated
.is_empty()
);
assert!(
plan("select c: ^cs first { where id == f(^cid) } from os")
.correlated
.is_empty()
);
assert_eq!(
plan("select c: ^cs first { where ^cid == meta.id } from os").correlated[0].path,
["meta", "id"]
);
assert!(
plan("select c: ^cs first { where $key == ^cid } from os")
.correlated
.is_empty()
);
assert_eq!(
plan("select c: ^cs first { where $value == ^cid } from os").correlated[0].path,
Vec::<String>::new()
);
}
#[test]
fn lifts_and_from_and_per_row_receivers_are_respected() {
assert!(
plan("select c: ^cs first { where ^^zs collect { ^^^t: x } && id == ^cid } from os")
.correlated
.is_empty()
);
assert!(
plan("select c: ^cs first { from lines where id == ^cid } from os")
.correlated
.is_empty()
);
let p = plan("select c: lines first { where id == ^cid } from os");
assert_eq!(p.correlated.len(), 1);
assert!(!p.receiver_stable, "`lines` is the enclosing row's");
}
#[test]
fn invariant_and_cardinality_annotations() {
let p = plan("select c: ^cs collect { name values } from os");
assert!(p.invariant && p.receiver_stable);
assert!(
!plan("from os where ^cs collect { ^n: name }").invariant,
"lifts"
);
let p = plan("from os where ^cs exists { where id == ^cid }");
assert!(p.from_cardinality && p.residual.is_none());
assert!(!plan("from os where ^cs exists { where id == ^cid && !closed }").from_cardinality);
assert!(plan("from os where ^cs count { } > 2").from_cardinality);
assert!(!plan("from os where ^cs count distinct { name } > 2").from_cardinality);
}
}