use crate::{
db::{
access::{
AccessPlan, SemanticIndexAccessContract, SemanticIndexKeyItemRef,
SemanticIndexKeyItemsRef,
},
predicate::{CoercionId, CompareOp, Predicate},
query::plan::{
OrderSpec,
key_item_match::eq_lookup_value_for_key_item,
planner::{
AccessCandidateScore, access_candidate_score_from_index_contract,
access_candidate_score_outranks, index_literal_matches_schema,
},
},
schema::SchemaInfo,
},
model::{entity::EntityModel, index::IndexKeyItemsRef},
value::Value,
};
fn leading_index_prefix_lookup_value(
index_contract: &SemanticIndexAccessContract,
field: &str,
value: &Value,
coercion: CoercionId,
literal_compatible: bool,
) -> Option<Value> {
let key_item = index_contract.key_item_at(0)?;
eq_lookup_value_for_key_item(key_item, field, value, coercion, literal_compatible)
}
#[expect(
clippy::too_many_arguments,
reason = "planner prefix access keeps field/value/order inputs explicit at this boundary"
)]
pub(super) fn index_prefix_for_eq(
model: &EntityModel,
candidate_indexes: &[SemanticIndexAccessContract],
schema: &SchemaInfo,
field: &str,
value: &Value,
coercion: CoercionId,
order: Option<&OrderSpec>,
grouped: bool,
) -> Option<AccessPlan<Value>> {
let literal_compatible = index_literal_matches_schema(schema, field, value);
let mut best: Option<(AccessCandidateScore, SemanticIndexAccessContract, Value)> = None;
for index in candidate_indexes {
let Some(lookup_value) =
leading_index_prefix_lookup_value(index, field, value, coercion, literal_compatible)
else {
continue;
};
let score = access_candidate_score_from_index_contract(
model,
order,
index.clone(),
1,
index.key_arity() == 1,
0,
grouped,
);
match best {
None => best = Some((score, index.clone(), lookup_value)),
Some((best_score, best_index, _))
if access_candidate_score_outranks(score, best_score, true)
|| (score == best_score && index.name() < best_index.name()) =>
{
best = Some((score, index.clone(), lookup_value));
}
_ => {}
}
}
best.map(|(_, index, lookup_value)| {
AccessPlan::index_prefix_from_contract(index, vec![lookup_value])
})
}
pub(super) fn index_multi_lookup_for_in(
_model: &EntityModel,
candidate_indexes: &[SemanticIndexAccessContract],
schema: &SchemaInfo,
field: &str,
values: &[Value],
coercion: CoercionId,
) -> Option<Vec<AccessPlan<Value>>> {
let cached_values = values
.iter()
.map(|value| (value, index_literal_matches_schema(schema, field, value)))
.collect::<Vec<_>>();
let mut out = Vec::new();
for index in candidate_indexes {
let mut lookup_values = Vec::with_capacity(values.len());
for (value, literal_compatible) in &cached_values {
let Some(lookup_value) = leading_index_prefix_lookup_value(
index,
field,
value,
coercion,
*literal_compatible,
) else {
lookup_values.clear();
break;
};
lookup_values.push(lookup_value);
}
if lookup_values.is_empty() {
continue;
}
out.push(AccessPlan::index_multi_lookup_from_contract(
index.clone(),
lookup_values,
));
}
if out.is_empty() { None } else { Some(out) }
}
pub(super) fn index_prefix_from_and(
model: &EntityModel,
candidate_indexes: &[SemanticIndexAccessContract],
schema: &SchemaInfo,
children: &[Predicate],
order: Option<&OrderSpec>,
grouped: bool,
) -> Option<AccessPlan<Value>> {
let mut field_values = Vec::new();
for child in children {
let Predicate::Compare(cmp) = child else {
continue;
};
if cmp.op != CompareOp::Eq {
continue;
}
if !matches!(
cmp.coercion.id,
CoercionId::Strict | CoercionId::TextCasefold
) {
continue;
}
field_values.push(CachedEqLiteral {
field: cmp.field.as_str(),
value: &cmp.value,
coercion: cmp.coercion.id,
compatible: index_literal_matches_schema(schema, &cmp.field, &cmp.value),
});
}
let mut best: Option<(
AccessCandidateScore,
SemanticIndexAccessContract,
Vec<Value>,
)> = None;
for index in candidate_indexes {
let Some(prefix) = build_index_eq_prefix(index, &field_values) else {
continue;
};
if prefix.is_empty() {
continue;
}
let score = access_candidate_score_from_index_contract(
model,
order,
index.clone(),
prefix.len(),
prefix.len() == index.key_arity(),
0,
grouped,
);
match &best {
None => best = Some((score, index.clone(), prefix)),
Some((best_score, best_index, _))
if access_candidate_score_outranks(score, *best_score, true)
|| (score == *best_score && index.name() < best_index.name()) =>
{
best = Some((score, index.clone(), prefix));
}
Some(_) => {}
}
}
best.map(|(_, index, values)| AccessPlan::index_prefix_from_contract(index, values))
}
struct CachedEqLiteral<'a> {
field: &'a str,
value: &'a Value,
coercion: CoercionId,
compatible: bool,
}
fn build_index_eq_prefix(
index_contract: &SemanticIndexAccessContract,
field_values: &[CachedEqLiteral<'_>],
) -> Option<Vec<Value>> {
match index_contract.key_items() {
SemanticIndexKeyItemsRef::Fields(fields) => build_index_eq_prefix_for_items(
fields
.iter()
.map(|field| SemanticIndexKeyItemRef::Field(field.as_str())),
field_values,
),
SemanticIndexKeyItemsRef::Static(IndexKeyItemsRef::Fields(fields)) => {
build_index_eq_prefix_for_items(
fields.iter().copied().map(SemanticIndexKeyItemRef::Field),
field_values,
)
}
SemanticIndexKeyItemsRef::Static(IndexKeyItemsRef::Items(items)) => {
build_index_eq_prefix_for_items(items.iter().copied().map(Into::into), field_values)
}
}
}
fn build_index_eq_prefix_for_items<'a, I>(
key_items: I,
field_values: &[CachedEqLiteral<'_>],
) -> Option<Vec<Value>>
where
I: IntoIterator<Item = SemanticIndexKeyItemRef<'a>>,
{
let mut prefix = Vec::new();
for key_item in key_items {
let mut matched: Option<Value> = None;
for cached in field_values {
let Some(candidate) = eq_lookup_value_for_key_item(
key_item,
cached.field,
cached.value,
cached.coercion,
cached.compatible,
) else {
continue;
};
if let Some(existing) = &matched
&& existing != &candidate
{
return None;
}
matched = Some(candidate);
}
let Some(value) = matched else {
break;
};
prefix.push(value);
}
Some(prefix)
}