use std::collections::HashSet;
use rust_rocksdb::{Direction, IteratorMode};
use serde_json::Value;
use super::types::{Context, IndexableCondition};
use super::QueryExecutor;
use crate::error::{DbError, DbResult};
use crate::sdbql::ast::*;
use crate::storage::index::{IndexSpec, IndexType};
use crate::storage::Collection;
pub(super) const AUTO_INDEX_CAP: usize = 16;
const MAX_INDEXED_IN_KEYS: usize = 10_000;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct IndexHint {
pub names: Vec<String>,
pub force: bool,
}
impl IndexHint {
pub fn from_options(index_hint: Option<&[String]>, force: bool) -> Option<Self> {
let names: Vec<String> = index_hint.unwrap_or_default().to_vec();
if names.is_empty() {
return None;
}
Some(Self { names, force })
}
}
pub(super) fn like_literal_prefix(pattern: &str) -> Option<String> {
let prefix: String = pattern
.chars()
.take_while(|c| !matches!(c, '%' | '_' | '\\'))
.collect();
(!prefix.is_empty()).then_some(prefix)
}
pub(super) fn prefix_successor(prefix: &str) -> Option<String> {
let mut chars: Vec<char> = prefix.chars().collect();
while let Some(c) = chars.pop() {
let mut next = c as u32 + 1;
while next <= char::MAX as u32 {
if let Some(n) = char::from_u32(next) {
chars.push(n);
return Some(chars.into_iter().collect());
}
next += 1; }
}
None
}
fn in_list_is_indexable(value: &Value) -> bool {
matches!(value, Value::Array(items)
if items.len() <= MAX_INDEXED_IN_KEYS && !items.iter().any(Value::is_null))
}
fn is_range_op(op: &BinaryOperator) -> bool {
matches!(
op,
BinaryOperator::LessThan
| BinaryOperator::LessThanOrEqual
| BinaryOperator::GreaterThan
| BinaryOperator::GreaterThanOrEqual
)
}
fn regular_index_on(collection: &Collection, field: &str) -> Option<crate::storage::index::Index> {
collection.get_all_indexes().into_iter().find(|i| {
i.fields.len() == 1
&& i.fields[0] == field
&& !matches!(i.index_type, IndexType::Fulltext | IndexType::Vector)
})
}
fn index_prefix_scan(
collection: &Collection,
field: &str,
prefix: &str,
cap: usize,
) -> Option<Vec<crate::storage::Document>> {
let index = regular_index_on(collection, field)?;
let upper = prefix_successor(prefix)?;
let encode = |s: &str| {
hex::encode(crate::storage::codec::encode_key(&Value::String(
s.to_string(),
)))
};
let base = format!("{}{}:", crate::storage::collection::IDX_PREFIX, index.name);
let lo = format!("{}{}", base, encode(prefix));
let hi = format!("{}{}", base, encode(&upper));
let db = &collection.db;
let cf = db.cf_handle(&collection.name)?;
let mut doc_keys: Vec<Vec<u8>> = Vec::new();
let iter = db.iterator_cf(&cf, IteratorMode::From(lo.as_bytes(), Direction::Forward));
for (k, v) in iter.flatten() {
if !k.starts_with(base.as_bytes()) || k.as_ref() >= hi.as_bytes() {
break;
}
doc_keys.push(Collection::doc_key(&String::from_utf8_lossy(&v)));
if doc_keys.len() >= cap {
break;
}
}
if doc_keys.is_empty() {
return Some(Vec::new());
}
let docs = db
.multi_get_cf(doc_keys.iter().map(|k| (&cf, k.as_slice())))
.into_iter()
.filter_map(|r| r.ok())
.flatten()
.filter_map(|bytes| crate::storage::serializer::deserialize_doc(&bytes).ok())
.collect();
Some(docs)
}
const AUTO_INDEX_MAX_DOCS_DEFAULT: usize = 1_000_000;
fn auto_index_max_docs() -> usize {
match std::env::var("SOLIDB_AUTO_INDEX_MAX_DOCS") {
Ok(v) => v.trim().parse().unwrap_or(AUTO_INDEX_MAX_DOCS_DEFAULT),
Err(_) => AUTO_INDEX_MAX_DOCS_DEFAULT,
}
}
fn auto_index_name(field: &str) -> String {
format!("_auto_{field}")
}
fn is_auto_index(index: &crate::storage::index::Index) -> bool {
index.fields.len() == 1 && index.name == auto_index_name(&index.fields[0])
}
pub(super) fn field_is_auto_indexable(field: &str) -> bool {
if matches!(field, "_key" | "_id" | "_rev" | "") {
return false;
}
let mut parts = field.split('.');
parts.all(|p| {
let mut chars = p.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
_ => false,
}
}) && !field.contains("..")
&& !field.starts_with('.')
&& !field.ends_with('.')
}
fn collection_bare_name(cf_name: &str) -> &str {
cf_name.rsplit(':').next().unwrap_or(cf_name)
}
impl<'a> QueryExecutor<'a> {
pub(super) fn extract_indexable_condition(
&self,
expr: &Expression,
var_name: &str,
ctx: &Context,
) -> Option<IndexableCondition> {
if let Expression::FunctionCall { name, args } = expr {
if name.eq_ignore_ascii_case("STARTS_WITH") && args.len() == 2 {
let field = self.extract_field_path(&args[0], var_name)?;
let prefix = match self.extract_indexable_value(&args[1], var_name, ctx)? {
Value::String(p) if !p.is_empty() => p,
_ => return None,
};
return Some(IndexableCondition {
field,
op: BinaryOperator::Like,
value: Value::String(prefix),
});
}
return None;
}
if let Expression::BinaryOp { left, op, right } = expr {
match op {
BinaryOperator::In => {
let field = self.extract_field_path(left, var_name)?;
let value = self.extract_indexable_value(right, var_name, ctx)?;
if !in_list_is_indexable(&value) {
return None;
}
return Some(IndexableCondition {
field,
op: BinaryOperator::In,
value,
});
}
BinaryOperator::Like => {
let field = self.extract_field_path(left, var_name)?;
let prefix = match self.extract_indexable_value(right, var_name, ctx)? {
Value::String(p) => like_literal_prefix(&p)?,
_ => return None,
};
return Some(IndexableCondition {
field,
op: BinaryOperator::Like,
value: Value::String(prefix),
});
}
BinaryOperator::Equal
| BinaryOperator::LessThan
| BinaryOperator::LessThanOrEqual
| BinaryOperator::GreaterThan
| BinaryOperator::GreaterThanOrEqual => {
if let Some(field) = self.extract_field_path(left, var_name) {
if let Some(value) = self.extract_indexable_value(right, var_name, ctx) {
return Some(IndexableCondition {
field,
op: op.clone(),
value,
});
}
}
if let Some(field) = self.extract_field_path(right, var_name) {
if let Some(value) = self.extract_indexable_value(left, var_name, ctx) {
let reversed_op = match op {
BinaryOperator::LessThan => BinaryOperator::GreaterThan,
BinaryOperator::LessThanOrEqual => {
BinaryOperator::GreaterThanOrEqual
}
BinaryOperator::GreaterThan => BinaryOperator::LessThan,
BinaryOperator::GreaterThanOrEqual => {
BinaryOperator::LessThanOrEqual
}
other => other.clone(),
};
return Some(IndexableCondition {
field,
op: reversed_op,
value,
});
}
}
}
BinaryOperator::And => {
if let Some(cond) = self.extract_indexable_condition(left, var_name, ctx) {
return Some(cond);
}
return self.extract_indexable_condition(right, var_name, ctx);
}
_ => {}
}
}
None
}
pub(super) fn extract_equality_conditions(
&self,
expr: &Expression,
var_name: &str,
ctx: &Context,
) -> Vec<IndexableCondition> {
let mut out = Vec::new();
self.collect_equality_conditions(expr, var_name, ctx, &mut out);
out
}
pub(super) fn extract_indexable_conditions(
&self,
expr: &Expression,
var_name: &str,
ctx: &Context,
) -> Vec<IndexableCondition> {
fn walk(
exec: &QueryExecutor<'_>,
expr: &Expression,
var_name: &str,
ctx: &Context,
out: &mut Vec<IndexableCondition>,
) {
if let Expression::BinaryOp {
left,
op: BinaryOperator::And,
right,
} = expr
{
walk(exec, left, var_name, ctx, out);
walk(exec, right, var_name, ctx, out);
} else if let Some(c) = exec.extract_indexable_condition(expr, var_name, ctx) {
out.push(c);
}
}
let mut out = Vec::new();
walk(self, expr, var_name, ctx, &mut out);
out
}
fn ordered_index_candidates(
&self,
filter: &Expression,
var_name: &str,
ctx: &Context,
) -> Vec<IndexableCondition> {
let all = self.extract_indexable_conditions(filter, var_name, ctx);
let first = self.extract_indexable_condition(filter, var_name, ctx);
let (mut out, rest): (Vec<IndexableCondition>, Vec<IndexableCondition>) = all
.into_iter()
.partition(|c| matches!(c.op, BinaryOperator::Equal | BinaryOperator::In));
out.extend(
rest.into_iter()
.filter(|c| matches!(c.op, BinaryOperator::Like)),
);
if let Some(first) = first.filter(|c| is_range_op(&c.op)) {
out.push(first);
}
out
}
fn collect_equality_conditions(
&self,
expr: &Expression,
var_name: &str,
ctx: &Context,
out: &mut Vec<IndexableCondition>,
) {
if let Expression::BinaryOp {
left,
op: BinaryOperator::And,
right,
} = expr
{
self.collect_equality_conditions(left, var_name, ctx, out);
self.collect_equality_conditions(right, var_name, ctx, out);
return;
}
if let Some(cond) = self.extract_indexable_condition(expr, var_name, ctx) {
if matches!(cond.op, BinaryOperator::Equal) {
out.push(cond);
}
}
}
pub(super) fn lookup_index_for_filter(
&self,
collection: &Collection,
filter: &Expression,
var_name: &str,
ctx: &Context,
) -> Option<(Vec<crate::storage::Document>, String, String)> {
self.lookup_index_for_filter_limited(collection, filter, var_name, ctx, None)
}
pub(super) fn lookup_index_for_filter_limited(
&self,
collection: &Collection,
filter: &Expression,
var_name: &str,
ctx: &Context,
limit: Option<usize>,
) -> Option<(Vec<crate::storage::Document>, String, String)> {
let eq_conditions = self.extract_equality_conditions(filter, var_name, ctx);
if eq_conditions.len() >= 2 {
let pairs: Vec<(String, Value)> = eq_conditions
.iter()
.map(|c| (c.field.clone(), c.value.clone()))
.collect();
if let Some((index, docs)) = collection.index_lookup_eq_composite(&pairs) {
let type_str = format!("{:?}", index.index_type);
return Some((docs, index.name, type_str));
}
}
for cond in self.ordered_index_candidates(filter, var_name, ctx) {
if let Some(docs) = self.use_index_for_condition(collection, &cond, limit) {
let (name, type_str) = collection
.get_all_indexes()
.into_iter()
.find(|i| i.fields.len() == 1 && i.fields[0] == cond.field)
.map(|i| (i.name, format!("{:?}", i.index_type)))
.unwrap_or_default();
return Some((docs, name, type_str));
}
}
self.geo_lookup_for_filter(collection, filter, var_name, ctx, None, limit)
}
pub(super) fn lookup_index_for_filter_hinted(
&self,
collection: &Collection,
filter: &Expression,
var_name: &str,
ctx: &Context,
limit: Option<usize>,
hint: Option<&IndexHint>,
) -> DbResult<Option<(Vec<crate::storage::Document>, String, String)>> {
let Some(hint) = hint.filter(|h| !h.names.is_empty()) else {
return Ok(
self.lookup_index_for_filter_limited(collection, filter, var_name, ctx, limit)
);
};
if let Some(hit) = self.lookup_with_hint(collection, filter, var_name, ctx, limit, hint) {
return Ok(Some(hit));
}
if hint.force {
return Err(DbError::ExecutionError(format!(
"could not use index hint to serve query: none of [{}] on '{}' can serve the \
FILTER (forceIndexHint is set)",
hint.names.join(", "),
collection
.name
.rsplit(':')
.next()
.unwrap_or(&collection.name)
)));
}
Ok(self.lookup_index_for_filter_limited(collection, filter, var_name, ctx, limit))
}
fn lookup_with_hint(
&self,
collection: &Collection,
filter: &Expression,
var_name: &str,
ctx: &Context,
limit: Option<usize>,
hint: &IndexHint,
) -> Option<(Vec<crate::storage::Document>, String, String)> {
let indexes = collection.get_all_indexes();
let candidates = self.ordered_index_candidates(filter, var_name, ctx);
for name in &hint.names {
if let Some(index) = indexes.iter().find(|i| i.name == *name) {
let type_str = format!("{:?}", index.index_type);
if index.fields.len() == 1 {
for cond in candidates.iter().filter(|c| c.field == index.fields[0]) {
if let Some(docs) = self.use_index_for_condition(collection, cond, limit) {
return Some((docs, index.name.clone(), type_str));
}
}
} else {
let pairs: Vec<(String, Value)> = self
.extract_equality_conditions(filter, var_name, ctx)
.into_iter()
.map(|c| (c.field, c.value))
.collect();
if let Some((used, docs)) = collection.index_lookup_eq_composite(&pairs) {
if used.name == *name {
return Some((docs, used.name, type_str));
}
}
}
} else if let Some(hit) =
self.geo_lookup_for_filter(collection, filter, var_name, ctx, Some(name), limit)
{
return Some(hit);
}
}
None
}
pub(super) fn filter_fully_covered_by_index(
&self,
expr: &Expression,
var_name: &str,
ctx: &Context,
) -> bool {
match expr {
Expression::BinaryOp { op, .. } => {
matches!(
op,
BinaryOperator::Equal
| BinaryOperator::LessThan
| BinaryOperator::LessThanOrEqual
| BinaryOperator::GreaterThan
| BinaryOperator::GreaterThanOrEqual
| BinaryOperator::In
) && self
.extract_indexable_condition(expr, var_name, ctx)
.is_some()
}
_ => false,
}
}
pub(super) fn extract_equi_join_term<'e>(
&self,
condition: &'e Expression,
var_name: &str,
) -> Option<(String, &'e Expression, bool)> {
match condition {
Expression::BinaryOp {
left,
op: BinaryOperator::Equal,
right,
} => {
if let Some(field) = self.extract_field_path(left, var_name) {
if !expression_references_var(right, var_name) {
return Some((field, right.as_ref(), true));
}
}
if let Some(field) = self.extract_field_path(right, var_name) {
if !expression_references_var(left, var_name) {
return Some((field, left.as_ref(), true));
}
}
None
}
Expression::BinaryOp {
left,
op: BinaryOperator::And,
right,
} => self
.extract_equi_join_term(left, var_name)
.or_else(|| self.extract_equi_join_term(right, var_name))
.map(|(field, expr, _)| (field, expr, false)),
_ => None,
}
}
pub(super) fn extract_indexable_value(
&self,
expr: &Expression,
var_name: &str,
ctx: &Context,
) -> Option<Value> {
match expr {
Expression::Literal(v) => Some(v.clone()),
Expression::BindVariable(name) => self.bind_vars.get(name).cloned(),
_ => {
if expression_references_var(expr, var_name) {
return None;
}
self.evaluate_expr_with_context(expr, ctx).ok()
}
}
}
#[allow(clippy::only_used_in_recursion)]
pub(super) fn extract_field_path(&self, expr: &Expression, var_name: &str) -> Option<String> {
match expr {
Expression::FieldAccess(base, field) => {
if let Expression::Variable(name) = base.as_ref() {
if name == var_name {
return Some(field.clone());
}
}
if let Some(base_path) = self.extract_field_path(base, var_name) {
return Some(format!("{}.{}", base_path, field));
}
None
}
_ => None,
}
}
pub(super) fn extract_vector_arg(value: &Value, context: &str) -> DbResult<Vec<f32>> {
match value {
Value::Array(arr) => arr
.iter()
.map(|v| {
v.as_f64().map(|f| f as f32).ok_or_else(|| {
DbError::ExecutionError(format!("{} must be an array of numbers", context))
})
})
.collect(),
_ => Err(DbError::ExecutionError(format!(
"{} must be an array",
context
))),
}
}
pub(super) fn use_index_for_condition(
&self,
collection: &Collection,
condition: &IndexableCondition,
limit: Option<usize>,
) -> Option<Vec<crate::storage::Document>> {
if condition.field == "_key" {
return self.key_fast_path(collection, condition);
}
let normalized_value = if let Value::Number(n) = &condition.value {
if let Some(f) = n.as_f64() {
if f.fract() == 0.0 && f.is_finite() {
Value::Number(serde_json::Number::from(f as i64))
} else {
condition.value.clone()
}
} else {
condition.value.clone()
}
} else {
condition.value.clone()
};
match condition.op {
BinaryOperator::Equal => {
if let Some(k) = limit {
if let Some(docs) =
collection.index_lookup_eq_limit(&condition.field, &normalized_value, k)
{
if !docs.is_empty() {
return Some(docs);
}
}
return collection.index_lookup_eq_limit(&condition.field, &condition.value, k);
}
if let Some(docs) = collection.index_lookup_eq(&condition.field, &normalized_value)
{
if !docs.is_empty() {
return Some(docs);
}
}
collection.index_lookup_eq(&condition.field, &condition.value)
}
BinaryOperator::GreaterThan => collection.index_lookup_gt(
&condition.field,
&normalized_value,
limit.or(self.scan_cap()),
),
BinaryOperator::GreaterThanOrEqual => collection.index_lookup_gte(
&condition.field,
&normalized_value,
limit.or(self.scan_cap()),
),
BinaryOperator::LessThan => collection.index_lookup_lt(
&condition.field,
&normalized_value,
limit.or(self.scan_cap()),
),
BinaryOperator::LessThanOrEqual => collection.index_lookup_lte(
&condition.field,
&normalized_value,
limit.or(self.scan_cap()),
),
BinaryOperator::In => self.index_lookup_in(collection, condition, limit),
BinaryOperator::Like => {
let prefix = condition.value.as_str()?;
let cap = limit.unwrap_or(self.max_intermediate_rows().saturating_add(1));
index_prefix_scan(collection, &condition.field, prefix, cap)
}
_ => None,
}
}
fn index_lookup_in(
&self,
collection: &Collection,
condition: &IndexableCondition,
limit: Option<usize>,
) -> Option<Vec<crate::storage::Document>> {
let Value::Array(items) = &condition.value else {
return None;
};
regular_index_on(collection, &condition.field)?;
let mut seen: HashSet<Vec<u8>> = HashSet::with_capacity(items.len());
let mut out = Vec::new();
for item in items {
if !seen.insert(crate::storage::codec::encode_key(item)) {
continue;
}
let docs = match limit {
Some(k) => {
let remaining = k.saturating_sub(out.len());
if remaining == 0 {
break;
}
collection.index_lookup_eq_limit(&condition.field, item, remaining)?
}
None => collection.index_lookup_eq(&condition.field, item)?,
};
out.extend(docs);
if out.len() > self.max_intermediate_rows() {
break; }
}
Some(out)
}
fn key_fast_path(
&self,
collection: &Collection,
condition: &IndexableCondition,
) -> Option<Vec<crate::storage::Document>> {
if let (BinaryOperator::In, Value::Array(keys)) = (&condition.op, &condition.value) {
let mut seen: HashSet<&str> = HashSet::with_capacity(keys.len());
let mut out = Vec::new();
for key in keys.iter().filter_map(Value::as_str) {
if !seen.insert(key) {
continue;
}
match collection.get(key) {
Ok(doc) => out.push(doc),
Err(DbError::DocumentNotFound(_)) => {}
Err(_) => return None,
}
}
return Some(out);
}
if !matches!(condition.op, BinaryOperator::Equal) {
return None;
}
let Some(key) = condition.value.as_str() else {
return Some(Vec::new());
};
match collection.get(key) {
Ok(doc) => Some(vec![doc]),
Err(DbError::DocumentNotFound(_)) => Some(Vec::new()),
Err(_) => None,
}
}
pub(super) fn would_auto_index(
&self,
collection: &Collection,
field: &str,
value: Option<&Value>,
) -> bool {
if value.is_some_and(Value::is_null) {
return false;
}
if !field_is_auto_indexable(field) {
return false;
}
match &self.principal {
Some(p) if p.can_write || p.can_admin => {}
_ => return false,
}
if crate::storage::is_protected_collection(&collection.name)
|| collection_bare_name(&collection.name).starts_with('_')
{
return false;
}
if !collection.auto_index_enabled() {
return false;
}
if collection
.get_shard_config()
.is_some_and(|c| c.num_shards > 0)
{
return false;
}
let max_docs = auto_index_max_docs();
if max_docs > 0 && collection.count() > max_docs {
return false;
}
let indexes = collection.get_all_indexes();
if indexes
.iter()
.any(|i| i.fields.len() == 1 && i.fields[0] == field)
{
return false;
}
indexes.iter().filter(|i| is_auto_index(i)).count() < AUTO_INDEX_CAP
}
pub(super) fn maybe_auto_index(
&self,
collection: &Collection,
field: &str,
value: Option<&Value>,
) -> bool {
if !self.would_auto_index(collection, field, value) {
return false;
}
let name = auto_index_name(field);
let spec = IndexSpec::Regular {
name: name.clone(),
fields: vec![field.to_string()],
index_type: IndexType::Persistent,
unique: false,
};
match collection.create_index(
name.clone(),
vec![field.to_string()],
IndexType::Persistent,
false,
) {
Ok(stats) if stats.indexed_documents == 0 => {
if let Err(e) = collection.drop_index(&name) {
tracing::warn!(
collection = %collection.name,
field,
error = %e,
"could not drop the empty auto-index"
);
}
false
}
Ok(_) => {
self.propagate_auto_index(collection, &spec);
true
}
Err(e) => {
tracing::warn!(
collection = %collection.name,
field,
error = %e,
"auto-index create failed; falling back to scan"
);
false
}
}
}
fn propagate_auto_index(&self, collection: &Collection, spec: &IndexSpec) {
let Some(db) = self.database.as_deref() else {
return;
};
if let Some(repl) = self.replication {
let payload = serde_json::to_vec(spec).ok();
let target = collection_bare_name(&collection.name).to_string();
repl.append(crate::sync::log::LogEntry::new_op(
db,
target,
crate::sync::protocol::Operation::CreateIndex,
spec.name().to_string(),
payload,
));
}
}
}
pub(super) fn expression_references_var(expr: &Expression, var_name: &str) -> bool {
match expr {
Expression::Variable(name) => name == var_name,
Expression::BindVariable(_) | Expression::Literal(_) => false,
Expression::FieldAccess(base, _) | Expression::OptionalFieldAccess(base, _) => {
expression_references_var(base, var_name)
}
Expression::DynamicFieldAccess(base, key) => {
expression_references_var(base, var_name) || expression_references_var(key, var_name)
}
Expression::ArrayAccess(base, idx) => {
expression_references_var(base, var_name) || expression_references_var(idx, var_name)
}
Expression::ArraySpreadAccess(base, _) => expression_references_var(base, var_name),
Expression::BinaryOp { left, right, .. } => {
expression_references_var(left, var_name) || expression_references_var(right, var_name)
}
Expression::UnaryOp { operand, .. } => expression_references_var(operand, var_name),
Expression::Object(fields) => fields
.iter()
.any(|(_, e)| expression_references_var(e, var_name)),
Expression::Array(items) => items.iter().any(|e| expression_references_var(e, var_name)),
Expression::Range(a, b) => {
expression_references_var(a, var_name) || expression_references_var(b, var_name)
}
Expression::FunctionCall { args, .. } => {
args.iter().any(|e| expression_references_var(e, var_name))
}
Expression::Subquery(_) => {
true
}
Expression::Ternary {
condition,
true_expr,
false_expr,
} => {
expression_references_var(condition, var_name)
|| expression_references_var(true_expr, var_name)
|| expression_references_var(false_expr, var_name)
}
Expression::Case {
operand,
when_clauses,
else_clause,
} => {
operand
.as_deref()
.is_some_and(|e| expression_references_var(e, var_name))
|| when_clauses.iter().any(|(c, r)| {
expression_references_var(c, var_name) || expression_references_var(r, var_name)
})
|| else_clause
.as_deref()
.is_some_and(|e| expression_references_var(e, var_name))
}
Expression::Pipeline { left, right } => {
expression_references_var(left, var_name) || expression_references_var(right, var_name)
}
Expression::Lambda { body, .. } => expression_references_var(body, var_name),
Expression::ArrayComparison {
quantifier,
left,
right,
..
} => {
expression_references_var(left, var_name)
|| expression_references_var(right, var_name)
|| matches!(quantifier, crate::sdbql::ast::ArrayQuantifier::AtLeast(n)
if expression_references_var(n, var_name))
}
Expression::ArrayInline {
base,
filter,
limit,
projection,
..
} => {
expression_references_var(base, var_name)
|| filter
.as_deref()
.is_some_and(|e| expression_references_var(e, var_name))
|| limit.as_ref().is_some_and(|(o, c)| {
expression_references_var(o, var_name) || expression_references_var(c, var_name)
})
|| projection
.as_deref()
.is_some_and(|e| expression_references_var(e, var_name))
}
Expression::WindowFunctionCall {
arguments,
over_clause,
..
} => {
arguments
.iter()
.any(|e| expression_references_var(e, var_name))
|| over_clause
.partition_by
.iter()
.any(|e| expression_references_var(e, var_name))
|| over_clause
.order_by
.iter()
.any(|(e, _)| expression_references_var(e, var_name))
}
Expression::TemplateString { parts } => parts.iter().any(|p| match p {
TemplateStringPart::Expression(e) => expression_references_var(e, var_name),
TemplateStringPart::Literal(_) => false,
}),
}
}
#[cfg(test)]
mod optimizer_helper_tests {
use super::*;
use serde_json::json;
#[test]
fn like_prefix_stops_at_wildcards_and_escapes() {
assert_eq!(like_literal_prefix("abc%"), Some("abc".to_string()));
assert_eq!(like_literal_prefix("ab_c%"), Some("ab".to_string()));
assert_eq!(like_literal_prefix("ab\\%c"), Some("ab".to_string()));
assert_eq!(like_literal_prefix("exact"), Some("exact".to_string()));
assert_eq!(like_literal_prefix("%abc"), None);
assert_eq!(like_literal_prefix(""), None);
}
#[test]
fn prefix_successor_bounds_every_extension() {
assert_eq!(prefix_successor("abc").as_deref(), Some("abd"));
assert_eq!(prefix_successor("a\u{10FFFF}").as_deref(), Some("b"));
assert_eq!(prefix_successor("\u{D7FF}").as_deref(), Some("\u{E000}"));
assert_eq!(prefix_successor("\u{10FFFF}"), None);
let upper = prefix_successor("User1").unwrap();
for s in ["User1", "User1\u{10FFFF}z", "User19"] {
assert!(s >= "User1" && s < upper.as_str(), "{s}");
}
assert!("User2" >= upper.as_str());
}
#[test]
fn in_lists_with_null_or_too_many_keys_are_not_indexed() {
assert!(in_list_is_indexable(&json!([1, "a"])));
assert!(in_list_is_indexable(&json!([])));
assert!(!in_list_is_indexable(&json!([1, null])));
assert!(!in_list_is_indexable(&json!({"a": 1})));
let long: Vec<Value> = (0..=MAX_INDEXED_IN_KEYS).map(|i| json!(i)).collect();
assert!(!in_list_is_indexable(&Value::Array(long)));
}
#[test]
fn index_hint_from_options() {
assert_eq!(IndexHint::from_options(None, true), None);
assert_eq!(IndexHint::from_options(Some(&[][..]), false), None);
let names = vec!["a".to_string()];
assert_eq!(
IndexHint::from_options(Some(names.as_slice()), true),
Some(IndexHint {
names: names.clone(),
force: true
})
);
}
}