use rushdb_lmpg::index;
use rushdb_lmpg::reader::TryRead;
use rushdb_types::encoding::{decode_order_preserving, encode_order_preserving, logical_tag};
use rushdb_types::ids::TenantId;
use rushdb_types::index::IndexDef;
use rushdb_types::model::LogicalType;
use rushdb_types::value::{normalize_name, Scalar};
use crate::planner::{range_scalar_matches, scalar_variants, BoundedOwners, OwnerSet, RangeOp};
use crate::{Expr, FieldPred, QueryError};
type Json = serde_json::Value;
pub(crate) fn covering<R: TryRead>(
snap: &R,
tenant: TenantId,
name: &str,
labels: &[String],
) -> Result<Option<Vec<IndexDef>>, QueryError> {
let norm = normalize_name(name);
if labels.is_empty() || norm.is_empty() || rushdb_types::value::is_reserved_name(&norm) {
return Ok(None);
}
let mut out = Vec::with_capacity(labels.len());
for label in labels {
match index::ready_ordered(snap, tenant, label, &norm)? {
Some(def) => out.push(def),
None => return Ok(None),
}
}
Ok(Some(out))
}
fn prefix_bounds(prefix: Vec<u8>) -> (Vec<u8>, Vec<u8>) {
let end = rushdb_storage::prefix_range(&prefix).1;
(prefix, end)
}
fn scan<R: TryRead>(
snap: &R,
tenant: TenantId,
defs: &[IndexDef],
(start, end): (&[u8], &[u8]),
keep: &dyn Fn(&[u8]) -> bool,
owners: &mut OwnerSet,
owner_limit: usize,
) -> Result<(), QueryError> {
let mut over_budget = None;
let mut owners = BoundedOwners::new(owners, owner_limit);
for def in defs {
index::for_each_entry(snap, tenant, def.id, start, end, &mut |entry, owner| {
if !keep(entry) {
return true;
}
match owners.insert(owner) {
Ok(()) => true,
Err(error) => {
over_budget = Some(error);
false
}
}
})?;
if let Some(error) = over_budget {
return Err(error);
}
}
Ok(())
}
fn entry_of(ty: LogicalType, scalar: &Scalar) -> Option<Vec<u8>> {
let mut entry = vec![logical_tag(ty)];
entry.extend(encode_order_preserving(ty, scalar).ok()?);
Some(entry)
}
fn equal<R: TryRead>(
snap: &R,
tenant: TenantId,
defs: &[IndexDef],
values: &[Json],
owner_limit: usize,
) -> Result<Option<OwnerSet>, QueryError> {
let mut owners = OwnerSet::new();
for value in values {
let Some(variants) = scalar_variants(value) else {
return Ok(None);
};
for (ty, scalar) in variants {
let Some(entry) = entry_of(ty, &scalar) else {
return Ok(None);
};
let (start, end) = prefix_bounds(entry.clone());
scan(
snap,
tenant,
defs,
(&start, &end),
&|found| found == entry.as_slice(),
&mut owners,
owner_limit,
)?;
}
}
Ok(Some(owners))
}
fn range<R: TryRead>(
snap: &R,
tenant: TenantId,
defs: &[IndexDef],
op: RangeOp,
bound: f64,
owner_limit: usize,
) -> Result<Option<OwnerSet>, QueryError> {
let mut owners = OwnerSet::new();
let above = matches!(op, RangeOp::Gt | RangeOp::Gte);
for ty in [
LogicalType::Int64,
LogicalType::Float64,
LogicalType::DateTime,
] {
let tag = logical_tag(ty);
let edge = match ty {
LogicalType::Float64 => Scalar::Float64(bound),
LogicalType::Int64 => Scalar::Int64(outward(bound, above)),
_ => Scalar::DateTime(outward(bound, above)),
};
let Some(edge) = entry_of(ty, &edge) else {
continue;
};
let (start, end) = if above {
(edge, vec![tag + 1])
} else {
(vec![tag], rushdb_storage::prefix_range(&edge).1)
};
scan(
snap,
tenant,
defs,
(&start, &end),
&|entry| {
entry.split_first().is_some_and(|(&found, payload)| {
found == tag
&& decode_order_preserving(ty, payload)
.is_ok_and(|scalar| range_scalar_matches(&scalar, op, bound))
})
},
&mut owners,
owner_limit,
)?;
}
Ok(Some(owners))
}
#[expect(
clippy::cast_possible_truncation,
reason = "floor/ceil made the bound integral; `as` saturates at the i64 edges"
)]
fn outward(bound: f64, above: bool) -> i64 {
if above {
bound.floor() as i64
} else {
bound.ceil() as i64
}
}
fn starts_with<R: TryRead>(
snap: &R,
tenant: TenantId,
defs: &[IndexDef],
prefix: &str,
owner_limit: usize,
) -> Result<Option<OwnerSet>, QueryError> {
let mut entry = vec![logical_tag(LogicalType::Utf8)];
entry.extend_from_slice(prefix.as_bytes());
let (start, end) = prefix_bounds(entry);
let mut owners = OwnerSet::new();
scan(
snap,
tenant,
defs,
(&start, &end),
&|entry| {
decode_order_preserving(LogicalType::Utf8, &entry[1..]).is_ok_and(
|scalar| matches!(scalar, Scalar::Utf8(text) if text.starts_with(prefix)),
)
},
&mut owners,
owner_limit,
)?;
Ok(Some(owners))
}
fn indexed_property(expr: &Expr) -> Option<&str> {
match expr {
Expr::Field(
FieldPred::Eq(name, _)
| FieldPred::In(name, _)
| FieldPred::Gt(name, _)
| FieldPred::Gte(name, _)
| FieldPred::Lt(name, _)
| FieldPred::Lte(name, _)
| FieldPred::StartsWith(name, _),
) => Some(name),
_ => None,
}
}
pub(crate) fn leaf_owners<R: TryRead>(
snap: &R,
tenant: TenantId,
expr: &Expr,
labels: &[String],
owner_limit: usize,
) -> Result<Option<OwnerSet>, QueryError> {
let Some(name) = indexed_property(expr) else {
return Ok(None);
};
let Some(defs) = covering(snap, tenant, name, labels)? else {
return Ok(None);
};
let cache = snap.owner_sets();
let ids: Vec<u32> = defs.iter().map(|def| def.id).collect();
let key = format!("{tenant:?}|{ids:?}|{expr:?}").into_bytes();
if let Some(hit) = cache
.and_then(|(sets, ci)| sets.get(&key, ci))
.and_then(|set| set.downcast::<OwnerSet>().ok())
{
if hit.len() > u64::try_from(owner_limit).unwrap_or(u64::MAX) {
return Err(QueryError::ResourceLimit(
"aggregate candidate budget exceeded".into(),
));
}
return Ok(Some(OwnerSet::clone(&hit)));
}
let owners = leaf_owners_uncached(snap, tenant, expr, &defs, owner_limit)?;
if let (Some(owners), Some((sets, ci))) = (owners.as_ref(), cache) {
sets.insert(
key,
ci,
std::sync::Arc::new(owners.clone()),
owners.serialized_size(),
);
}
Ok(owners)
}
fn leaf_owners_uncached<R: TryRead>(
snap: &R,
tenant: TenantId,
expr: &Expr,
defs: &[IndexDef],
owner_limit: usize,
) -> Result<Option<OwnerSet>, QueryError> {
let defs = defs.to_vec();
match expr {
Expr::Field(FieldPred::Eq(_, value)) if !value.is_null() => equal(
snap,
tenant,
&defs,
std::slice::from_ref(value),
owner_limit,
),
Expr::Field(FieldPred::In(_, values)) if values.iter().all(|v| !v.is_null()) => {
equal(snap, tenant, &defs, values, owner_limit)
}
Expr::Field(FieldPred::Gt(_, bound)) => {
range(snap, tenant, &defs, RangeOp::Gt, *bound, owner_limit)
}
Expr::Field(FieldPred::Gte(_, bound)) => {
range(snap, tenant, &defs, RangeOp::Gte, *bound, owner_limit)
}
Expr::Field(FieldPred::Lt(_, bound)) => {
range(snap, tenant, &defs, RangeOp::Lt, *bound, owner_limit)
}
Expr::Field(FieldPred::Lte(_, bound)) => {
range(snap, tenant, &defs, RangeOp::Lte, *bound, owner_limit)
}
Expr::Field(FieldPred::StartsWith(_, prefix)) => {
starts_with(snap, tenant, &defs, prefix, owner_limit)
}
_ => Ok(None),
}
}
pub(crate) fn answers<R: TryRead>(
snap: &R,
tenant: TenantId,
expr: &Expr,
labels: &[String],
) -> Result<bool, QueryError> {
let Some(name) = indexed_property(expr) else {
return Ok(false);
};
Ok(covering(snap, tenant, name, labels)?.is_some())
}
pub fn explain_index<R: TryRead>(
snap: &R,
tenant: TenantId,
labels: &[String],
where_: &Expr,
) -> Result<Option<String>, QueryError> {
let mut leaves = Vec::new();
crate::planner::collect_leaves(where_, &mut leaves);
for leaf in leaves {
if let Some(name) = indexed_property(leaf) {
if let Some(defs) = covering(snap, tenant, name, labels)? {
return Ok(Some(
defs.iter()
.map(|def| def.name.as_str())
.collect::<Vec<_>>()
.join(","),
));
}
}
}
Ok(None)
}