use crate::{
normalize, AggregateRequest, GroupBucket, Json, Lmpg, Map, QueryError, QuerySnapshot,
MISSING_SENTINEL,
};
use super::aggregation::MAX_AGGREGATE_BUCKETS;
pub(crate) fn dictionary_grouped_count(
lmpg: &Lmpg,
req: &AggregateRequest,
snap: &QuerySnapshot,
is_cancelled: &dyn Fn() -> bool,
) -> Result<Option<Vec<GroupBucket>>, QueryError> {
use rushdb_lmpg::derived;
use rushdb_types::ids::OwnerKind;
use rushdb_types::model::TypeState;
let only_root_counts =
!req.counts.is_empty() && req.counts.iter().all(|(_, field)| field == "*");
if req.group_by.len() != 1
|| req.labels.len() != 1
|| !only_root_counts
|| req.where_.is_some()
|| !req.related.is_empty()
|| req.time_bucket.is_some()
|| !req.plain_fields.is_empty()
|| !req.collects.is_empty()
|| !req.sums.is_empty()
|| !req.mins.is_empty()
|| !req.maxs.is_empty()
|| !req.outputs.is_empty()
|| !req.expr_sums.is_empty()
|| !req.expr_avgs.is_empty()
|| !req.avg_terms.is_empty()
{
return Ok(None);
}
let group = &req.group_by[0];
let norm = normalize(group.strip_prefix("$record.").unwrap_or(group));
if norm.is_empty() || rushdb_types::value::is_reserved_name(&norm) {
return Ok(None);
}
let tenant = lmpg.tenant;
let label = &req.labels[0];
let labels = std::slice::from_ref(label);
let total = derived::label_count(snap, tenant, label)?;
let bucket = |value: Json, count: u64| GroupBucket {
keys: Map::from_iter([(group.clone(), value)]),
count_roots: count,
..GroupBucket::default()
};
let missing = || Json::String(MISSING_SENTINEL.into());
let held = match crate::planner::family_id(snap, tenant, &norm)? {
Some(fid) => definitions_held_under(snap, tenant, fid, label)?,
None => Vec::new(),
};
let [(state, pid, support)] = held[..] else {
if held.is_empty() {
return Ok(Some(if total == 0 {
Vec::new()
} else {
vec![bucket(missing(), total)]
}));
}
return Ok(None);
};
let (_, array_labels) = derived::array_markers(snap, tenant, OwnerKind::Record, pid)?;
if array_labels.iter().any(|l| l == label)
|| derived::long_values(snap, tenant, OwnerKind::Record, pid, labels)? > 0
{
return Ok(None);
}
let (mut buckets, counted) = match state {
TypeState::Known(ty) => {
dictionary_buckets(snap, tenant, pid, ty, labels, &bucket, is_cancelled)?
}
TypeState::Unresolved => (Vec::new(), 0),
};
let nulls = support.saturating_sub(counted);
if nulls > 0 {
buckets.push(bucket(Json::Null, nulls));
}
let absent = total.saturating_sub(support);
if absent > 0 {
buckets.push(bucket(missing(), absent));
}
if buckets.len() > MAX_AGGREGATE_BUCKETS {
return Err(too_many_groups());
}
Ok(Some(buckets))
}
fn definitions_held_under(
snap: &QuerySnapshot,
tenant: rushdb_types::ids::TenantId,
fid: u64,
label: &str,
) -> Result<Vec<(rushdb_types::model::TypeState, u64, u64)>, QueryError> {
let mut held = Vec::new();
for state in crate::planner::all_type_states() {
let key = rushdb_lmpg::reader::keys_ext::definition_key(tenant, fid, state);
let Some(raw) = snap.get(&key)? else {
continue;
};
let pid = u64::from_be_bytes(
raw.as_slice()
.try_into()
.map_err(|_| rushdb_storage::StorageError::CorruptLog)?,
);
let support = rushdb_lmpg::derived::prop_support_label(
snap,
tenant,
rushdb_types::ids::OwnerKind::Record,
pid,
label,
)?;
if support > 0 {
held.push((state, pid, support));
}
}
Ok(held)
}
pub(crate) fn too_many_groups() -> QueryError {
QueryError::ResourceLimit(format!(
"aggregate exceeds {MAX_AGGREGATE_BUCKETS} distinct groups; narrow the groupBy or add filters"
))
}
fn dictionary_buckets(
snap: &QuerySnapshot,
tenant: rushdb_types::ids::TenantId,
pid: u64,
ty: rushdb_types::model::LogicalType,
labels: &[String],
bucket: &dyn Fn(Json, u64) -> GroupBucket,
is_cancelled: &dyn Fn() -> bool,
) -> Result<(Vec<GroupBucket>, u64), QueryError> {
let mut buckets = Vec::new();
let mut counted = 0u64;
let mut failure: Option<QueryError> = None;
rushdb_lmpg::derived::for_each_dictionary_entry(
snap,
tenant,
rushdb_types::ids::OwnerKind::Record,
pid,
labels,
&mut |entry, count| {
if is_cancelled() {
failure = Some(QueryError::Cancelled);
return false;
}
let Some(payload) = entry.get(1..) else {
failure = Some(rushdb_storage::StorageError::CorruptLog.into());
return false;
};
let Ok(scalar) = rushdb_types::encoding::decode_order_preserving(ty, payload) else {
failure = Some(rushdb_storage::StorageError::CorruptLog.into());
return false;
};
if buckets.len() >= MAX_AGGREGATE_BUCKETS {
failure = Some(too_many_groups());
return false;
}
counted = counted.saturating_add(count);
buckets.push(bucket(rushdb_types::value::scalar_to_json(&scalar), count));
true
},
)?;
if let Some(error) = failure {
return Err(error);
}
Ok((buckets, counted))
}