use super::{
access, canonical_id, hex_decode_16, normalize, scalar_equal, Expr, FieldPred, Json, Lmpg,
LogicalType, QueryError, QuerySnapshot, Scalar, SearchRequest, TypeState,
};
use rushdb_lmpg::reader::TryRead;
mod explain;
pub use explain::{explain_cardinality, explain_walk};
pub(super) struct IndexPlan {
pub(super) ords: OwnerSet,
pub(super) exact: bool,
pub(super) known_total: Option<u64>,
pub(super) residual: Option<Expr>,
}
const PLANNER_MATERIALIZE_CAP: u64 = 2_000_000;
pub const UNINDEXED_WALK_THRESHOLD: u64 = 10_000;
static UNINDEXED_WALKS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
#[must_use]
pub fn unindexed_walks_total() -> u64 {
UNINDEXED_WALKS.load(std::sync::atomic::Ordering::Relaxed)
}
const PLANNER_BOUNDED_PAGE_CAP: usize = 100_000;
#[expect(
clippy::too_many_lines,
reason = "the planner is a single cohesive access-path selection routine; splitting it would obscure the fall-through ordering"
)]
pub(super) fn index_candidates_with_budget(
lmpg: &Lmpg,
snap: &QuerySnapshot,
req: &SearchRequest,
budget: Option<usize>,
is_cancelled: &dyn Fn() -> bool,
) -> Result<Option<IndexPlan>, QueryError> {
let owner_limit = budget.map_or(usize::MAX, |bytes| bytes / OWNER_SET_BYTES_PER_OWNER);
if req.force_scan {
return Ok(None);
}
let Some(expr) = &req.where_ else {
return Ok(None);
};
let mut leaves: Vec<&Expr> = Vec::new();
collect_leaves(expr, &mut leaves);
if leaves.is_empty() {
return Ok(None);
}
let mut eligible: Vec<(u64, &Expr)> = Vec::new();
let mut residual: Vec<Expr> = Vec::new();
let mut has_residual = false;
let mut exactable = true;
for leaf in leaves {
if is_cancelled() {
return Err(QueryError::Cancelled);
}
if let Some(estimate) = leaf_estimate(snap, lmpg.tenant, leaf, &req.labels)? {
if leaf_is_null_only(leaf) {
residual.push(leaf.clone());
}
eligible.push((estimate, leaf));
} else {
residual.push(leaf.clone());
has_residual = true;
if leaf_is_null_only(leaf) {
exactable = false;
}
}
}
if eligible.is_empty() {
return Ok(None);
}
if !has_residual
&& exactable
&& req.related.is_empty()
&& req.order_by.is_empty()
&& eligible.len() == 1
{
if let Expr::Field(FieldPred::Exists(name, true)) = eligible[0].1 {
if let Some(limit) = req.pagination.limit {
let page = req
.pagination
.skip
.saturating_add(limit)
.min(u64::try_from(PLANNER_BOUNDED_PAGE_CAP).unwrap_or(u64::MAX));
let page = usize::try_from(page).unwrap_or(PLANNER_BOUNDED_PAGE_CAP);
if let Some(total) = total_support(snap, lmpg.tenant, name, &req.labels)? {
let ords =
owners_with_presence_bounded(snap, lmpg.tenant, name, &req.labels, page)?;
return Ok(Some(IndexPlan {
ords: ords.into_iter().collect(),
exact: true,
known_total: Some(total),
residual: None,
}));
}
}
}
}
eligible.sort_by_key(|(estimate, _)| *estimate);
if eligible[0].0 != u64::MAX
&& eligible[0].0 > PLANNER_MATERIALIZE_CAP
&& !crate::index_access::answers(snap, lmpg.tenant, eligible[0].1, &req.labels)?
{
return Ok(None);
}
let label_scoped = req.labels.is_empty()
|| eligible
.iter()
.any(|(_, leaf)| !is_id_leaf(leaf) && !matches!(leaf, Expr::Or(_)));
let mut acc: Option<OwnerSet> = None;
for (_, leaf) in eligible {
if is_cancelled() {
return Err(QueryError::Cancelled);
}
if acc.as_ref().is_some_and(OwnerSet::is_empty) {
break;
}
let scope = LeafScope {
labels: &req.labels,
owner_limit,
candidates: acc.as_ref(),
};
let Some(owners) = leaf_owners(snap, lmpg.tenant, leaf, &scope)? else {
return Ok(None);
};
acc = Some(match acc {
None => owners,
Some(prev) => prev & owners,
});
}
let mut out = acc.unwrap_or_default();
if !label_scoped {
let mut outside = Vec::new();
for ord in &out {
if is_cancelled() {
return Err(QueryError::Cancelled);
}
let mut allowed = false;
for label in &req.labels {
let key = rushdb_types::keys::label_member_key(lmpg.tenant, label, ord);
if snap.get(&key)?.is_some() {
allowed = true;
break;
}
}
if !allowed {
outside.push(ord);
}
}
for ord in outside {
out.remove(ord);
}
}
let exact = !has_residual && exactable && req.related.is_empty();
Ok(Some(IndexPlan {
ords: out,
exact,
known_total: None,
residual: match residual.len() {
0 => None,
1 => residual.pop(),
_ => Some(Expr::And(residual)),
},
}))
}
pub(super) fn collect_leaves<'a>(expr: &'a Expr, out: &mut Vec<&'a Expr>) {
match expr {
Expr::And(items) => {
for item in items {
collect_leaves(item, out);
}
}
_ => out.push(expr),
}
}
fn leaf_is_null_only(expr: &Expr) -> bool {
matches!(expr, Expr::Field(FieldPred::Eq(_, value)) if value.is_null())
|| matches!(expr, Expr::Field(FieldPred::IsNull(_, true)))
}
fn leaf_estimate<R: TryRead>(
snap: &R,
tenant: rushdb_types::ids::TenantId,
expr: &Expr,
labels: &[String],
) -> Result<Option<u64>, QueryError> {
if matches!(expr, Expr::Field(FieldPred::StartsWith(..))) {
return Ok(crate::index_access::answers(snap, tenant, expr, labels)?.then_some(u64::MAX));
}
Ok(match expr {
Expr::Field(FieldPred::Eq(_, value)) if value.is_null() => Some(u64::MAX),
Expr::Field(FieldPred::Eq(name, value)) => {
estimate_values(snap, tenant, name, std::slice::from_ref(value), labels)?
}
Expr::Field(FieldPred::In(name, values)) if values.iter().all(|v| !v.is_null()) => {
estimate_values(snap, tenant, name, values, labels)?
}
Expr::Field(FieldPred::Id(_)) => Some(1),
Expr::Or(items) if !items.is_empty() && items.iter().all(is_id_leaf) => {
Some(items.len() as u64)
}
Expr::Field(FieldPred::IsNull(_, true) | FieldPred::Exists(_, true)) => Some(u64::MAX),
Expr::Field(FieldPred::Gt(name, bound)) => {
range_leaf_estimate(snap, tenant, name, RangeOp::Gt, *bound)?
}
Expr::Field(FieldPred::Gte(name, bound)) => {
range_leaf_estimate(snap, tenant, name, RangeOp::Gte, *bound)?
}
Expr::Field(FieldPred::Lt(name, bound)) => {
range_leaf_estimate(snap, tenant, name, RangeOp::Lt, *bound)?
}
Expr::Field(FieldPred::Lte(name, bound)) => {
range_leaf_estimate(snap, tenant, name, RangeOp::Lte, *bound)?
}
_ => None,
})
}
fn is_range_leaf(expr: &Expr) -> bool {
matches!(
expr,
Expr::Field(
FieldPred::Gt(..) | FieldPred::Gte(..) | FieldPred::Lt(..) | FieldPred::Lte(..)
)
)
}
fn range_leaf_histogram_estimate<R: TryRead>(
snap: &R,
tenant: rushdb_types::ids::TenantId,
name: &str,
op: RangeOp,
bound: f64,
) -> Result<Option<(u64, u64)>, QueryError> {
let norm = normalize(name);
if norm.is_empty() || rushdb_types::value::is_reserved_name(&norm) {
return Ok(None);
}
let Some(fid) = family_id(snap, tenant, &norm)? else {
return Ok(None);
};
let above = matches!(op, RangeOp::Gt | RangeOp::Gte);
for ty in [
LogicalType::Int64,
LogicalType::Float64,
LogicalType::DateTime,
] {
let Some(pid) = property_id(snap, tenant, fid, ty)? else {
continue;
};
let Some(hist) = crate::stats_cache::distribution(
snap,
tenant,
rushdb_types::ids::OwnerKind::Record,
None,
pid,
)?
else {
continue;
};
let scalar = match ty {
LogicalType::Float64 => Scalar::Float64(bound),
LogicalType::Int64 => Scalar::Int64(round_bound(bound, above)),
LogicalType::DateTime => Scalar::DateTime(round_bound(bound, above)),
LogicalType::Boolean | LogicalType::Utf8 => continue,
};
let Ok(enc) = rushdb_types::encoding::encode_order_preserving(ty, &scalar) else {
continue;
};
if !hist.partial && hist.distinct <= LOW_CARDINALITY_EXACT_THRESHOLD {
let exact = exact_low_cardinality_range_count(snap, tenant, pid, ty, op, bound)?;
return Ok(Some((exact, hist.total())));
}
return Ok(Some((hist.estimate(&enc, above), hist.total())));
}
Ok(None)
}
const LOW_CARDINALITY_EXACT_THRESHOLD: u64 = 256;
fn exact_low_cardinality_range_count<R: TryRead>(
snap: &R,
tenant: rushdb_types::ids::TenantId,
pid: u64,
ty: LogicalType,
op: RangeOp,
bound: f64,
) -> Result<u64, QueryError> {
let tag = rushdb_types::encoding::logical_tag(ty);
let mut total = 0u64;
for (entry, count) in rushdb_lmpg::derived::dictionary(
snap,
tenant,
rushdb_types::ids::OwnerKind::Record,
pid,
&[],
)? {
let Some((&entry_tag, payload)) = entry.split_first() else {
continue;
};
if entry_tag != tag {
continue;
}
let Ok(scalar) = rushdb_types::encoding::decode_order_preserving(ty, payload) else {
continue;
};
if range_scalar_matches(&scalar, op, bound) {
total = total.saturating_add(count);
}
}
Ok(total)
}
fn range_leaf_estimate<R: TryRead>(
snap: &R,
tenant: rushdb_types::ids::TenantId,
name: &str,
op: RangeOp,
bound: f64,
) -> Result<Option<u64>, QueryError> {
Ok(Some(
range_leaf_histogram_estimate(snap, tenant, name, op, bound)?
.map_or(u64::MAX, |(estimate, _total)| estimate),
))
}
#[expect(
clippy::cast_precision_loss,
reason = "estimate/total feed a selectivity fraction, not an exact count; this is \
approximate by design"
)]
pub fn explain_range_selectivity<R: TryRead>(
snap: &R,
tenant: rushdb_types::ids::TenantId,
where_: &Expr,
) -> Result<Option<f64>, QueryError> {
let mut leaves = Vec::new();
collect_leaves(where_, &mut leaves);
for leaf in leaves {
let Expr::Field(pred) = leaf else {
continue;
};
let (name, op, bound) = match pred {
FieldPred::Gt(name, bound) => (name, RangeOp::Gt, *bound),
FieldPred::Gte(name, bound) => (name, RangeOp::Gte, *bound),
FieldPred::Lt(name, bound) => (name, RangeOp::Lt, *bound),
FieldPred::Lte(name, bound) => (name, RangeOp::Lte, *bound),
_ => continue,
};
if let Some((estimate, total)) =
range_leaf_histogram_estimate(snap, tenant, name, op, bound)?
{
if total > 0 {
return Ok(Some((estimate as f64 / total as f64).clamp(0.0, 1.0)));
}
}
}
Ok(None)
}
pub(crate) type OwnerSet = roaring::RoaringTreemap;
pub(crate) const OWNER_SET_BYTES_PER_OWNER: usize = 4;
pub(super) fn extend_owners(
owners: &mut OwnerSet,
additions: impl IntoIterator<Item = u64>,
limit: usize,
) -> Result<(), QueryError> {
let mut owners = BoundedOwners::new(owners, limit);
for owner in additions {
owners.insert(owner)?;
}
Ok(())
}
pub(super) struct BoundedOwners<'a> {
owners: &'a mut OwnerSet,
len: u64,
limit: u64,
}
impl<'a> BoundedOwners<'a> {
pub(super) fn new(owners: &'a mut OwnerSet, limit: usize) -> Self {
let len = owners.len();
Self {
owners,
len,
limit: u64::try_from(limit).unwrap_or(u64::MAX),
}
}
pub(super) fn contains(&self, owner: u64) -> bool {
self.owners.contains(owner)
}
pub(super) fn insert(&mut self, owner: u64) -> Result<(), QueryError> {
if self.owners.contains(owner) {
return Ok(());
}
if self.len >= self.limit {
return Err(QueryError::ResourceLimit(
"aggregate candidate budget exceeded".into(),
));
}
self.owners.insert(owner);
self.len += 1;
Ok(())
}
}
fn leaf_owners<R: TryRead>(
snap: &R,
tenant: rushdb_types::ids::TenantId,
expr: &Expr,
scope: &LeafScope<'_>,
) -> Result<Option<OwnerSet>, QueryError> {
let (labels, owner_limit) = (scope.labels, scope.owner_limit);
if let Some(owners) = crate::index_access::leaf_owners(snap, tenant, expr, labels, owner_limit)?
{
return Ok(Some(owners));
}
match expr {
Expr::Field(FieldPred::Eq(name, value)) if value.is_null() => {
owners_with_presence(snap, tenant, name, true, labels, owner_limit)
}
Expr::Field(FieldPred::Eq(name, value)) => {
owners_for_values(snap, tenant, name, std::slice::from_ref(value), scope)
}
Expr::Field(FieldPred::In(name, values)) if values.iter().all(|v| !v.is_null()) => {
owners_for_values(snap, tenant, name, values, scope)
}
Expr::Field(FieldPred::Id(id)) => Ok(Some(owners_for_id(snap, tenant, id)?)),
Expr::Or(items) if !items.is_empty() && items.iter().all(is_id_leaf) => {
let mut out = OwnerSet::new();
for item in items {
if let Expr::Field(FieldPred::Id(id)) = item {
extend_owners(&mut out, owners_for_id(snap, tenant, id)?, owner_limit)?;
}
}
Ok(Some(out))
}
Expr::Field(FieldPred::Exists(name, true)) => {
owners_with_presence(snap, tenant, name, false, labels, owner_limit)
}
Expr::Field(FieldPred::IsNull(name, true)) => {
owners_with_presence(snap, tenant, name, true, labels, owner_limit)
}
Expr::Field(FieldPred::Gt(name, bound)) => {
owners_with_range(snap, tenant, name, RangeOp::Gt, *bound, scope)
}
Expr::Field(FieldPred::Gte(name, bound)) => {
owners_with_range(snap, tenant, name, RangeOp::Gte, *bound, scope)
}
Expr::Field(FieldPred::Lt(name, bound)) => {
owners_with_range(snap, tenant, name, RangeOp::Lt, *bound, scope)
}
Expr::Field(FieldPred::Lte(name, bound)) => {
owners_with_range(snap, tenant, name, RangeOp::Lte, *bound, scope)
}
_ => Ok(None),
}
}
fn is_id_leaf(expr: &Expr) -> bool {
matches!(expr, Expr::Field(FieldPred::Id(_)))
}
fn owners_for_id<R: TryRead>(
snap: &R,
tenant: rushdb_types::ids::TenantId,
id: &str,
) -> Result<OwnerSet, QueryError> {
let canonical = canonical_id(id);
let Some(bytes) = hex_decode_16(&canonical) else {
return Ok(OwnerSet::new());
};
let rid = rushdb_types::ids::RecordId(bytes);
Ok(access::record_by_id(snap, tenant, &rid)?
.map(|header| header.ordinal)
.into_iter()
.collect())
}
fn estimate_values<R: TryRead>(
snap: &R,
tenant: rushdb_types::ids::TenantId,
name: &str,
values: &[Json],
labels: &[String],
) -> Result<Option<u64>, QueryError> {
let norm = normalize(name);
if norm.is_empty() || rushdb_types::value::is_reserved_name(&norm) {
return Ok(None);
}
let Some(fid) = family_id(snap, tenant, &norm)? else {
return Ok(Some(0));
};
let Some((pids, encodings)) = value_probes(snap, tenant, fid, values)? else {
return Ok(None);
};
let mut total = 0u64;
for (ty, enc) in &encodings {
let mut entry = vec![rushdb_types::encoding::logical_tag(*ty)];
entry.extend_from_slice(enc);
if entry.len() > rushdb_types::keys::DICT_MAX_VALUE_BYTES {
return Ok(Some(u64::MAX));
}
for &pid in &pids {
for label in labels_for(snap, tenant, pid, labels)? {
total = total.saturating_add(rushdb_lmpg::derived::value_count(
snap,
tenant,
rushdb_types::ids::OwnerKind::Record,
&label,
pid,
&entry,
)?);
}
}
}
Ok(Some(total))
}
fn labels_for<R: TryRead>(
snap: &R,
tenant: rushdb_types::ids::TenantId,
pid: u64,
labels: &[String],
) -> Result<Vec<String>, QueryError> {
if labels.is_empty() {
Ok(rushdb_lmpg::derived::labels_of_pid(
snap,
tenant,
rushdb_types::ids::OwnerKind::Record,
pid,
)?)
} else {
Ok(labels.to_vec())
}
}
pub(super) struct LeafScope<'a> {
pub(super) labels: &'a [String],
pub(super) owner_limit: usize,
pub(super) candidates: Option<&'a OwnerSet>,
}
fn owners_matching<R: TryRead>(
snap: &R,
tenant: rushdb_types::ids::TenantId,
pid: u64,
scope: &LeafScope<'_>,
owners: &mut OwnerSet,
keep: &dyn Fn(&access::Occurrence) -> bool,
) -> Result<bool, QueryError> {
let kind = rushdb_types::ids::OwnerKind::Record;
let labels = labels_for(snap, tenant, pid, scope.labels)?;
let mut population = 0u64;
for label in &labels {
population = population.saturating_add(rushdb_lmpg::derived::prop_support_label(
snap, tenant, kind, pid, label,
)?);
}
let (_, array_labels) = rushdb_lmpg::derived::array_markers(snap, tenant, kind, pid)?;
let matches = |owner: u64, scalar: bool| -> Result<bool, QueryError> {
Ok(if scalar {
access::occurrence(snap, tenant, kind, owner, pid, 0)?.is_some_and(|occ| keep(&occ))
} else {
access::occurrences_of_pid(snap, tenant, kind, owner, pid)?
.iter()
.any(keep)
})
};
let mut owners = BoundedOwners::new(owners, scope.owner_limit);
if let Some(candidates) = scope.candidates.filter(|c| c.len() < population) {
let scalar = array_labels.is_empty();
for owner in candidates {
if !owners.contains(owner) && matches(owner, scalar)? {
owners.insert(owner)?;
}
}
return Ok(true);
}
if population > PLANNER_MATERIALIZE_CAP {
return Ok(false);
}
if population > UNINDEXED_WALK_THRESHOLD {
UNINDEXED_WALKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
for label in &labels {
let scalar = !array_labels.contains(label);
for owner in rushdb_lmpg::derived::presence_owners(
snap,
tenant,
kind,
label,
pid,
rushdb_types::keys::NULLNESS_VALUE,
)? {
if !owners.contains(owner) && matches(owner, scalar)? {
owners.insert(owner)?;
}
}
}
Ok(true)
}
fn owners_with_range<R: TryRead>(
snap: &R,
tenant: rushdb_types::ids::TenantId,
name: &str,
op: RangeOp,
bound: f64,
scope: &LeafScope<'_>,
) -> Result<Option<OwnerSet>, QueryError> {
let norm = normalize(name);
if norm.is_empty() || rushdb_types::value::is_reserved_name(&norm) {
return Ok(None);
}
let Some(fid) = family_id(snap, tenant, &norm)? else {
return Ok(Some(OwnerSet::new()));
};
let mut owners = OwnerSet::new();
for ty in [
LogicalType::Int64,
LogicalType::Float64,
LogicalType::DateTime,
] {
let Some(pid) = property_id(snap, tenant, fid, ty)? else {
continue;
};
let keep = |occ: &access::Occurrence| {
matches!(occ.value, rushdb_types::occurrence::OccurrenceValue::Stored { ty: stored } if stored == ty)
&& rushdb_types::encoding::decode_order_preserving(ty, &occ.payload)
.is_ok_and(|scalar| range_scalar_matches(&scalar, op, bound))
};
if !owners_matching(snap, tenant, pid, scope, &mut owners, &keep)? {
return Ok(None);
}
}
Ok(Some(owners))
}
#[derive(Clone, Copy)]
pub(super) enum RangeOp {
Gt,
Gte,
Lt,
Lte,
}
#[expect(
clippy::cast_possible_truncation,
reason = "floor/ceil already made the value integral; `as` saturates at the i64 edges"
)]
fn round_bound(bound: f64, lower: bool) -> i64 {
if lower {
bound.floor() as i64
} else {
bound.ceil() as i64
}
}
pub(super) fn range_scalar_matches(scalar: &Scalar, op: RangeOp, bound: f64) -> bool {
let scalar = match scalar {
Scalar::DateTime(value) => Scalar::Int64(*value),
other => other.clone(),
};
let Some(ordering) = rushdb_types::value::scalar_cmp(&scalar, &Scalar::Float64(bound)) else {
return false;
};
match op {
RangeOp::Gt => ordering == std::cmp::Ordering::Greater,
RangeOp::Gte => matches!(
ordering,
std::cmp::Ordering::Greater | std::cmp::Ordering::Equal
),
RangeOp::Lt => ordering == std::cmp::Ordering::Less,
RangeOp::Lte => matches!(
ordering,
std::cmp::Ordering::Less | std::cmp::Ordering::Equal
),
}
}
pub(super) fn family_id<R: TryRead>(
snap: &R,
tenant: rushdb_types::ids::TenantId,
normalized: &str,
) -> Result<Option<u64>, QueryError> {
use rushdb_lmpg::reader::keys_ext as kx;
let sig = kx::family_sig_key(
tenant,
"default",
rushdb_types::ids::OwnerKind::Record,
normalized,
);
let Some(raw) = snap.get(&sig)? else {
return Ok(None);
};
Ok(Some(u64::from_be_bytes(
raw.as_slice()
.try_into()
.map_err(|_| rushdb_storage::StorageError::CorruptLog)?,
)))
}
fn property_id<R: TryRead>(
snap: &R,
tenant: rushdb_types::ids::TenantId,
fid: u64,
ty: LogicalType,
) -> Result<Option<u64>, QueryError> {
use rushdb_lmpg::reader::keys_ext as kx;
let Some(raw) = snap.get(&kx::definition_key(tenant, fid, TypeState::Known(ty)))? else {
return Ok(None);
};
Ok(Some(u64::from_be_bytes(
raw.as_slice()
.try_into()
.map_err(|_| rushdb_storage::StorageError::CorruptLog)?,
)))
}
fn all_type_states() -> Vec<TypeState> {
let mut states = vec![TypeState::Unresolved];
states.extend(
[
LogicalType::Boolean,
LogicalType::Int64,
LogicalType::Float64,
LogicalType::Utf8,
LogicalType::DateTime,
]
.into_iter()
.map(TypeState::Known),
);
states
}
pub(super) fn pids_of<R: TryRead>(
snap: &R,
tenant: rushdb_types::ids::TenantId,
fid: u64,
) -> Result<Vec<u64>, QueryError> {
use rushdb_lmpg::reader::keys_ext as kx;
let mut pids = Vec::new();
for state in all_type_states() {
let Some(raw) = snap.get(&kx::definition_key(tenant, fid, state))? else {
continue;
};
pids.push(u64::from_be_bytes(
raw.as_slice()
.try_into()
.map_err(|_| rushdb_storage::StorageError::CorruptLog)?,
));
}
Ok(pids)
}
fn presence_prefixes<R: TryRead>(
snap: &R,
tenant: rushdb_types::ids::TenantId,
name: &str,
null_only: bool,
labels: &[String],
) -> Result<Option<Vec<Vec<u8>>>, QueryError> {
let norm = normalize(name);
if norm.is_empty() || rushdb_types::value::is_reserved_name(&norm) {
return Ok(None);
}
let Some(fid) = family_id(snap, tenant, &norm)? else {
return Ok(Some(Vec::new()));
};
let mut prefixes = Vec::new();
for pid in pids_of(snap, tenant, fid)? {
for label in labels_for(snap, tenant, pid, labels)? {
let nullness: &[u8] = if null_only {
&[rushdb_types::keys::NULLNESS_NULL]
} else {
&[
rushdb_types::keys::NULLNESS_VALUE,
rushdb_types::keys::NULLNESS_NULL,
]
};
for &nullness in nullness {
prefixes.push(rushdb_types::keys::presence_nullness_prefix(
tenant,
rushdb_types::ids::OwnerKind::Record,
&label,
pid,
nullness,
));
}
}
}
Ok(Some(prefixes))
}
pub(super) fn owners_with_presence<R: TryRead>(
snap: &R,
tenant: rushdb_types::ids::TenantId,
name: &str,
null_only: bool,
labels: &[String],
owner_limit: usize,
) -> Result<Option<OwnerSet>, QueryError> {
let Some(prefixes) = presence_prefixes(snap, tenant, name, null_only, labels)? else {
return Ok(None);
};
let mut set = OwnerSet::new();
let mut owners = BoundedOwners::new(&mut set, owner_limit);
for prefix in prefixes {
let (start, end) = rushdb_storage::prefix_range(&prefix);
let mut failed = None;
snap.for_each_key(&start, &end, &mut |key| {
let result = rushdb_types::keys::read_ord_tail(key, prefix.len())
.ok_or_else(|| rushdb_storage::StorageError::CorruptLog.into())
.and_then(|owner| owners.insert(owner));
match result {
Ok(()) => true,
Err(error) => {
failed = Some(error);
false
}
}
})?;
if let Some(error) = failed {
return Err(error);
}
}
Ok(Some(set))
}
fn owners_for_values<R: TryRead>(
snap: &R,
tenant: rushdb_types::ids::TenantId,
name: &str,
values: &[Json],
scope: &LeafScope<'_>,
) -> Result<Option<OwnerSet>, QueryError> {
let norm = normalize(name);
if norm.is_empty() || rushdb_types::value::is_reserved_name(&norm) {
return Ok(None);
}
let Some(fid) = family_id(snap, tenant, &norm)? else {
return Ok(Some(OwnerSet::new()));
};
let Some((pids, encodings)) = value_probes(snap, tenant, fid, values)? else {
return Ok(None);
};
let keep = |occ: &access::Occurrence| {
encodings.iter().any(|(ty, enc)| {
matches!(occ.value, rushdb_types::occurrence::OccurrenceValue::Stored { ty: stored } if stored == *ty)
&& occ.payload == *enc
})
};
let mut owners = OwnerSet::new();
for pid in pids {
if !owners_matching(snap, tenant, pid, scope, &mut owners, &keep)? {
return Ok(None);
}
}
Ok(Some(owners))
}
type ValueProbes = (std::collections::BTreeSet<u64>, Vec<(LogicalType, Vec<u8>)>);
fn value_probes<R: TryRead>(
snap: &R,
tenant: rushdb_types::ids::TenantId,
fid: u64,
values: &[Json],
) -> Result<Option<ValueProbes>, QueryError> {
let mut pids = std::collections::BTreeSet::new();
let mut encodings = Vec::new();
for value in values {
let Some(variants) = scalar_variants(value) else {
return Ok(None);
};
for (ty, scalar) in variants {
let Ok(enc) = rushdb_types::encoding::encode_order_preserving(ty, &scalar) else {
return Ok(None);
};
encodings.push((ty, enc));
if let Some(pid) = property_id(snap, tenant, fid, ty)? {
pids.insert(pid);
}
}
}
Ok(Some((pids, encodings)))
}
pub(super) fn total_support<R: TryRead>(
snap: &R,
tenant: rushdb_types::ids::TenantId,
name: &str,
labels: &[String],
) -> Result<Option<u64>, QueryError> {
let norm = normalize(name);
if norm.is_empty() || rushdb_types::value::is_reserved_name(&norm) {
return Ok(None);
}
let Some(fid) = family_id(snap, tenant, &norm)? else {
return Ok(Some(0));
};
let kind = rushdb_types::ids::OwnerKind::Record;
let mut total = 0u64;
for pid in pids_of(snap, tenant, fid)? {
if labels.is_empty() {
total = total.saturating_add(rushdb_lmpg::derived::prop_support_total(
snap, tenant, kind, pid,
)?);
} else {
for label in labels {
total = total.saturating_add(rushdb_lmpg::derived::prop_support_label(
snap, tenant, kind, pid, label,
)?);
}
}
}
Ok(Some(total))
}
pub(super) fn owners_with_presence_bounded(
snap: &rushdb_storage::ReadView,
tenant: rushdb_types::ids::TenantId,
name: &str,
labels: &[String],
k: usize,
) -> Result<Vec<u64>, QueryError> {
if k == 0 {
return Ok(Vec::new());
}
let Some(prefixes) = presence_prefixes(snap, tenant, name, false, labels)? else {
return Ok(Vec::new());
};
let mut cursors: Vec<rushdb_storage::ScanCursor<'_>> = Vec::new();
for prefix in &prefixes {
cursors.push(snap.scan(
rushdb_storage::ScanBounds::prefix(prefix),
rushdb_storage::ScanProjection::Keys,
)?);
}
let mut pending: Vec<Option<u64>> = Vec::with_capacity(cursors.len());
for cursor in &mut cursors {
pending.push(next_presence_owner(cursor)?);
}
let mut seen = std::collections::HashSet::new();
let mut out = Vec::with_capacity(k.min(4096));
while out.len() < k {
let Some(i) = pending
.iter()
.enumerate()
.filter_map(|(i, owner)| owner.map(|owner| (owner, i)))
.min()
.map(|(_, i)| i)
else {
break;
};
let owner = pending[i]
.take()
.ok_or(rushdb_storage::StorageError::CorruptLog)?;
pending[i] = next_presence_owner(&mut cursors[i])?;
if seen.insert(owner) {
out.push(owner);
}
}
Ok(out)
}
fn next_presence_owner(
cursor: &mut rushdb_storage::ScanCursor<'_>,
) -> Result<Option<u64>, QueryError> {
let batch = cursor.next_batch(1, 1024 * 1024)?;
batch
.rows
.into_iter()
.next()
.map(|row| {
rushdb_types::keys::parse_presence_key(&row.key)
.map(|(_, _, _, _, owner)| owner)
.ok_or_else(|| rushdb_storage::StorageError::CorruptLog.into())
})
.transpose()
}
#[expect(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
reason = "the f64 range guards against i64 bounds before narrowing an integral JSON number back to i64"
)]
pub(super) fn scalar_variants(value: &Json) -> Option<Vec<(LogicalType, Scalar)>> {
match value {
Json::String(s) => {
let mut out = vec![(LogicalType::Utf8, Scalar::Utf8(s.clone()))];
if let Some(ms) = crate::datetime::parse_iso_8601(s) {
out.push((LogicalType::DateTime, Scalar::DateTime(ms)));
}
Some(out)
}
Json::Bool(b) => Some(vec![(LogicalType::Boolean, Scalar::Boolean(*b))]),
Json::Number(n) => {
let mut out = Vec::new();
let push_integer = |out: &mut Vec<(LogicalType, Scalar)>, i: i64| {
out.push((LogicalType::Int64, Scalar::Int64(i)));
out.push((LogicalType::DateTime, Scalar::DateTime(i)));
};
if let Some(i) = n.as_i64() {
push_integer(&mut out, i);
}
if let Some(f) = n.as_f64() {
let matches_integer = n
.as_i64()
.is_some_and(|i| scalar_equal(&Scalar::Int64(i), &Scalar::Float64(f)));
let is_fractional = n.as_i64().is_none() && !n.is_u64();
if f.is_finite() && (matches_integer || is_fractional) {
out.push((LogicalType::Float64, Scalar::Float64(f)));
}
if f == 0.0 {
if !out
.iter()
.any(|(_, scalar)| matches!(scalar, Scalar::Int64(0)))
{
push_integer(&mut out, 0);
}
out.retain(
|(_, scalar)| !matches!(scalar, Scalar::Float64(value) if *value == 0.0),
);
out.push((LogicalType::Float64, Scalar::Float64(-0.0)));
out.push((LogicalType::Float64, Scalar::Float64(0.0)));
}
if !n.is_u64() && n.as_i64().is_none() && f.fract() == 0.0 && f >= i64::MIN as f64 {
let i = f as i64;
if (i as f64).to_bits() == f.to_bits() && f < 2_f64.powi(63) {
push_integer(&mut out, i);
}
}
}
if out.is_empty() {
None
} else {
Some(out)
}
}
_ => None,
}
}