use std::ops::Bound;
use crate::document::Value;
use crate::fts::FtsDef;
use crate::index::{
CompoundIndexDef, IndexDef, compound_range_eq, index_range_cmp, index_range_eq,
};
use crate::query::filter::Filter;
#[derive(Debug)]
pub enum QueryPlan {
FullScan,
ById { ids: Vec<ulid::Ulid> },
IndexEq {
field: String,
start: Vec<u8>,
end: Vec<u8>,
},
IndexRange {
field: String,
start: Bound<Vec<u8>>,
end: Bound<Vec<u8>>,
},
IndexIn {
field: String,
ranges: Vec<(Vec<u8>, Vec<u8>)>, },
IndexOr { plans: Vec<Self> },
IndexAnd { plans: Vec<Self> },
FtsSearch {
field: String,
tokens: Vec<String>,
},
CompoundIndexEq {
fields: Vec<String>,
start: Vec<u8>,
end: Vec<u8>,
},
}
pub fn plan(filter: &Filter, indexes: &[IndexDef]) -> QueryPlan {
plan_with_fts(filter, indexes, &[])
}
pub fn plan_with_fts(filter: &Filter, indexes: &[IndexDef], fts_indexes: &[FtsDef]) -> QueryPlan {
plan_full(filter, indexes, fts_indexes, &[])
}
pub fn plan_full(
filter: &Filter,
indexes: &[IndexDef],
fts_indexes: &[FtsDef],
compound_indexes: &[CompoundIndexDef],
) -> QueryPlan {
let indexed_fields: Vec<&str> = indexes.iter().map(|i| i.field.as_str()).collect();
let fts_fields: Vec<&str> = fts_indexes.iter().map(|i| i.field.as_str()).collect();
plan_inner(filter, &indexed_fields, &fts_fields, compound_indexes)
}
fn plan_inner(
filter: &Filter,
indexed_fields: &[&str],
fts_fields: &[&str],
compound_indexes: &[CompoundIndexDef],
) -> QueryPlan {
match filter {
Filter::All => QueryPlan::FullScan,
Filter::Eq(field, value) if field == "_id" => {
let ids = match value {
crate::document::Value::Str(s) => {
ulid::Ulid::from_string(s).ok().into_iter().collect()
}
_ => Vec::new(),
};
QueryPlan::ById { ids }
}
Filter::In(field, values) if field == "_id" => {
let ids = values
.iter()
.filter_map(|v| match v {
crate::document::Value::Str(s) => ulid::Ulid::from_string(s).ok(),
_ => None,
})
.collect();
QueryPlan::ById { ids }
}
Filter::Eq(field, value) if indexed_fields.contains(&field.as_str()) => {
eq_plan(field, value).unwrap_or(QueryPlan::FullScan)
}
Filter::Gt(field, value) if indexed_fields.contains(&field.as_str()) => {
lower_bound_plan(field, value, false).unwrap_or(QueryPlan::FullScan)
}
Filter::Gte(field, value) if indexed_fields.contains(&field.as_str()) => {
lower_bound_plan(field, value, true).unwrap_or(QueryPlan::FullScan)
}
Filter::Lt(field, value) if indexed_fields.contains(&field.as_str()) => {
upper_bound_plan(field, value, false).unwrap_or(QueryPlan::FullScan)
}
Filter::Lte(field, value) if indexed_fields.contains(&field.as_str()) => {
upper_bound_plan(field, value, true).unwrap_or(QueryPlan::FullScan)
}
Filter::In(field, values) if indexed_fields.contains(&field.as_str()) => {
let mut ranges: Vec<(Vec<u8>, Vec<u8>)> = Vec::with_capacity(values.len());
for v in values {
let Some(range) = index_range_eq(v) else {
return QueryPlan::FullScan;
};
ranges.push(range);
if let Value::Float(f) = v
&& *f == 0.0
&& let Some(range) = index_range_eq(&Value::Float(-*f))
{
ranges.push(range);
}
}
if !ranges.is_empty() {
return QueryPlan::IndexIn {
field: field.clone(),
ranges,
};
}
QueryPlan::FullScan
}
Filter::Contains(field, query) if fts_fields.contains(&field.as_str()) => {
use crate::fts::tokenize;
let tokens = tokenize(query);
if tokens.is_empty() {
return QueryPlan::FullScan;
}
QueryPlan::FtsSearch {
field: field.clone(),
tokens,
}
}
Filter::And(filters) => {
let eq_map: Vec<(&str, &crate::document::Value)> = filters
.iter()
.filter_map(|f| {
if let Filter::Eq(field, val) = f {
Some((field.as_str(), val))
} else {
None
}
})
.collect();
for cidx in compound_indexes {
let values: Option<Vec<&crate::document::Value>> = cidx
.fields
.iter()
.map(|f| {
eq_map
.iter()
.find(|(k, _)| *k == f.as_str())
.map(|(_, v)| *v)
})
.collect();
if let Some(vals) = values {
let mut variants: Vec<Vec<crate::document::Value>> =
vec![vals.iter().map(|v| (*v).clone()).collect()];
for i in 0..vals.len() {
if matches!(vals[i], crate::document::Value::Float(f) if *f == 0.0) {
let alternate = crate::document::Value::Float(
if matches!(vals[i], crate::document::Value::Float(f) if f.is_sign_negative())
{
0.0
} else {
-0.0
},
);
let mut additional = variants.clone();
for variant in &mut additional {
variant[i] = alternate.clone();
}
variants.extend(additional);
}
}
let plans: Vec<_> = variants
.into_iter()
.filter_map(|values| {
let refs: Vec<_> = values.iter().collect();
compound_range_eq(&refs).map(|(start, end)| {
QueryPlan::CompoundIndexEq {
fields: cidx.fields.clone(),
start,
end,
}
})
})
.collect();
if plans.len() == 1 {
return plans.into_iter().next().unwrap();
} else if !plans.is_empty() {
return QueryPlan::IndexOr { plans };
}
}
}
if let Some(plan) = bounded_range_from_and(filters, indexed_fields) {
return plan;
}
let indexed_eq: Vec<QueryPlan> = filters
.iter()
.filter(|f| {
matches!(f, Filter::Eq(field, _)
if field != "_id" && indexed_fields.contains(&field.as_str()))
})
.filter_map(
|f| match plan_inner(f, indexed_fields, fts_fields, compound_indexes) {
QueryPlan::FullScan => None,
plan => Some(plan),
},
)
.collect();
if indexed_eq.len() > 1 {
return QueryPlan::IndexAnd { plans: indexed_eq };
}
for f in filters {
let sub = plan_inner(f, indexed_fields, fts_fields, compound_indexes);
if !matches!(sub, QueryPlan::FullScan) {
return sub;
}
}
QueryPlan::FullScan
}
Filter::Or(filters) => {
let sub_plans: Vec<QueryPlan> = filters
.iter()
.map(|f| plan_inner(f, indexed_fields, fts_fields, compound_indexes))
.collect();
if sub_plans.iter().all(|p| !matches!(p, QueryPlan::FullScan)) {
return QueryPlan::IndexOr { plans: sub_plans };
}
QueryPlan::FullScan
}
_ => QueryPlan::FullScan,
}
}
fn lower_bound_plan(field: &str, value: &Value, inclusive: bool) -> Option<QueryPlan> {
let negative_zero = Value::Float(-0.0);
let value = if inclusive && matches!(value, Value::Float(f) if *f == 0.0) {
&negative_zero
} else {
value
};
let (start, end) = index_range_cmp(Some((value, inclusive)), None)?;
let primary = QueryPlan::IndexRange {
field: field.to_string(),
start,
end,
};
if let Value::Float(f) = value
&& !f.is_nan()
{
let twin_lo = Value::Int(f.floor() as i64);
let twin_hi = Value::Int(i64::MAX);
if let Some((s, e)) = index_range_cmp(Some((&twin_lo, true)), Some((&twin_hi, true))) {
return Some(QueryPlan::IndexOr {
plans: vec![
primary,
QueryPlan::IndexRange {
field: field.to_string(),
start: s,
end: e,
},
],
});
}
}
Some(primary)
}
fn upper_bound_plan(field: &str, value: &Value, inclusive: bool) -> Option<QueryPlan> {
let positive_zero = Value::Float(0.0);
let value = if inclusive && matches!(value, Value::Float(f) if *f == 0.0) {
&positive_zero
} else {
value
};
let (start, end) = index_range_cmp(None, Some((value, inclusive)))?;
let primary = QueryPlan::IndexRange {
field: field.to_string(),
start,
end,
};
if let Value::Int(n) = value {
let twin_hi = Value::Float(f64_next_up(*n as f64));
let twin_lo = Value::Float(f64::NEG_INFINITY);
if let Some((s, e)) = index_range_cmp(Some((&twin_lo, true)), Some((&twin_hi, true))) {
return Some(QueryPlan::IndexOr {
plans: vec![
primary,
QueryPlan::IndexRange {
field: field.to_string(),
start: s,
end: e,
},
],
});
}
}
Some(primary)
}
fn eq_plan(field: &str, value: &Value) -> Option<QueryPlan> {
let (start, end) = index_range_eq(value)?;
if let Value::Float(f) = value
&& *f == 0.0
{
let (s2, e2) = index_range_eq(&Value::Float(-*f))?;
return Some(QueryPlan::IndexIn {
field: field.to_string(),
ranges: vec![(start, end), (s2, e2)],
});
}
Some(QueryPlan::IndexEq {
field: field.to_string(),
start,
end,
})
}
fn f64_next_up(f: f64) -> f64 {
if f.is_nan() || f == f64::INFINITY {
return f;
}
let bits = if f == 0.0 {
1 } else if f > 0.0 {
f.to_bits() + 1
} else {
f.to_bits() - 1
};
f64::from_bits(bits)
}
fn bounded_range_from_and(filters: &[Filter], indexed_fields: &[&str]) -> Option<QueryPlan> {
for field in indexed_fields {
let mut lower: Option<(&Value, bool)> = None;
let mut upper: Option<(&Value, bool)> = None;
for f in filters {
match f {
Filter::Gt(fl, v) if fl == field => lower = Some((v, false)),
Filter::Gte(fl, v) if fl == field => lower = Some((v, true)),
Filter::Lt(fl, v) if fl == field => upper = Some((v, false)),
Filter::Lte(fl, v) if fl == field => upper = Some((v, true)),
_ => {}
}
}
if let (Some((lo, li)), Some((hi, ui))) = (lower, upper) {
return Some(QueryPlan::IndexAnd {
plans: vec![
lower_bound_plan(field, lo, li)?,
upper_bound_plan(field, hi, ui)?,
],
});
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::document::Value;
fn indexes(fields: &[&str]) -> Vec<IndexDef> {
fields
.iter()
.map(|f| IndexDef {
collection: "col".into(),
field: f.to_string(),
})
.collect()
}
#[test]
fn eq_with_index() {
let f = Filter::Eq("age".into(), Value::Int(30));
let plan = plan(&f, &indexes(&["age"]));
assert!(matches!(plan, QueryPlan::IndexEq { .. }));
}
#[test]
fn eq_without_index() {
let f = Filter::Eq("age".into(), Value::Int(30));
let plan = plan(&f, &indexes(&[]));
assert!(matches!(plan, QueryPlan::FullScan));
}
#[test]
fn range_with_index() {
let f = Filter::Gte("score".into(), Value::Float(5.0));
let plan = plan(&f, &indexes(&["score"]));
match plan {
QueryPlan::IndexOr { plans } => {
assert_eq!(plans.len(), 2);
assert!(
plans
.iter()
.all(|p| matches!(p, QueryPlan::IndexRange { .. }))
);
}
other => panic!("expected IndexOr of two ranges, got {other:?}"),
}
}
#[test]
fn range_with_index_string_bound_stays_single_range() {
let f = Filter::Gte("name".into(), Value::Str("m".into()));
let plan = plan(&f, &indexes(&["name"]));
assert!(matches!(plan, QueryPlan::IndexRange { .. }));
}
#[test]
fn in_with_index() {
let f = Filter::In(
"status".into(),
vec![Value::Str("active".into()), Value::Str("pending".into())],
);
let plan = plan(&f, &indexes(&["status"]));
assert!(matches!(plan, QueryPlan::IndexIn { .. }));
}
#[test]
fn and_picks_indexed_subfilter() {
let f = Filter::And(vec![
Filter::Eq("unindexed".into(), Value::Int(1)),
Filter::Eq("age".into(), Value::Int(30)),
]);
let plan = plan(&f, &indexes(&["age"]));
assert!(matches!(plan, QueryPlan::IndexEq { field, .. } if field == "age"));
}
#[test]
fn or_all_indexed_uses_index_or() {
let f = Filter::Or(vec![
Filter::Eq("age".into(), Value::Int(30)),
Filter::Eq("age".into(), Value::Int(40)),
]);
let plan = plan(&f, &indexes(&["age"]));
assert!(matches!(plan, QueryPlan::IndexOr { .. }));
}
#[test]
fn or_partially_unindexed_falls_back() {
let f = Filter::Or(vec![
Filter::Eq("age".into(), Value::Int(30)),
Filter::Eq("unindexed".into(), Value::Int(40)),
]);
let plan = plan(&f, &indexes(&["age"]));
assert!(matches!(plan, QueryPlan::FullScan));
}
}