use inillucent_value::Collation;
use crate::ast::{BinaryOp, CompoundOp, JoinKind, NullOrder, SortOrder};
use crate::bind::{BoundExpr, BoundSelect, BoundSource, ColumnUse, SourceRows};
use crate::catalog_view::{IndexInfo, TableInfo};
use crate::cost;
mod hint;
mod partial;
mod pattern;
mod range;
mod terms;
pub use hint::unanswerable_index_hint;
use hint::{forced_path, index_usable, outer_terms, statement_terms};
use partial::implies;
use terms::{
collation_of, compares_unconverted, comparison_against_column, comparison_against_rowid,
comparison_collation, indexable_comparison,
};
mod seek_union;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BoundKind {
GreaterEqual,
Greater,
LessEqual,
Less,
}
#[derive(Clone, Debug, PartialEq)]
pub struct RangeBound {
pub kind: BoundKind,
pub value: BoundExpr,
pub unconverted: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct IndexSeekBranch {
pub equalities: Vec<BoundExpr>,
pub unconverted: Vec<usize>,
pub low: Option<RangeBound>,
pub high: Option<RangeBound>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum AccessPath {
TableScan {
root: u32,
},
RowidSeek {
root: u32,
key: BoundExpr,
},
RowidRange {
root: u32,
low: Option<RangeBound>,
high: Option<RangeBound>,
},
IndexSeek {
table_root: u32,
index_root: u32,
index_name: Vec<u8>,
equalities: Vec<BoundExpr>,
unconverted: Vec<usize>,
low: Option<RangeBound>,
high: Option<RangeBound>,
collations: Vec<Collation>,
descending: Vec<bool>,
columns: Vec<Option<u16>>,
without_rowid: bool,
key_entry_slots: Vec<usize>,
covering: Option<Vec<(u16, usize)>>,
},
RowidSeekUnion {
root: u32,
keys: Vec<BoundExpr>,
},
IndexSeekUnion {
table_root: u32,
index_root: u32,
index_name: Vec<u8>,
branches: Vec<IndexSeekBranch>,
collations: Vec<Collation>,
descending: Vec<bool>,
columns: Vec<Option<u16>>,
without_rowid: bool,
key_entry_slots: Vec<usize>,
covering: Option<Vec<(u16, usize)>>,
dedup: bool,
},
Subquery {
plan: Box<PhysicalPlan>,
width: usize,
correlated: bool,
},
Recursive {
seeds: Vec<(CompoundOp, PhysicalPlan)>,
steps: Vec<(CompoundOp, PhysicalPlan)>,
width: usize,
},
RecursiveSelf {
cte: usize,
},
VectorProbe {
root: u32,
index: Vec<u8>,
probe: Box<BoundExpr>,
depth: usize,
},
VirtualScan {
module: crate::vtab::ModuleRef,
offer: Vec<VirtualConstraint>,
order_by: Vec<crate::vtab::OrderSpec>,
chosen: Option<VirtualChoice>,
},
}
#[derive(Clone, Debug, PartialEq)]
pub struct VirtualChoice {
pub index_number: i32,
pub index_string: String,
pub arguments: Vec<usize>,
pub recheck: Vec<usize>,
pub ordered: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct VirtualConstraint {
pub spec: crate::vtab::ConstraintSpec,
pub value: BoundExpr,
pub predicate: BoundExpr,
}
impl AccessPath {
pub fn describe(&self, table: &str) -> String {
self.describe_over(table, None)
}
pub fn describe_over(&self, table: &str, info: Option<&TableInfo>) -> String {
match self {
AccessPath::TableScan { .. } => format!("SCAN {table}"),
AccessPath::RowidSeek { .. } => {
format!("SEARCH {table} USING INTEGER PRIMARY KEY (rowid=?)")
}
AccessPath::RowidRange { .. } => {
format!("SEARCH {table} USING INTEGER PRIMARY KEY (rowid>?)")
}
AccessPath::RowidSeekUnion { .. } => {
format!("SEARCH {table} USING INTEGER PRIMARY KEY (rowid=?)")
}
AccessPath::Recursive { .. } => format!("SCAN {table} USING RECURSIVE QUEUE"),
AccessPath::RecursiveSelf { .. } => format!("SCAN {table}"),
AccessPath::VectorProbe { index, depth, .. } => format!(
"SEARCH {table} USING VECTOR INDEX {} (k={depth})",
String::from_utf8_lossy(index)
),
AccessPath::VirtualScan { .. } => format!("SCAN {table} VIRTUAL TABLE INDEX"),
AccessPath::Subquery { correlated, .. } => {
if *correlated {
format!("CORRELATED SCALAR SUBQUERY {table}")
} else {
format!("SCAN {table}")
}
}
AccessPath::IndexSeek {
index_name,
equalities,
low,
high,
covering,
..
} => {
let kind = if covering.is_some() {
"COVERING INDEX"
} else {
"INDEX"
};
if equalities.is_empty() && low.is_none() && high.is_none() {
return format!(
"SCAN {table} USING {kind} {}",
String::from_utf8_lossy(index_name)
);
}
let detail = index_seek_detail(
index_name,
info,
equalities.len(),
low.is_some() || high.is_some(),
);
format!(
"SEARCH {table} USING {kind} {} ({detail})",
String::from_utf8_lossy(index_name)
)
}
AccessPath::IndexSeekUnion {
index_name,
branches,
covering,
..
} => {
let kind = if covering.is_some() {
"COVERING INDEX"
} else {
"INDEX"
};
let mut lines: Vec<String> = Vec::new();
for branch in branches {
let detail = index_seek_detail(
index_name,
info,
branch.equalities.len(),
branch.low.is_some() || branch.high.is_some(),
);
let line = format!(
"SEARCH {table} USING {kind} {} ({detail})",
String::from_utf8_lossy(index_name)
);
if !lines.contains(&line) {
lines.push(line);
}
}
lines.join(" OR ")
}
}
}
}
fn index_seek_detail(
index_name: &[u8],
info: Option<&TableInfo>,
equalities: usize,
ranged: bool,
) -> String {
let keyed = info.and_then(|held| {
held.indexes
.iter()
.find(|candidate| candidate.name == index_name)
});
let named = |position: usize| -> String {
keyed
.and_then(|index| index.columns.get(position))
.and_then(|key| key.column)
.and_then(|at| info.and_then(|held| held.column(at)))
.map(|column| String::from_utf8_lossy(&column.name).into_owned())
.unwrap_or_else(|| "?".to_string())
};
let mut detail = String::new();
for index in 0..equalities {
if index > 0 {
detail.push_str(" AND ");
}
detail.push_str(&format!("{}=?", named(index)));
}
if ranged {
if !detail.is_empty() {
detail.push_str(" AND ");
}
detail.push_str(&format!("{}>?", named(equalities)));
}
detail
}
#[derive(Clone, Debug, PartialEq)]
pub struct PlannedSource {
pub cost: f64,
pub rows: f64,
pub id: usize,
pub table: TableInfo,
pub alias: Vec<u8>,
pub path: AccessPath,
pub join: JoinKind,
pub on: Option<BoundExpr>,
pub on_enforced: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AggregationMode {
None,
Whole,
Grouped,
}
#[derive(Clone, Debug, PartialEq)]
pub struct PhysicalPlan {
pub sources: Vec<PlannedSource>,
pub residuals: Vec<Option<BoundExpr>>,
pub constant_filter: Option<BoundExpr>,
pub select: BoundSelect,
pub aggregation: AggregationMode,
pub needs_sort: bool,
pub reverse: bool,
pub grouped_walk: bool,
pub distinct_walk: bool,
pub compounds: Vec<(CompoundOp, PhysicalPlan)>,
pub levers: Levers,
pub subqueries: bool,
}
impl PhysicalPlan {
pub fn max_source_id(&self) -> usize {
let mut highest = 0usize;
for source in &self.sources {
highest = highest.max(source.id);
match &source.path {
AccessPath::Subquery { plan, .. } => {
highest = highest.max(plan.max_source_id());
}
AccessPath::Recursive { seeds, steps, .. } => {
for (_, arm) in seeds.iter().chain(steps.iter()) {
highest = highest.max(arm.max_source_id());
}
}
_ => {}
}
}
for (_, arm) in &self.compounds {
highest = highest.max(arm.max_source_id());
}
highest
}
pub fn describe(&self) -> Vec<String> {
let mut lines = Vec::new();
for source in &self.sources {
lines.push(
source
.path
.describe_over(&String::from_utf8_lossy(&source.alias), Some(&source.table)),
);
}
for (op, arm) in &self.compounds {
lines.push(format!("COMPOUND QUERY {}", compound_name(*op)));
lines.extend(arm.describe());
}
if self.aggregation == AggregationMode::Grouped && !self.grouped_walk {
lines.push("USE TEMP B-TREE FOR GROUP BY".to_string());
}
if self.needs_sort {
lines.push("USE TEMP B-TREE FOR ORDER BY".to_string());
}
if self.select.distinct && !self.distinct_walk {
lines.push("USE TEMP B-TREE FOR DISTINCT".to_string());
}
lines
}
}
fn compound_name(op: CompoundOp) -> &'static str {
match op {
CompoundOp::Union => "UNION",
CompoundOp::UnionAll => "UNION ALL",
CompoundOp::Intersect => "INTERSECT",
CompoundOp::Except => "EXCEPT",
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct Levers {
disabled: u32,
}
impl Levers {
pub const COVERING_INDEX: u32 = 1;
pub const INDEXED_WRITE: u32 = 2;
pub const ORDERED_WALK: u32 = 4;
pub const STREAMING_GROUP: u32 = 8;
pub const FUSED_BYTECODE: u32 = 16;
pub const PLAN_CACHE: u32 = 32;
pub const AUTOMATIC_INDEX: u32 = 64;
pub const EVERY: u32 = Levers::PLAN_CACHE
| Levers::COVERING_INDEX
| Levers::INDEXED_WRITE
| Levers::ORDERED_WALK
| Levers::STREAMING_GROUP
| Levers::FUSED_BYTECODE
| Levers::AUTOMATIC_INDEX;
pub fn all() -> Levers {
Levers { disabled: 0 }
}
pub fn without(mask: u32) -> Levers {
Levers {
disabled: mask & Levers::EVERY,
}
}
pub fn has(self, lever: u32) -> bool {
self.disabled & lever == 0
}
pub fn disabled(self) -> u32 {
self.disabled
}
pub fn names_disabled(self) -> Vec<&'static str> {
let mut names = Vec::new();
if !self.has(Levers::COVERING_INDEX) {
names.push("covering-index");
}
if !self.has(Levers::INDEXED_WRITE) {
names.push("indexed-write");
}
if !self.has(Levers::ORDERED_WALK) {
names.push("ordered-walk");
}
if !self.has(Levers::STREAMING_GROUP) {
names.push("streaming-group");
}
if !self.has(Levers::FUSED_BYTECODE) {
names.push("fused-bytecode");
}
names
}
}
pub fn plan_select_with(select: BoundSelect, levers: Levers) -> PhysicalPlan {
let mut select = select;
let compound_arms = core::mem::take(&mut select.compounds);
let terms = statement_terms(&select);
let order = choose_order(&select, &terms, levers);
let ordered: Vec<usize> = order.clone();
let ids: Vec<usize> = ordered
.iter()
.filter_map(|position| select.sources.get(*position))
.map(|source| source.id)
.collect();
let mut consumed = vec![false; terms.len()];
let mut sources = Vec::with_capacity(select.sources.len());
for (level, position) in ordered.iter().enumerate() {
let Some(source) = select.sources.get(*position) else {
continue;
};
let mut on_enforced = false;
let path = if is_outer(source.join) && matches!(source.rows, SourceRows::Table) {
match source.table.module.clone() {
Some(_) => choose_path(level, &ids, source, &select, &terms, &mut consumed, levers),
None => {
let on_terms = if source.join == JoinKind::Left {
outer_terms(source)
} else {
Vec::new()
};
let mut on_consumed = vec![false; on_terms.len()];
let chosen = choose_path(
level,
&ids,
source,
&select,
&on_terms,
&mut on_consumed,
levers,
);
on_enforced = !on_terms.is_empty() && on_consumed.iter().all(|held| *held);
chosen
}
}
} else {
choose_path(level, &ids, source, &select, &terms, &mut consumed, levers)
};
let (cost, rows) = path_cost(source, &path);
sources.push(PlannedSource {
cost,
rows,
id: source.id,
table: (*source.table).clone(),
alias: source.alias.clone(),
path,
join: source.join,
on: is_outer(source.join)
.then(|| source.constraint.clone())
.flatten(),
on_enforced,
});
}
let (residuals, constant_filter) = distribute_residuals(&terms, &consumed, &ids);
let aggregation = if !select.group_by.is_empty() {
AggregationMode::Grouped
} else if !select.aggregates.is_empty() {
AggregationMode::Whole
} else {
AggregationMode::None
};
let adjacent = levers.has(Levers::STREAMING_GROUP)
&& sources.len() == 1
&& select.windows.is_empty()
&& select.compounds.is_empty();
let outer = sources.first();
let grouped_walk = adjacent
&& aggregation == AggregationMode::Grouped
&& outer.is_some_and(|outer| grouped_by_walk(&select, outer));
let distinct_walk = adjacent && outer.is_some_and(|outer| distinct_by_walk(&select, outer));
let streamed_in_order = (grouped_walk && !select.distinct)
|| (distinct_walk && aggregation == AggregationMode::None);
let single = levers.has(Levers::ORDERED_WALK)
&& sources.len() == 1
&& select.windows.is_empty()
&& select.compounds.is_empty()
&& ((aggregation == AggregationMode::None && !select.distinct) || streamed_in_order);
let provided = if single {
sources
.first()
.and_then(|outer| ordering_provided(&select, outer.id, &outer.table, &outer.path))
} else {
None
};
let needs_sort = !select.order_by.is_empty() && provided.is_none();
let reverse = provided.unwrap_or(false);
let compounds: Vec<(CompoundOp, PhysicalPlan)> = compound_arms
.into_iter()
.map(|(op, arm)| (op, plan_select_with(arm, levers)))
.collect();
let subqueries = holds_subquery(&select)
|| residuals.iter().flatten().any(expression_holds_subquery)
|| constant_filter
.as_ref()
.is_some_and(expression_holds_subquery)
|| compounds.iter().any(|(_op, arm)| arm.subqueries);
PhysicalPlan {
sources,
residuals,
constant_filter,
select,
aggregation,
needs_sort,
reverse,
grouped_walk,
distinct_walk,
compounds,
subqueries,
levers,
}
}
fn holds_subquery(select: &BoundSelect) -> bool {
select.filter.iter().any(expression_holds_subquery)
|| select.group_by.iter().any(expression_holds_subquery)
|| select.having.iter().any(expression_holds_subquery)
|| select
.columns
.iter()
.any(|column| expression_holds_subquery(&column.expr))
|| select
.order_by
.iter()
.any(|term| expression_holds_subquery(&term.expr))
|| select.limit.iter().any(expression_holds_subquery)
|| select.offset.iter().any(expression_holds_subquery)
|| select
.values
.iter()
.flatten()
.any(expression_holds_subquery)
|| select.aggregates.iter().any(|aggregate| {
aggregate.arguments.iter().any(expression_holds_subquery)
|| aggregate.filter.iter().any(expression_holds_subquery)
|| aggregate
.order_by
.iter()
.any(|term| expression_holds_subquery(&term.expr))
})
|| select.windows.iter().any(|window| {
window.arguments.iter().any(expression_holds_subquery)
|| window.filter.iter().any(expression_holds_subquery)
|| window.partition_by.iter().any(expression_holds_subquery)
|| window
.order_by
.iter()
.any(|term| expression_holds_subquery(&term.expr))
})
|| select.sources.iter().any(|source| {
source.constraint.iter().any(expression_holds_subquery)
|| matches!(&source.rows, SourceRows::Subquery(block) if holds_subquery(block))
})
}
pub fn expression_holds_subquery(expr: &BoundExpr) -> bool {
matches!(expr, BoundExpr::Subquery { .. })
|| expr
.children()
.iter()
.any(|child| expression_holds_subquery(child))
}
fn grouped_by_walk(select: &BoundSelect, outer: &PlannedSource) -> bool {
if select.group_by.is_empty() {
return false;
}
let Some(key) = path_ordering(&outer.table, &outer.path) else {
return false;
};
let mut wanted: Vec<(OrderedBy, Collation)> = Vec::new();
for expr in &select.group_by {
let Some(named) = walk_key_of(expr, outer.id, &outer.table) else {
return false;
};
let collation = crate::bind::result_collation(expr);
if !wanted.iter().any(|(held, _)| *held == named) {
wanted.push((named, collation));
}
}
covers_prefix(&key, &wanted)
}
fn distinct_by_walk(select: &BoundSelect, outer: &PlannedSource) -> bool {
if !select.distinct || !select.group_by.is_empty() || !select.aggregates.is_empty() {
return false;
}
let Some(key) = path_ordering(&outer.table, &outer.path) else {
return false;
};
let mut wanted: Vec<(OrderedBy, Collation)> = Vec::new();
for column in &select.columns {
let Some(named) = walk_key_of(&column.expr, outer.id, &outer.table) else {
return false;
};
let collation = crate::bind::result_collation(&column.expr);
if !wanted.iter().any(|(held, _)| *held == named) {
wanted.push((named, collation));
}
}
covers_prefix(&key, &wanted)
}
fn covers_prefix(key: &PathOrdering, wanted: &[(OrderedBy, Collation)]) -> bool {
let free: Vec<&(OrderedBy, Collation)> = wanted
.iter()
.filter(|(named, _)| !key.pinned.contains(named))
.collect();
if free.len() > key.columns.len() {
return false;
}
let prefix = match key.columns.get(..free.len()) {
Some(prefix) => prefix,
None => return false,
};
free.iter().all(|(named, collation)| {
prefix
.iter()
.any(|(held, _, held_collation)| held == named && held_collation == collation)
})
}
fn walk_key_of(expr: &BoundExpr, id: usize, table: &TableInfo) -> Option<OrderedBy> {
let mut expr = expr;
while let BoundExpr::Collate { operand, .. } = expr {
expr = operand;
}
match expr {
BoundExpr::Column { source, column, .. } if *source == id => {
Some(named_key(table, OrderedBy::Column(*column)))
}
BoundExpr::Rowid { source } if *source == id => Some(OrderedBy::Rowid),
_ => None,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum OrderedBy {
Column(u16),
Rowid,
}
struct PathOrdering {
columns: Vec<(OrderedBy, bool, Collation)>,
pinned: Vec<OrderedBy>,
}
fn ordering_provided(
select: &BoundSelect,
id: usize,
table: &TableInfo,
path: &AccessPath,
) -> Option<bool> {
if select.order_by.is_empty() {
return Some(false);
}
let key = path_ordering(table, path)?;
let mut reverse: Option<bool> = None;
let mut at = 0usize;
for term in &select.order_by {
let mut expr = &term.expr;
while let BoundExpr::Collate { operand, .. } = expr {
expr = operand;
}
let named = match expr {
BoundExpr::Column { source, column, .. } if *source == id => {
named_key(table, OrderedBy::Column(*column))
}
BoundExpr::Rowid { source } if *source == id => OrderedBy::Rowid,
_ => return None,
};
let descending = matches!(term.order, SortOrder::Descending);
let natural = match term.nulls {
NullOrder::First => !descending,
NullOrder::Last => descending,
};
if !natural {
return None;
}
if key.pinned.contains(&named) {
continue;
}
let (held, held_descending, held_collation) = key.columns.get(at).copied()?;
if held != named || held_collation != term.collation {
return None;
}
let walk = descending != held_descending;
match reverse {
None => reverse = Some(walk),
Some(existing) if existing == walk => {}
Some(_) => return None,
}
at = at.saturating_add(1);
}
Some(reverse.unwrap_or(false))
}
fn path_ordering(table: &TableInfo, path: &AccessPath) -> Option<PathOrdering> {
match path {
AccessPath::TableScan { .. } | AccessPath::RowidRange { .. } => Some(PathOrdering {
columns: rowid_key(table),
pinned: Vec::new(),
}),
AccessPath::RowidSeek { .. } => Some(PathOrdering {
columns: Vec::new(),
pinned: Vec::new(),
}),
AccessPath::IndexSeek {
index_name,
equalities,
..
} => {
let index = table
.indexes
.iter()
.find(|candidate| candidate.name == *index_name)?;
let mut columns: Vec<(OrderedBy, bool, Collation)> = Vec::new();
let mut pinned: Vec<OrderedBy> = Vec::new();
for (at, key_column) in index.columns.iter().enumerate() {
let Some(column) = key_column.column else {
break;
};
let named = named_key(table, OrderedBy::Column(column));
let collation = collation_of(&key_column.collation);
if at < equalities.len() {
pinned.push(named);
continue;
}
columns.push((named, key_column.descending, collation));
}
columns.push((OrderedBy::Rowid, false, Collation::Binary));
Some(PathOrdering { columns, pinned })
}
AccessPath::IndexSeekUnion {
index_name,
dedup: false,
..
} => {
let index = table
.indexes
.iter()
.find(|candidate| candidate.name == *index_name)?;
let mut columns: Vec<(OrderedBy, bool, Collation)> = Vec::new();
for key_column in &index.columns {
let Some(column) = key_column.column else {
break;
};
let named = named_key(table, OrderedBy::Column(column));
let collation = collation_of(&key_column.collation);
columns.push((named, key_column.descending, collation));
}
columns.push((OrderedBy::Rowid, false, Collation::Binary));
Some(PathOrdering {
columns,
pinned: Vec::new(),
})
}
_ => None,
}
}
fn rowid_key(table: &TableInfo) -> Vec<(OrderedBy, bool, Collation)> {
let _ = table;
vec![(OrderedBy::Rowid, false, Collation::Binary)]
}
fn named_key(table: &TableInfo, named: OrderedBy) -> OrderedBy {
match named {
OrderedBy::Column(column) if table.rowid_alias == Some(column) => OrderedBy::Rowid,
other => other,
}
}
fn choose_order(select: &BoundSelect, terms: &[BoundExpr], levers: Levers) -> Vec<usize> {
let count = select.sources.len();
if count < 2 {
return (0..count).collect();
}
let mut order = Vec::with_capacity(count);
let mut run: Vec<usize> = Vec::new();
for position in 0..count {
let pins = select
.sources
.get(position)
.is_some_and(|source| matches!(source.join, JoinKind::Cross) || is_outer(source.join));
if pins {
order.extend(best_order(select, terms, &run, levers));
run.clear();
order.push(position);
continue;
}
run.push(position);
}
order.extend(best_order(select, terms, &run, levers));
order
}
fn best_order(
select: &BoundSelect,
terms: &[BoundExpr],
run: &[usize],
levers: Levers,
) -> Vec<usize> {
if run.len() < 2 || run.len() > 8 {
return run.to_vec();
}
let mut best: Option<(f64, Vec<usize>)> = None;
let mut candidate = run.to_vec();
permute(&mut candidate, 0, &mut |order| {
let cost = order_cost(select, terms, order, levers);
let better = best
.as_ref()
.is_none_or(|(existing, _)| cost < *existing - 1e-9);
if better {
best = Some((cost, order.to_vec()));
}
});
best.map(|(_, order)| order).unwrap_or_else(|| run.to_vec())
}
fn permute(order: &mut Vec<usize>, at: usize, visit: &mut impl FnMut(&[usize])) {
if at >= order.len() {
visit(order);
return;
}
for index in at..order.len() {
order.swap(at, index);
permute(order, at.saturating_add(1), visit);
order.swap(at, index);
}
}
fn order_cost(select: &BoundSelect, terms: &[BoundExpr], order: &[usize], levers: Levers) -> f64 {
let ids: Vec<usize> = order
.iter()
.filter_map(|position| select.sources.get(*position))
.map(|source| source.id)
.collect();
let mut consumed = vec![false; terms.len()];
let mut total = 0.0f64;
let mut outer_rows = 1.0f64;
for (level, position) in order.iter().enumerate() {
let Some(source) = select.sources.get(*position) else {
continue;
};
let path = choose_path(level, &ids, source, select, terms, &mut consumed, levers);
let (cost, rows) = path_cost(source, &path);
total += outer_rows * cost;
outer_rows *= rows.max(1.0);
}
total
}
fn vector_path(
id: usize,
position: usize,
source: &BoundSource,
select: &BoundSelect,
) -> Option<AccessPath> {
if position != 0 || select.sources.len() != 1 {
return None;
}
if select.distinct
|| !select.group_by.is_empty()
|| !select.aggregates.is_empty()
|| select.offset.is_some()
|| !select.compounds.is_empty()
{
return None;
}
let [term] = select.order_by.as_slice() else {
return None;
};
if term.order != crate::ast::SortOrder::Ascending {
return None;
}
let Some(BoundExpr::Integer(depth)) = select.limit.as_ref() else {
return None;
};
let depth = usize::try_from(*depth).ok().filter(|held| *held > 0)?;
let BoundExpr::Function {
func, arguments, ..
} = &term.expr
else {
return None;
};
let wanted = match func {
crate::function::ScalarFunc::VectorDistanceCos => crate::catalog_view::IndexMetric::Cosine,
crate::function::ScalarFunc::VectorDistanceL2 => crate::catalog_view::IndexMetric::L2,
_ => return None,
};
let [BoundExpr::Column {
source: held,
column,
..
}, probe] = arguments.as_slice()
else {
return None;
};
if *held != id || reads_a_column(probe) {
return None;
}
let index = source.table.indexes.iter().find(|held| {
held.origin == crate::catalog_view::IndexOrigin::Module
&& held.metric == Some(wanted)
&& held
.columns
.first()
.is_some_and(|first| first.column == Some(*column))
})?;
Some(AccessPath::VectorProbe {
root: source.table.root,
index: index.name.clone(),
probe: Box::new(probe.clone()),
depth,
})
}
fn reads_a_column(expr: &BoundExpr) -> bool {
if matches!(
expr,
BoundExpr::Column { .. } | BoundExpr::Rowid { .. } | BoundExpr::VirtualFunction { .. }
) {
return true;
}
expr.children().into_iter().any(reads_a_column)
}
fn path_cost(source: &BoundSource, path: &AccessPath) -> (f64, f64) {
let rows = estimated_rows(&source.table);
match path {
AccessPath::TableScan { .. } => (cost::scan_cost(rows), rows),
AccessPath::VirtualScan { offer, .. } => {
let usable = offer.iter().filter(|item| item.spec.usable).count();
let rows = if usable == 0 {
rows
} else {
rows / (usable as f64 * 8.0)
};
(cost::scan_cost(rows.max(1.0)), rows.max(1.0))
}
AccessPath::VectorProbe { depth, .. } => {
let matches = (*depth as f64).min(rows).max(1.0);
(cost::search_cost(rows, matches, true), matches)
}
AccessPath::RowidSeek { .. } => (cost::search_cost(rows, 1.0, true), 1.0),
AccessPath::RowidSeekUnion { keys, .. } => {
let branches = keys.len().max(1) as f64;
(cost::search_cost(rows, 1.0, true) * branches, branches)
}
AccessPath::RowidRange { low, high, .. } => {
let bounds = usize::from(low.is_some()) + usize::from(high.is_some());
let mut matches = rows;
for _ in 0..bounds {
matches /= cost::RANGE_SHARE;
}
let matches = matches.max(1.0);
(cost::search_cost(rows, matches, true), matches)
}
AccessPath::IndexSeek {
index_name,
equalities,
low,
high,
covering,
..
} => {
let bounds = usize::from(low.is_some()) + usize::from(high.is_some());
index_seek_cost(
source,
rows,
index_name,
equalities.len(),
bounds,
covering.is_some(),
)
}
AccessPath::IndexSeekUnion {
index_name,
branches,
covering,
..
} => {
let mut total_cost = 0.0f64;
let mut total_matches = 0.0f64;
for branch in branches {
let bounds = usize::from(branch.low.is_some()) + usize::from(branch.high.is_some());
let (branch_cost, branch_matches) = index_seek_cost(
source,
rows,
index_name,
branch.equalities.len(),
bounds,
covering.is_some(),
);
total_cost += branch_cost;
total_matches += branch_matches;
}
(total_cost, total_matches.max(1.0))
}
AccessPath::Subquery { .. } | AccessPath::Recursive { .. } => (cost::scan_cost(rows), rows),
AccessPath::RecursiveSelf { .. } => (1.0, 1.0),
}
}
fn index_seek_cost(
source: &BoundSource,
rows: f64,
index_name: &[u8],
equalities: usize,
bounds: usize,
covering: bool,
) -> (f64, f64) {
let index = source
.table
.indexes
.iter()
.find(|candidate| candidate.name == index_name);
let matches = index_matches(index, rows, equalities, bounds);
let Some(index) = index else {
return (cost::search_cost(rows, matches, false), matches);
};
if !covering {
return (cost::search_cost(rows, matches, false), matches);
}
let width = cost::entry_share(index.columns.len(), source.table.columns.len());
(cost::search_cost(rows, matches * width, true), matches)
}
fn estimated_rows(table: &TableInfo) -> f64 {
match table.analysed_rows {
Some(rows) if rows > 0 => rows as f64,
Some(_) => 1.0,
None => cost::DEFAULT_ROWS,
}
}
fn index_matches(index: Option<&IndexInfo>, rows: f64, equalities: usize, bounds: usize) -> f64 {
if equalities == 0 && bounds == 0 {
if let Some(index) = index {
if index.partial_sql.is_some() {
if let Some(held) = index.analysed_rows {
return (held as f64).max(1.0);
}
}
}
}
let mut matches = match index {
Some(index) if !index.prefix_rows.is_empty() && equalities > 0 => index
.prefix_rows
.get(equalities.saturating_sub(1))
.copied()
.map(|value| value as f64)
.unwrap_or(rows),
Some(index) if index.unique && equalities >= index.columns.len() => 1.0,
Some(_) if equalities > 0 => cost::default_equality_rows(equalities, rows),
_ => {
let mut estimate = rows;
for _ in 0..equalities {
estimate /= cost::EQUALITY_SHARE;
}
estimate
}
};
for _ in 0..bounds {
matches /= cost::RANGE_SHARE;
}
matches.max(1.0)
}
pub fn is_outer(join: JoinKind) -> bool {
matches!(join, JoinKind::Left | JoinKind::Right | JoinKind::Full)
}
pub fn split_conjunction(expr: &BoundExpr, into: &mut Vec<BoundExpr>) {
match expr {
BoundExpr::And(left, right) => {
split_conjunction(left, into);
split_conjunction(right, into);
}
BoundExpr::Between {
negated: false,
operand,
low,
high,
low_affinity,
low_collation,
high_affinity,
high_collation,
} if matches!(
**operand,
BoundExpr::Column { .. } | BoundExpr::Rowid { .. }
) =>
{
into.push(BoundExpr::Compare {
op: BinaryOp::GreaterEqual,
left: operand.clone(),
right: low.clone(),
affinity: *low_affinity,
collation: *low_collation,
});
into.push(BoundExpr::Compare {
op: BinaryOp::LessEqual,
left: operand.clone(),
right: high.clone(),
affinity: *high_affinity,
collation: *high_collation,
});
}
other => into.push(other.clone()),
}
}
fn distribute_residuals(
terms: &[BoundExpr],
consumed: &[bool],
ids: &[usize],
) -> (Vec<Option<BoundExpr>>, Option<BoundExpr>) {
let levels = ids.len();
let mut residuals: Vec<Option<BoundExpr>> = vec![None; levels];
let mut constant: Option<BoundExpr> = None;
for (index, term) in terms.iter().enumerate() {
if consumed.get(index).copied().unwrap_or(false) {
continue;
}
let mut used = Vec::new();
term.sources_used(&mut used);
let level = used
.iter()
.filter_map(|source| ids.iter().position(|id| id == source))
.max();
match level {
Some(level) if level < levels => {
if let Some(slot) = residuals.get_mut(level) {
*slot = Some(match slot.take() {
Some(existing) => {
BoundExpr::And(Box::new(existing), Box::new(term.clone()))
}
None => term.clone(),
});
}
}
_ => {
constant = Some(match constant.take() {
Some(existing) => BoundExpr::And(Box::new(existing), Box::new(term.clone())),
None => term.clone(),
});
}
}
}
(residuals, constant)
}
fn choose_path(
position: usize,
ids: &[usize],
source: &BoundSource,
select: &BoundSelect,
terms: &[BoundExpr],
consumed: &mut [bool],
levers: Levers,
) -> AccessPath {
match &source.rows {
SourceRows::Subquery(block) => {
let width = block.columns.len();
let correlated = !block.correlations.is_empty();
return AccessPath::Subquery {
plan: Box::new(plan_select_with((**block).clone(), levers)),
width,
correlated,
};
}
SourceRows::Recursive(body) => {
let width = body.seeds.first().map_or(0, |(_, arm)| arm.columns.len());
return AccessPath::Recursive {
seeds: body
.seeds
.iter()
.map(|(op, arm)| (*op, plan_select_with(arm.clone(), levers)))
.collect(),
steps: body
.steps
.iter()
.map(|(op, arm)| (*op, plan_select_with(arm.clone(), levers)))
.collect(),
width,
};
}
SourceRows::RecursiveSelf { cte } => {
return AccessPath::RecursiveSelf { cte: *cte };
}
SourceRows::Table => {}
}
let id = ids.get(position).copied().unwrap_or(position);
let table = &source.table;
let forced = match &source.index_hint {
crate::bind::IndexChoice::Only(wanted) => Some(wanted.as_slice()),
_ => None,
};
if let Some(path) = vector_path(id, position, source, select) {
let named = match &path {
AccessPath::VectorProbe { index, .. } => table
.indexes
.iter()
.find(|held| &held.name == index)
.map(|held| held.folded.as_slice()),
_ => None,
};
if forced.is_none() || forced == named {
return path;
}
}
if let Some(module) = table.module.clone() {
return virtual_path(id, position, ids, source, select, module, terms, consumed);
}
if forced.is_some() {
return forced_path(id, position, ids, source, select, terms, consumed, levers);
}
let mut candidates: Vec<(AccessPath, Vec<bool>)> = Vec::new();
let mut trial = consumed.to_vec();
if let Some(path) = rowid_path(id, position, ids, table, terms, &mut trial) {
candidates.push((path, trial));
}
if source.index_hint != crate::bind::IndexChoice::NotIndexed {
let mut trial = consumed.to_vec();
let needed = select.columns_read(id);
if let Some(path) = index_path(
id, position, ids, source, terms, &mut trial, &needed, levers,
) {
candidates.push((path, trial));
}
}
candidates.push((
AccessPath::TableScan { root: table.root },
consumed.to_vec(),
));
let sort = sort_penalty(select, position, source, levers);
let mut best: Option<(f64, AccessPath, Vec<bool>)> = None;
for (path, trial) in candidates {
let (mut cost, _) = path_cost(source, &path);
if !levers.has(Levers::ORDERED_WALK)
|| ordering_provided(select, id, table, &path).is_none()
{
cost += sort;
}
if best
.as_ref()
.is_none_or(|(existing, _, _)| cost < *existing - 1e-9)
{
best = Some((cost, path, trial));
}
}
match best {
Some((_, path, trial)) => {
consumed.copy_from_slice(&trial);
path
}
None => AccessPath::TableScan { root: table.root },
}
}
fn sort_penalty(
select: &BoundSelect,
position: usize,
source: &BoundSource,
levers: Levers,
) -> f64 {
let streams = levers.has(Levers::STREAMING_GROUP)
&& ((!select.group_by.is_empty() && !select.distinct)
|| (select.distinct && select.group_by.is_empty() && select.aggregates.is_empty()));
let answerable = levers.has(Levers::ORDERED_WALK)
&& position == 0
&& select.sources.len() == 1
&& select.windows.is_empty()
&& select.compounds.is_empty()
&& !select.order_by.is_empty()
&& ((select.group_by.is_empty() && select.aggregates.is_empty() && !select.distinct)
|| streams);
if !answerable {
return 0.0;
}
cost::sort_cost(estimated_rows(&source.table))
}
fn virtual_path(
id: usize,
position: usize,
ids: &[usize],
source: &BoundSource,
select: &BoundSelect,
module: crate::vtab::ModuleRef,
terms: &[BoundExpr],
consumed: &mut [bool],
) -> AccessPath {
let table = &source.table;
let mut offer = Vec::new();
for (index, term) in terms.iter().enumerate() {
if consumed.get(index).copied().unwrap_or(false) {
continue;
}
let Some((column, op, value)) = virtual_constraint(id, table, term) else {
continue;
};
offer.push(VirtualConstraint {
spec: crate::vtab::ConstraintSpec {
column,
op,
usable: is_available(position, ids, &value),
},
value,
predicate: term.clone(),
});
if let Some(slot) = consumed.get_mut(index) {
*slot = true;
}
}
let order_by = order_offer(id, position, select);
AccessPath::VirtualScan {
module,
offer,
order_by,
chosen: None,
}
}
fn order_offer(id: usize, position: usize, select: &BoundSelect) -> Vec<crate::vtab::OrderSpec> {
if position != 0 {
return Vec::new();
}
let mut offer = Vec::new();
for term in &select.order_by {
let column = match &term.expr {
BoundExpr::Column { source, column, .. } if *source == id => i32::from(*column),
BoundExpr::Rowid { source } if *source == id => crate::vtab::ROWID_COLUMN,
_ => return Vec::new(),
};
offer.push(crate::vtab::OrderSpec {
column,
descending: term.order == crate::ast::SortOrder::Descending,
});
}
offer
}
pub fn conjunction(filter: &BoundExpr) -> Vec<BoundExpr> {
let mut terms = Vec::new();
split_conjunction(filter, &mut terms);
terms
}
fn virtual_constraint(
id: usize,
table: &TableInfo,
term: &BoundExpr,
) -> Option<(i32, crate::vtab::ConstraintOp, BoundExpr)> {
use crate::vtab::{ConstraintOp, ROWID_COLUMN};
if let BoundExpr::Pattern {
negated: false,
op,
operand,
pattern,
escape: None,
} = term
{
if let BoundExpr::Column { source, column, .. } = operand.as_ref() {
if *source == id {
let op = match op {
crate::ast::PatternOp::Match => ConstraintOp::Match,
crate::ast::PatternOp::Like => ConstraintOp::Like,
crate::ast::PatternOp::Glob => ConstraintOp::Glob,
crate::ast::PatternOp::Regexp => ConstraintOp::Regexp,
};
return Some((i32::from(*column), op, pattern.as_ref().clone()));
}
}
}
if let Some((op, value)) = comparison_against_rowid(id, term) {
return binary_constraint(op).map(|op| (ROWID_COLUMN, op, value));
}
for column in 0..table.columns.len() {
let column = column as u16;
if let Some((op, value)) = comparison_against_column(id, column, term) {
return binary_constraint(op).map(|op| (i32::from(column), op, value));
}
}
None
}
fn binary_constraint(op: BinaryOp) -> Option<crate::vtab::ConstraintOp> {
use crate::vtab::ConstraintOp;
Some(match op {
BinaryOp::Equal => ConstraintOp::Eq,
BinaryOp::NotEqual => ConstraintOp::Ne,
BinaryOp::Less => ConstraintOp::Lt,
BinaryOp::LessEqual => ConstraintOp::Le,
BinaryOp::Greater => ConstraintOp::Gt,
BinaryOp::GreaterEqual => ConstraintOp::Ge,
_ => return None,
})
}
pub fn write_path_with(
table: &TableInfo,
source_id: usize,
filter: Option<&BoundExpr>,
levers: Levers,
) -> AccessPath {
if !levers.has(Levers::INDEXED_WRITE) {
return AccessPath::TableScan { root: table.root };
}
let scan = AccessPath::TableScan { root: table.root };
if table.module.is_some() || table.without_rowid {
return scan;
}
let Some(filter) = filter else {
return scan;
};
let mut terms = Vec::new();
split_conjunction(filter, &mut terms);
let ids = [source_id];
let mut consumed = vec![false; terms.len()];
if let Some(path) = rowid_path(source_id, 0, &ids, table, &terms, &mut consumed) {
return path;
}
let source = BoundSource {
index_hint: crate::bind::IndexChoice::Any,
id: source_id,
rows: SourceRows::Table,
table: std::rc::Rc::new(table.clone()),
alias: table.name.clone(),
join: JoinKind::Inner,
constraint: None,
suppressed: Vec::new(),
index_exprs: Vec::new(),
};
let mut consumed = vec![false; terms.len()];
let needed = ColumnUse {
opaque: true,
..ColumnUse::default()
};
let Some(path) = index_path(
source_id,
0,
&ids,
&source,
&terms,
&mut consumed,
&needed,
levers,
) else {
return scan;
};
let (index_cost, _) = path_cost(&source, &path);
let (scan_cost, _) = path_cost(&source, &scan);
if index_cost <= scan_cost {
return path;
}
scan
}
fn rowid_path(
id: usize,
position: usize,
ids: &[usize],
table: &TableInfo,
terms: &[BoundExpr],
consumed: &mut [bool],
) -> Option<AccessPath> {
if !table.has_rowid() {
return None;
}
for (index, term) in terms.iter().enumerate() {
if consumed.get(index).copied().unwrap_or(false) {
continue;
}
let Some((op, value)) = comparison_against_rowid(id, term) else {
continue;
};
if op != BinaryOp::Equal || !is_available(position, ids, &value) {
continue;
}
if let Some(slot) = consumed.get_mut(index) {
*slot = true;
}
return Some(AccessPath::RowidSeek {
root: table.root,
key: value,
});
}
if let Some(path) = seek_union::rowid_in_list_path(id, position, ids, table, terms, consumed) {
return Some(path);
}
if position != 0 {
return None;
}
let mut low = None;
let mut high = None;
let mut used = Vec::new();
for (index, term) in terms.iter().enumerate() {
if consumed.get(index).copied().unwrap_or(false) {
continue;
}
let Some((op, value)) = comparison_against_rowid(id, term) else {
continue;
};
if !is_available(position, ids, &value) {
continue;
}
match op {
BinaryOp::Greater if low.is_none() => {
low = Some(RangeBound {
kind: BoundKind::Greater,
value,
unconverted: false,
});
used.push(index);
}
BinaryOp::GreaterEqual if low.is_none() => {
low = Some(RangeBound {
kind: BoundKind::GreaterEqual,
value,
unconverted: false,
});
used.push(index);
}
BinaryOp::Less if high.is_none() => {
high = Some(RangeBound {
kind: BoundKind::Less,
value,
unconverted: false,
});
used.push(index);
}
BinaryOp::LessEqual if high.is_none() => {
high = Some(RangeBound {
kind: BoundKind::LessEqual,
value,
unconverted: false,
});
used.push(index);
}
_ => {}
}
}
if low.is_none() && high.is_none() {
return None;
}
for index in used {
if let Some(slot) = consumed.get_mut(index) {
*slot = true;
}
}
Some(AccessPath::RowidRange {
root: table.root,
low,
high,
})
}
fn index_path(
id: usize,
position: usize,
ids: &[usize],
source: &BoundSource,
terms: &[BoundExpr],
consumed: &mut [bool],
needed: &ColumnUse,
levers: Levers,
) -> Option<AccessPath> {
let table = &source.table;
let forced = match &source.index_hint {
crate::bind::IndexChoice::Only(wanted) => Some(wanted.as_slice()),
_ => None,
};
let context = CandidateContext {
id,
position,
ids,
table,
terms,
consumed,
needed,
levers,
forced: forced.is_some(),
};
let mut best: Option<(f64, AccessPath, Vec<usize>)> = None;
for (at, index) in table.indexes.iter().enumerate() {
if index.origin == crate::catalog_view::IndexOrigin::Module {
continue;
}
if forced.is_some_and(|wanted| wanted != index.folded.as_slice()) {
continue;
}
let computed = source.index_exprs.iter().find(|held| held.position == at);
let usable = index_usable(source, at, index, terms);
if !usable && index.partial_sql.is_some() {
continue;
}
if let Some((path, used)) = index_candidate(&context, index, computed, usable) {
consider_index_candidate(source, &mut best, path, used);
}
if let Some((path, used)) = seek_union::in_list_union_path(&context, index, usable) {
consider_index_candidate(source, &mut best, path, used);
}
if let Some((path, used)) = seek_union::keyset_range_union_path(&context, index, usable) {
consider_index_candidate(source, &mut best, path, used);
}
}
let (_, path, used) = best?;
for index in used {
if let Some(slot) = consumed.get_mut(index) {
*slot = true;
}
}
Some(path)
}
pub(crate) struct CandidateContext<'a> {
pub(crate) id: usize,
pub(crate) position: usize,
pub(crate) ids: &'a [usize],
pub(crate) table: &'a TableInfo,
pub(crate) terms: &'a [BoundExpr],
pub(crate) consumed: &'a [bool],
pub(crate) needed: &'a ColumnUse,
pub(crate) levers: Levers,
pub(crate) forced: bool,
}
fn consider_index_candidate(
source: &BoundSource,
best: &mut Option<(f64, AccessPath, Vec<usize>)>,
path: AccessPath,
used: Vec<usize>,
) {
let (cost, _) = path_cost(source, &path);
let better = best
.as_ref()
.is_none_or(|(existing, _, _)| cost <= *existing + 1e-9);
if better {
*best = Some((cost, path, used));
}
}
fn index_candidate(
context: &CandidateContext<'_>,
index: &IndexInfo,
computed: Option<&crate::dml::BoundIndexExprs>,
usable: bool,
) -> Option<(AccessPath, Vec<usize>)> {
let CandidateContext {
id,
position,
ids,
table,
terms,
consumed,
needed,
levers,
forced,
} = *context;
let mut equalities = Vec::new();
let mut unconverted = Vec::new();
let mut used = Vec::new();
let mut collations = Vec::new();
let mut descending = Vec::new();
let mut columns: Vec<Option<u16>> = Vec::new();
let mut key = 0usize;
while let Some(key_column) = index.columns.get(key) {
let collation = collation_of(&key_column.collation);
let found = match key_column.column {
Some(column) => {
find_equality(id, position, ids, column, collation, terms, consumed, &used)
.map(|(term_index, value)| (term_index, value, Some(column)))
}
None => computed
.and_then(|held| held.keys.get(key).cloned().flatten())
.and_then(|wanted| {
find_expr_equality(position, ids, &wanted, collation, terms, consumed, &used)
})
.map(|(term_index, value)| (term_index, value, None)),
};
let Some((term_index, value, column)) = found else {
break;
};
if terms.get(term_index).is_some_and(compares_unconverted) {
unconverted.push(equalities.len());
}
equalities.push(value);
used.push(term_index);
collations.push(collation);
descending.push(key_column.descending);
columns.push(column);
key = key.saturating_add(1);
}
let range = match index.columns.get(key) {
Some(key_column) if position == 0 => range::key_range(context, key_column, &mut used),
_ => None,
};
let (low, high) = match range {
Some(found) => {
collations.push(found.collation);
descending.push(found.descending);
columns.push(Some(found.column));
(found.low, found.high)
}
None => (None, None),
};
let covering = levers
.has(Levers::COVERING_INDEX)
.then(|| covering_slots(table, index, needed, usable))
.flatten();
let partial_walk = usable && index.partial_sql.is_some();
if equalities.is_empty()
&& low.is_none()
&& high.is_none()
&& covering.is_none()
&& !partial_walk
&& !(forced && usable)
{
return None;
}
Some((
AccessPath::IndexSeek {
table_root: table.root,
index_root: index.root,
index_name: index.name.clone(),
equalities,
unconverted,
low,
high,
collations,
descending,
columns,
without_rowid: table.without_rowid,
key_entry_slots: if table.without_rowid && index.root != table.root {
let leading = index.columns.len();
(0..table.primary_key().len())
.map(|offset| leading.saturating_add(offset))
.collect()
} else {
Vec::new()
},
covering,
},
used,
))
}
pub const ROWID_ENTRY_SLOT: usize = usize::MAX;
fn covering_slots(
table: &TableInfo,
index: &IndexInfo,
needed: &ColumnUse,
usable: bool,
) -> Option<Vec<(u16, usize)>> {
if needed.opaque || table.without_rowid || !usable {
return None;
}
let mut slots = Vec::with_capacity(needed.columns.len());
for slot in &needed.columns {
if table.rowid_alias == Some(*slot) {
slots.push((*slot, ROWID_ENTRY_SLOT));
continue;
}
let position = index
.columns
.iter()
.position(|key| key.column == Some(*slot))?;
slots.push((*slot, position));
}
Some(slots)
}
fn find_equality(
id: usize,
position: usize,
ids: &[usize],
column: u16,
collation: Collation,
terms: &[BoundExpr],
consumed: &[bool],
used: &[usize],
) -> Option<(usize, BoundExpr)> {
for (index, term) in terms.iter().enumerate() {
if consumed.get(index).copied().unwrap_or(false) || used.contains(&index) {
continue;
}
let Some((op, value)) = indexable_comparison(id, column, term) else {
continue;
};
if op != BinaryOp::Equal || !is_available(position, ids, &value) {
continue;
}
if comparison_collation(term) != collation {
continue;
}
return Some((index, value));
}
None
}
fn find_expr_equality(
position: usize,
ids: &[usize],
wanted: &BoundExpr,
collation: Collation,
terms: &[BoundExpr],
consumed: &[bool],
used: &[usize],
) -> Option<(usize, BoundExpr)> {
for (index, term) in terms.iter().enumerate() {
if consumed.get(index).copied().unwrap_or(false) || used.contains(&index) {
continue;
}
let BoundExpr::Compare {
op, left, right, ..
} = term
else {
continue;
};
if *op != BinaryOp::Equal || comparison_collation(term) != collation {
continue;
}
let value = if left.as_ref() == wanted {
right.as_ref().clone()
} else if right.as_ref() == wanted {
left.as_ref().clone()
} else {
continue;
};
if !is_available(position, ids, &value) {
continue;
}
return Some((index, value));
}
None
}
fn is_available(position: usize, ids: &[usize], value: &BoundExpr) -> bool {
let mut used = Vec::new();
value.sources_used(&mut used);
used.iter().all(|source| {
ids.iter()
.position(|id| id == source)
.is_none_or(|level| level < position)
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bind::BoundExpr;
#[test]
fn only_conjunctions_split() {
let expr = BoundExpr::And(
Box::new(BoundExpr::Integer(1)),
Box::new(BoundExpr::Or(
Box::new(BoundExpr::Integer(2)),
Box::new(BoundExpr::Integer(3)),
)),
);
let mut terms = Vec::new();
split_conjunction(&expr, &mut terms);
assert_eq!(terms.len(), 2);
assert!(matches!(terms.get(1), Some(BoundExpr::Or(_, _))));
}
#[test]
fn a_seek_key_may_only_read_outer_terms() {
let outer = BoundExpr::Column {
source: 0,
column: 0,
slot: 0,
affinity: inillucent_value::Affinity::Integer,
collation: Collation::Binary,
};
let ids = [0usize, 1usize];
assert!(is_available(1, &ids, &outer));
assert!(!is_available(0, &ids, &outer));
assert!(is_available(0, &ids, &BoundExpr::Integer(5)));
assert!(is_available(0, &[7usize], &outer));
}
}