#[cfg(test)]
mod admission_tests;
#[cfg(test)]
pub(super) mod prefix_tests;
use crate::{
db::{
access::{
AccessPath, AccessPlan, SemanticIndexAccessContract, SemanticIndexKeyItemRef,
SemanticIndexRangeSpec,
},
index::{TextPrefixBoundMode, admit_text_prefix_bounds, starts_with_component_bounds},
predicate::{CoercionId, CompareOp, ComparePredicate},
query::construction::ConstructionBudget,
query::plan::{
OrderSpec, field_key_contract_supports_operator,
key_item_match::{
copy_lookup_value_for_key_item, key_item_supports_lookup_value,
key_item_supports_starts_with_value,
},
order_contract::CandidateOrderContract,
planner::{
AccessCandidateScore, access_candidate_score_from_index_contract,
access_candidate_score_outranks, index_literal_matches_schema,
prefix::{index_multi_lookup_for_in, index_prefix_for_eq},
range_bound_count,
},
},
schema::{FieldType, SchemaInfo, literal_matches_type},
},
error::InternalError,
value::Value,
};
use icydb_diagnostic_code::DiagnosticExecutionBudgetResource as Resource;
use std::ops::Bound;
pub(super) fn plan_compare(
candidate_indexes: &[SemanticIndexAccessContract],
schema: &SchemaInfo,
cmp: &ComparePredicate,
order: Option<&OrderSpec>,
grouped: bool,
budget: &dyn ConstructionBudget,
) -> Result<AccessPlan<Value>, InternalError> {
let primary_key_name = schema.scalar_primary_key_name();
if primary_key_exact_coercion_supports_access(cmp.coercion.id)
&& primary_key_name.is_some_and(|name| cmp.field == name)
&& let Some(field_type) = primary_key_name.and_then(|name| schema.field(name))
&& let Some(path) = plan_pk_compare(field_type, &cmp.value, cmp.op, budget)?
{
return Ok(path);
}
match cmp.op {
CompareOp::Eq => {
if !coercion_supports_index_lookup(cmp.coercion.id) {
return Ok(AccessPlan::full_scan());
}
if let Some(paths) =
index_prefix_for_eq(candidate_indexes, schema, cmp, order, grouped, budget)?
{
return Ok(paths);
}
}
CompareOp::In => {
if !coercion_supports_index_lookup(cmp.coercion.id) {
return Ok(AccessPlan::full_scan());
}
if let Value::List(items) = &cmp.value {
if items.is_empty() {
return Ok(AccessPlan::by_keys(Vec::new()));
}
if let Some(path) = index_multi_lookup_for_in(
candidate_indexes,
schema,
cmp,
items,
order,
grouped,
budget,
)? {
return Ok(path);
}
}
}
CompareOp::Gt | CompareOp::Gte | CompareOp::Lt | CompareOp::Lte => {
if !coercion_supports_index_lookup(cmp.coercion.id) {
return Ok(AccessPlan::full_scan());
}
let Some(field_type) = schema.field(&cmp.field) else {
return Ok(AccessPlan::full_scan());
};
if !field_supports_ordered_compare(field_type, cmp.coercion.id) {
return Ok(AccessPlan::full_scan());
}
if let Some(path) =
plan_ordered_compare(candidate_indexes, schema, cmp, order, grouped, budget)?
{
return Ok(path);
}
}
CompareOp::StartsWith => {
if !coercion_supports_index_lookup(cmp.coercion.id) {
return Ok(AccessPlan::full_scan());
}
if let Some(path) =
plan_starts_with_compare(candidate_indexes, schema, cmp, order, grouped, budget)?
{
return Ok(path);
}
}
_ => {
}
}
Ok(AccessPlan::full_scan())
}
const fn coercion_supports_index_lookup(coercion: CoercionId) -> bool {
matches!(coercion, CoercionId::Strict | CoercionId::TextCasefold)
}
const fn primary_key_exact_coercion_supports_access(coercion: CoercionId) -> bool {
matches!(coercion, CoercionId::Strict | CoercionId::NumericWiden)
}
const fn field_supports_ordered_compare(field_type: &FieldType, coercion: CoercionId) -> bool {
match coercion {
CoercionId::Strict => field_type.is_orderable(),
CoercionId::TextCasefold => field_type.is_text(),
_ => false,
}
}
fn plan_pk_compare(
field_type: &FieldType,
value: &Value,
op: CompareOp,
budget: &dyn ConstructionBudget,
) -> Result<Option<AccessPlan<Value>>, InternalError> {
if !field_type.is_keyable() {
return Ok(None);
}
let path = match op {
CompareOp::Eq => {
if !literal_matches_type(value, field_type) {
return Ok(None);
}
AccessPath::ByKey(budget.copy_value(value)?)
}
CompareOp::In => {
let Value::List(items) = value else {
return Ok(None);
};
for item in items {
if !literal_matches_type(item, field_type) {
return Ok(None);
}
}
let mut keys = budget.vec_with_capacity(items.len())?;
for item in items {
keys.push(budget.copy_value(item)?);
}
AccessPath::ByKeys(keys)
}
_ => {
return Ok(None);
}
};
Ok(Some(AccessPlan::Path(budget.boxed(path)?)))
}
fn plan_starts_with_compare(
candidate_indexes: &[SemanticIndexAccessContract],
schema: &SchemaInfo,
cmp: &ComparePredicate,
order: Option<&OrderSpec>,
grouped: bool,
budget: &dyn ConstructionBudget,
) -> Result<Option<AccessPlan<Value>>, InternalError> {
if !schema.field(&cmp.field).is_some_and(FieldType::is_text) {
return Ok(None);
}
let literal_compatible = index_literal_matches_schema(schema, &cmp.field, &cmp.value);
budget.charge(
Resource::PredicateExpressionSteps,
candidate_indexes.len() as u64,
)?;
let mut candidates = candidate_indexes.iter().filter_map(|index| {
let key = index.key_item_at(0)?;
key_item_supports_starts_with_value(
key,
cmp.field.as_str(),
&cmp.value,
cmp.coercion.id,
literal_compatible,
)
.then_some((index, key))
});
let Some(first) = candidates.next() else {
return Ok(None);
};
let Some(Value::Text(prefix)) = copy_lookup_value_for_key_item(
first.1,
&cmp.field,
&cmp.value,
cmp.coercion.id,
literal_compatible,
budget,
)?
else {
return Ok(None);
};
let mode = if first.1.is_expression() {
TextPrefixBoundMode::LowerOnly
} else {
TextPrefixBoundMode::Strict
};
admit_text_prefix_bounds(&prefix, mode, budget)?;
let Some((lower, upper)) = starts_with_component_bounds(&prefix, mode) else {
return Ok(None);
};
let mut best: Option<(AccessCandidateScore, &SemanticIndexAccessContract)> = None;
let order_contract = CandidateOrderContract::prepare(schema, order, grouped, budget)?;
for (index, _) in std::iter::once(first).chain(candidates) {
let score = access_candidate_score_from_index_contract(
order_contract.as_ref(),
index,
0,
false,
range_bound_count(&lower, &upper),
);
match best {
None => best = Some((score, index)),
Some((best_score, best_index))
if access_candidate_score_outranks(score, best_score, false)
|| (score == best_score && index.name() < best_index.name()) =>
{
best = Some((score, index));
}
_ => {}
}
}
let Some((_, index)) = best else {
return Ok(None);
};
let mut slots = budget.vec_with_capacity(1)?;
slots.push(0usize);
let spec = SemanticIndexRangeSpec::from_access_contract(
index.clone(),
slots,
Vec::new(),
lower,
upper,
);
Ok(Some(AccessPlan::Path(
budget.boxed(AccessPath::IndexRange { spec })?,
)))
}
fn plan_ordered_compare(
candidate_indexes: &[SemanticIndexAccessContract],
schema: &SchemaInfo,
cmp: &ComparePredicate,
order: Option<&OrderSpec>,
grouped: bool,
budget: &dyn ConstructionBudget,
) -> Result<Option<AccessPlan<Value>>, InternalError> {
let literal_compatible = index_literal_matches_schema(schema, &cmp.field, &cmp.value);
if !literal_compatible
|| !matches!(
cmp.op,
CompareOp::Gt | CompareOp::Gte | CompareOp::Lt | CompareOp::Lte
)
{
return Ok(None);
}
budget.charge(
Resource::PredicateExpressionSteps,
candidate_indexes.len() as u64,
)?;
let mut best: Option<(
AccessCandidateScore,
&SemanticIndexAccessContract,
SemanticIndexKeyItemRef<'_>,
)> = None;
let order_contract = CandidateOrderContract::prepare(schema, order, grouped, budget)?;
for index in candidate_indexes {
let Some(leading_key_item) = index.key_item_at(0) else {
continue;
};
if !key_item_supports_lookup_value(
leading_key_item,
cmp.field.as_str(),
&cmp.value,
cmp.coercion.id,
literal_compatible,
) {
continue;
}
if !leading_key_item.is_expression()
&& !field_key_contract_supports_operator(index, cmp.field.as_str(), cmp.op)
{
continue;
}
let score =
access_candidate_score_from_index_contract(order_contract.as_ref(), index, 0, false, 1);
match best {
None => best = Some((score, index, leading_key_item)),
Some((best_score, best_index, _))
if access_candidate_score_outranks(score, best_score, false)
|| (score == best_score && index.name() < best_index.name()) =>
{
best = Some((score, index, leading_key_item));
}
_ => {}
}
}
let Some((_, index, key)) = best else {
return Ok(None);
};
let Some(value) =
copy_lookup_value_for_key_item(key, &cmp.field, &cmp.value, cmp.coercion.id, true, budget)?
else {
return Ok(None);
};
let (lower, upper) = match cmp.op {
CompareOp::Gt => (Bound::Excluded(value), Bound::Unbounded),
CompareOp::Gte => (Bound::Included(value), Bound::Unbounded),
CompareOp::Lt => (Bound::Unbounded, Bound::Excluded(value)),
CompareOp::Lte => (Bound::Unbounded, Bound::Included(value)),
_ => return Ok(None),
};
let mut slots = budget.vec_with_capacity(1)?;
slots.push(0usize);
let spec = SemanticIndexRangeSpec::from_access_contract(
index.clone(),
slots,
Vec::new(),
lower,
upper,
);
Ok(Some(AccessPlan::Path(
budget.boxed(AccessPath::IndexRange { spec })?,
)))
}