use std::collections::BTreeSet;
use rushdb_lmpg::reader::TryRead;
use rushdb_types::ids::{OwnerKind, TenantId};
use rushdb_types::value::{is_reserved_name, ID_FIELD};
use crate::planner::{family_id, pids_of};
use crate::{normalize, QueryError};
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum OrderPath {
StorageMirror,
Id { walk_descending: bool, mirror: bool },
Property {
descending: bool,
pids: Vec<u64>,
holding: BTreeSet<String>,
scope: Vec<String>,
missing: u64,
index: Option<IndexOrder>,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct IndexOrder {
pub(crate) ids: Vec<u32>,
pub(crate) numeric: bool,
}
#[derive(Clone, Debug)]
pub(crate) struct OrderPlan {
pub(crate) path: OrderPath,
pub(crate) order_first_reads: u64,
pub(crate) predicate_first_reads: u64,
}
impl OrderPlan {
pub(crate) fn mirror(&self) -> bool {
matches!(
self.path,
OrderPath::StorageMirror | OrderPath::Id { mirror: true, .. }
)
}
pub(crate) fn source(&self) -> &'static str {
match self.path {
OrderPath::StorageMirror => "storage",
OrderPath::Id { .. } => "id",
OrderPath::Property { index: None, .. } => "property",
OrderPath::Property { index: Some(_), .. } => "index",
}
}
}
pub(crate) struct PageShape<'a> {
pub(crate) labels: &'a [String],
pub(crate) order_by: &'a [(String, bool)],
pub(crate) skip: usize,
pub(crate) limit: usize,
pub(crate) matches: Option<u64>,
pub(crate) candidates: u64,
}
pub(crate) fn plan_order<R: TryRead>(
snap: &R,
tenant: TenantId,
page: &PageShape<'_>,
) -> Result<Option<OrderPlan>, QueryError> {
let Some(matches) = page.matches.filter(|matches| *matches > 0) else {
return Ok(None);
};
let keep = u128::try_from(page.skip.saturating_add(page.limit)).unwrap_or(u128::MAX);
let skip = u128::try_from(page.skip).unwrap_or(u128::MAX);
let (matches, candidates) = (u128::from(matches), u128::from(page.candidates.max(1)));
let from_far_end = matches.saturating_sub(skip);
let mirror = page.skip > 0 && from_far_end < keep;
let walked = keep.min(from_far_end);
let plan = |path, reads: u128, predicate_first: u128| {
(reads < predicate_first).then(|| OrderPlan {
path,
order_first_reads: saturate(reads),
predicate_first_reads: saturate(predicate_first),
})
};
match page.order_by {
[] => {
let near = keep.saturating_mul(candidates) / matches;
let far = walked.saturating_mul(candidates) / matches;
Ok(if mirror {
plan(OrderPath::StorageMirror, far, near)
} else {
None
})
}
[(field, descending)] if field == ID_FIELD => {
let records = u128::from(rushdb_lmpg::derived::tenant_usage(snap, tenant)?.records);
let reads = walked.saturating_mul(records) / matches;
Ok(plan(
OrderPath::Id {
walk_descending: *descending != mirror,
mirror,
},
reads,
candidates,
))
}
[(field, descending)] => property_plan(
snap,
tenant,
page,
field,
*descending,
keep,
matches,
candidates,
),
_ => Ok(None),
}
}
#[expect(
clippy::too_many_arguments,
reason = "the page's numbers, already widened by the caller"
)]
fn property_plan<R: TryRead>(
snap: &R,
tenant: TenantId,
page: &PageShape<'_>,
field: &str,
descending: bool,
keep: u128,
matches: u128,
candidates: u128,
) -> Result<Option<OrderPlan>, QueryError> {
let norm = normalize(field);
if norm.is_empty() || is_reserved_name(&norm) {
return Ok(None);
}
let kind = OwnerKind::Record;
let pids = match family_id(snap, tenant, &norm)? {
Some(fid) => pids_of(snap, tenant, fid)?,
None => Vec::new(),
};
for &pid in &pids {
if rushdb_lmpg::derived::array_markers(snap, tenant, kind, pid)?.0 {
return Ok(None);
}
}
let scope: Vec<String> = if page.labels.is_empty() {
rushdb_lmpg::reader::try_access::labels(snap, tenant)?
} else {
page.labels
.iter()
.collect::<BTreeSet<_>>()
.into_iter()
.cloned()
.collect()
};
let mut holding = BTreeSet::new();
let mut holders = 0u64;
let mut records = 0u64;
for label in &scope {
records = records.saturating_add(rushdb_lmpg::derived::label_count(snap, tenant, label)?);
for &pid in &pids {
let held = rushdb_lmpg::derived::prop_support_label(snap, tenant, kind, pid, label)?;
if held > 0 {
holding.insert(label.clone());
holders = holders.saturating_add(held);
}
}
}
let (records, holders) = (u128::from(records.max(1)), u128::from(holders));
let missing = records.saturating_sub(holders);
let matching = |part: u128| part.saturating_mul(matches) / records;
let missing_walk = |wanted: u128| {
let found = matching(missing);
if missing == 0 {
0
} else if found == 0 || wanted >= found {
records
} else {
(wanted.saturating_mul(records) / found).min(records)
}
};
let index = index_order(snap, tenant, &norm, &pids, &holding)?;
let holder_reads = |wanted: u128| match index {
Some(_) => (wanted.saturating_mul(records) / matches).min(holders),
None => holders,
};
let reads = if descending {
holder_reads(keep).saturating_add(if matching(holders) >= keep {
0
} else {
missing_walk(keep - matching(holders))
})
} else if matching(missing) >= keep {
missing_walk(keep)
} else {
missing_walk(u128::MAX).saturating_add(holder_reads(keep - matching(missing)))
};
let path = OrderPath::Property {
descending,
pids,
holding,
scope,
missing: saturate(missing),
index,
};
Ok((reads < candidates).then(|| OrderPlan {
path,
order_first_reads: saturate(reads),
predicate_first_reads: saturate(candidates),
}))
}
fn index_order<R: TryRead>(
snap: &R,
tenant: TenantId,
property: &str,
pids: &[u64],
holding: &BTreeSet<String>,
) -> Result<Option<IndexOrder>, QueryError> {
use rushdb_types::model::{LogicalType, TypeState};
if holding.is_empty() {
return Ok(None);
}
let mut typed = None;
for &pid in pids {
let Some(def) = rushdb_lmpg::reader::try_access::definition(snap, tenant, pid)? else {
continue;
};
let TypeState::Known(ty) = def.type_state else {
continue;
};
let held = holding.iter().try_fold(0u64, |held, label| {
rushdb_lmpg::derived::prop_support_label(snap, tenant, OwnerKind::Record, pid, label)
.map(|n| held.saturating_add(n))
})?;
if held == 0 {
continue;
}
if typed.replace(ty).is_some() {
return Ok(None);
}
}
let numeric = match typed {
Some(LogicalType::Int64 | LogicalType::DateTime) => true,
Some(LogicalType::Utf8) => false,
_ => return Ok(None),
};
let mut ids = Vec::new();
for label in holding {
match rushdb_lmpg::index::ready_ordered(snap, tenant, label, property)? {
Some(def) => ids.push(def.id),
None => return Ok(None),
}
}
Ok(Some(IndexOrder { ids, numeric }))
}
fn saturate(value: u128) -> u64 {
u64::try_from(value).unwrap_or(u64::MAX)
}