1use super::{
4 aggregate_budget, alias_field_values, attach_collects_with_budget, binding_roots_with_cancel,
5 canonical_scalar_cmp, count_related, infer_scalar, json_value_cmp, normalize, reconstruct,
6 related_specs_from_where, resolve_field, resolve_field_numeric, working_set_budget,
7 AggregateRequest, Arc, GroupBucket, Json, Lmpg, Map, OutputExpr, Pagination, PointFields,
8 QueryError, QuerySnapshot, Scalar, SearchRequest, MISSING_SENTINEL,
9};
10use crate::aggregate_catalog::{dictionary_grouped_count, too_many_groups};
11use std::collections::{BTreeSet, HashMap, HashSet};
12
13pub fn aggregate(lmpg: &Lmpg, req: &AggregateRequest) -> Result<Vec<GroupBucket>, QueryError> {
22 aggregate_with_cancel(lmpg, req, &|| false).map(|(buckets, _)| buckets)
23}
24
25pub const MAX_AGGREGATE_BUCKETS: usize = 1_000;
28
29pub fn aggregate_with_cancel(
46 lmpg: &Lmpg,
47 req: &AggregateRequest,
48 is_cancelled: &dyn Fn() -> bool,
49) -> Result<(Vec<GroupBucket>, u64), QueryError> {
50 let snap = Arc::new(lmpg.store.read_view()?);
51 aggregate_with_cancel_in_view(lmpg, req, is_cancelled, snap)
52}
53
54pub fn aggregate_in_view(
61 lmpg: &Lmpg,
62 req: &AggregateRequest,
63 snap: QuerySnapshot,
64) -> Result<(Vec<GroupBucket>, u64), QueryError> {
65 aggregate_with_cancel_in_view(lmpg, req, &|| false, snap)
66}
67
68pub fn aggregate_with_cancel_in_view(
81 lmpg: &Lmpg,
82 req: &AggregateRequest,
83 is_cancelled: &dyn Fn() -> bool,
84 snap: QuerySnapshot,
85) -> Result<(Vec<GroupBucket>, u64), QueryError> {
86 let is_cancelled: &dyn Fn() -> bool = &crate::cancel_probe(lmpg, is_cancelled);
87 if !snap.belongs_to(&lmpg.store) {
88 return Err(rushdb_storage::StorageError::InvalidRead(
89 "aggregate view belongs to another Store".into(),
90 )
91 .into());
92 }
93 if is_cancelled() {
94 return Err(QueryError::Cancelled);
95 }
96 let budget = working_set_budget(lmpg);
97 aggregate_budget::check(budget, std::mem::size_of::<GroupBucket>())?;
98 aggregate_budgeted(lmpg, req, is_cancelled, snap, budget)
99 .map_err(aggregate_budget::resource_error)
100}
101
102#[expect(
103 clippy::too_many_lines,
104 clippy::cast_sign_loss,
105 clippy::cast_precision_loss,
106 clippy::needless_pass_by_value,
107 reason = "aggregate accumulation keeps one live budget and pinned snapshot"
108)]
109fn aggregate_budgeted(
110 lmpg: &Lmpg,
111 req: &AggregateRequest,
112 is_cancelled: &dyn Fn() -> bool,
113 snap: QuerySnapshot,
114 budget: Option<usize>,
115) -> Result<(Vec<GroupBucket>, u64), QueryError> {
116 let page_req = SearchRequest {
117 labels: req.labels.clone(),
118 where_: req.where_.clone(),
119 related: req.related.clone(),
120 collects: Vec::new(),
121 pagination: Pagination::default(),
122 order_by: Vec::new(),
123 force_scan: false,
124 };
125 if is_count_only(req) {
131 if let Some(total) = catalog_count_total(lmpg, &page_req, &snap)? {
132 let mut buckets = count_only_bucket(total);
133 apply_aggregate_pagination(&mut buckets, req);
134 return Ok((buckets, total));
135 }
136 }
137 if let Some(buckets) = dictionary_grouped_count(lmpg, req, &snap, is_cancelled)? {
140 aggregate_budget::check(
141 budget,
142 buckets.iter().fold(0usize, |sum, b| {
143 sum.saturating_add(aggregate_budget::bucket_bytes(b))
144 }),
145 )?;
146 let total = buckets.iter().map(|b| b.count_roots).sum();
147 return Ok((order_and_page(buckets, req), total));
148 }
149 let candidates: Vec<u64> =
150 binding_roots_with_cancel(lmpg, &page_req, is_cancelled, &snap, budget)?;
151 let candidate_bytes = candidates.len().saturating_mul(std::mem::size_of::<u64>());
154 aggregate_budget::check(budget, candidate_bytes)?;
155 let total_matched = candidates.len() as u64;
156 if is_count_only(req) {
157 let mut buckets = count_only_bucket(total_matched);
158 apply_aggregate_pagination(&mut buckets, req);
159 return Ok((buckets, total_matched));
160 }
161 let alias_specs = related_specs_from_where(req.where_.as_ref());
163 let pure_projection = !req.plain_fields.is_empty()
164 && req.group_by.is_empty()
165 && req.sums.is_empty()
166 && req.counts.is_empty()
167 && req.mins.is_empty()
168 && req.maxs.is_empty()
169 && req.outputs.is_empty()
170 && req.expr_sums.is_empty()
171 && req.expr_avgs.is_empty()
172 && req.avg_terms.is_empty()
173 && req.time_bucket.is_none();
174
175 let per_root =
180 req.group_by.is_empty() && (!req.plain_fields.is_empty() || !req.collects.is_empty());
181 let per_row = pure_projection || per_root;
185 let candidates = if per_row && req.order_by.is_empty() {
186 let page_end = req.skip.saturating_add(req.limit.unwrap_or(u64::MAX));
187 candidates
188 .into_iter()
189 .take(usize::try_from(page_end).unwrap_or(usize::MAX))
190 .collect()
191 } else {
192 candidates
193 };
194
195 let mut buckets: Vec<GroupBucket> = Vec::new();
196 let mut bucket_index: HashMap<String, usize> = HashMap::new();
199 let names = referenced_fields(req);
204 let point_fields = PointFields::resolve(&snap, lmpg.tenant, names.iter().map(String::as_str))?;
205 let selected: HashSet<String> = names
206 .iter()
207 .flat_map(|name| [name.clone(), normalize(name)])
208 .collect();
209 let mut session = reconstruct::ReconstructSession::new(&snap, lmpg.tenant);
210 let metric_count = req
211 .counts
212 .len()
213 .saturating_add(req.sums.len())
214 .saturating_add(req.mins.len())
215 .saturating_add(req.maxs.len())
216 .saturating_add(req.expr_avgs.len())
217 .saturating_add(req.expr_sums.len())
218 .saturating_add(req.avg_terms.len())
219 .saturating_add(req.group_by.len())
220 .saturating_add(req.plain_fields.len());
221 let mut retained_bytes = candidate_bytes;
222 for ord in candidates {
223 if is_cancelled() {
224 return Err(QueryError::Cancelled);
225 }
226 let obj: Map<String, Json> = match &point_fields {
227 Some(fields) => fields.row(&snap, lmpg.tenant, ord)?,
228 None => match session.reconstruct_record_selected(ord, &selected)? {
229 Some(Json::Object(map)) => map,
230 _ => continue,
231 },
232 };
233 let row_bytes = aggregate_budget::object_bytes(&obj);
236 aggregate_budget::check(budget, retained_bytes.saturating_add(row_bytes))?;
237 let alias_budget = budget.map(|limit| {
238 limit.saturating_sub(retained_bytes.saturating_add(row_bytes))
239 / metric_count.saturating_add(1)
240 });
241
242 let mut bucket_keys: Map<String, Json> = Map::new();
243 if pure_projection || per_root {
244 bucket_keys.insert("__rushdb_projection_root".into(), Json::Number(ord.into()));
248 }
249 if pure_projection {
250 for (output, field) in &req.plain_fields {
251 if let Some(value) = resolve_field(&obj, field) {
252 bucket_keys.insert(output.clone(), value);
253 }
254 }
255 } else if per_root {
256 for (output, field) in &req.plain_fields {
258 if let Some(value) = resolve_field(&obj, field) {
259 bucket_keys.insert(output.clone(), value);
260 }
261 }
262 }
263 for g in &req.group_by {
264 if req.time_bucket_alias.as_deref() == Some(g.as_str()) {
267 continue;
268 }
269 let v = resolve_field(&obj, g).unwrap_or(Json::String(MISSING_SENTINEL.into()));
272 bucket_keys.insert(g.clone(), v);
273 }
274 if let Some(tb) = &req.time_bucket {
275 let keyed = resolve_field(&obj, &tb.field)
276 .and_then(|v| v.as_f64())
277 .map_or_else(
278 || Json::String(MISSING_SENTINEL.into()),
279 |ms| Json::Number((tb.floor_millis(ms) as u64).into()),
280 );
281 let tb_key = req.time_bucket_alias.clone().unwrap_or_else(|| {
284 format!("__bucket_{}/{}", normalize(&tb.field), tb.unit.unit_str())
285 });
286 bucket_keys.insert(tb_key, keyed);
287 }
288
289 let (bucket, previous_bucket_bytes) = if per_row {
291 (new_bucket(&mut buckets, bucket_keys), 0)
292 } else {
293 let rendered = serde_json::to_string(&bucket_keys).unwrap_or_default();
294 if let Some(&position) = bucket_index.get(&rendered) {
295 let bucket = &mut buckets[position];
296 let bytes = aggregate_budget::bucket_bytes(bucket);
297 (bucket, bytes)
298 } else {
299 if buckets.len() >= MAX_AGGREGATE_BUCKETS {
300 return Err(too_many_groups());
301 }
302 bucket_index.insert(rendered, buckets.len());
303 (new_bucket(&mut buckets, bucket_keys), 0)
304 }
305 };
306
307 bucket.count_roots += 1;
309
310 for (out, field) in &req.counts {
311 if req.hidden_outputs.contains(out) {
312 continue;
313 }
314 let target_key = out.clone();
315 if field.as_str() == "*" {
316 continue;
317 }
318 if let Some(related) = count_related(
322 lmpg,
323 &snap,
324 lmpg.tenant,
325 ord,
326 &alias_specs,
327 field,
328 alias_budget,
329 )? {
330 aggregate_budget::check(alias_budget, related.len().saturating_mul(64))?;
331 for _other in related {
332 bump_count(bucket, &target_key);
333 }
334 continue;
335 }
336 if resolve_field(&obj, field).is_some() {
337 bump_count(bucket, &target_key);
338 }
339 }
340 for (out, field) in &req.sums {
341 if let Some(values) = alias_field_values(
344 lmpg,
345 &snap,
346 lmpg.tenant,
347 ord,
348 &alias_specs,
349 field,
350 alias_budget,
351 )? {
352 let key = out.clone();
353 let cur = bucket
354 .sums
355 .get(&key)
356 .and_then(serde_json::Value::as_f64)
357 .unwrap_or(0.0);
358 let sum = values
359 .iter()
360 .try_fold(cur, |sum, value| -> Result<f64, QueryError> {
361 if !value.is_number() {
362 return Ok(sum);
363 }
364 let value = exact_f64(value).ok_or_else(|| {
365 QueryError::InvalidPredicate(
366 "aggregate integer is not exactly representable as f64".into(),
367 )
368 })?;
369 Ok(sum + value)
370 })?;
371 bucket.sums.insert(key, serde_json::json!(sum));
372 } else if let Some(Json::Number(n)) = resolve_field_numeric(&obj, field) {
373 let key = out.clone();
374 let cur = bucket
375 .sums
376 .get(&key)
377 .and_then(serde_json::Value::as_f64)
378 .unwrap_or(0.0);
379 let next = cur
380 + exact_f64(&Json::Number(n.clone())).ok_or_else(|| {
381 QueryError::InvalidPredicate(
382 "aggregate integer is not exactly representable as f64".into(),
383 )
384 })?;
385 bucket.sums.insert(key, serde_json::json!(next));
386 }
387 }
388 for (out, expr) in &req.expr_sums {
389 if let Some(v) = eval_row_expr(expr, &obj) {
390 let key = out.clone();
391 let cur = bucket
392 .sums
393 .get(&key)
394 .and_then(serde_json::Value::as_f64)
395 .unwrap_or(0.0);
396 bucket.sums.insert(key, serde_json::json!(cur + v));
397 }
398 }
399 for (out, expr) in &req.expr_avgs {
400 if let Some(v) = eval_row_expr(expr, &obj) {
401 let key = out.clone();
402 let cur = bucket
403 .sums
404 .get(&key)
405 .and_then(serde_json::Value::as_f64)
406 .unwrap_or(0.0);
407 bucket.sums.insert(key, serde_json::json!(cur + v));
408 bump_avg_count(bucket, format!("__avg_n_{out}"), 1);
409 }
410 }
411 for (out, field) in &req.avg_terms {
412 if let Some(values) = alias_field_values(
413 lmpg,
414 &snap,
415 lmpg.tenant,
416 ord,
417 &alias_specs,
418 field,
419 alias_budget,
420 )? {
421 let key = out.clone();
422 let sum = values
423 .iter()
424 .try_fold(0.0, |sum, value| -> Result<f64, QueryError> {
425 if !value.is_number() {
426 return Ok(sum);
427 }
428 let value = exact_f64(value).ok_or_else(|| {
429 QueryError::InvalidPredicate(
430 "aggregate integer is not exactly representable as f64".into(),
431 )
432 })?;
433 Ok(sum + value)
434 })?;
435 let cur = bucket
436 .sums
437 .get(&key)
438 .and_then(serde_json::Value::as_f64)
439 .unwrap_or(0.0);
440 bucket.sums.insert(key, serde_json::json!(cur + sum));
441 let added = values.iter().filter(|v| v.is_number()).count() as u64;
442 bump_avg_count(bucket, format!("__avg_n_{out}"), added);
443 } else if let Some(Json::Number(n)) = resolve_field_numeric(&obj, field) {
444 let key = out.clone();
445 let cur = bucket
446 .sums
447 .get(&key)
448 .and_then(serde_json::Value::as_f64)
449 .unwrap_or(0.0);
450 let value = exact_f64(&Json::Number(n.clone())).ok_or_else(|| {
451 QueryError::InvalidPredicate(
452 "aggregate integer is not exactly representable as f64".into(),
453 )
454 })?;
455 bucket.sums.insert(key, serde_json::json!(cur + value));
456 bump_avg_count(bucket, format!("__avg_n_{out}"), 1);
457 }
458 }
459 for (out, field) in &req.mins {
460 if let Some(values) = alias_field_values(
461 lmpg,
462 &snap,
463 lmpg.tenant,
464 ord,
465 &alias_specs,
466 field,
467 alias_budget,
468 )? {
469 for v in values {
470 minmax(bucket, out.clone(), &v, true);
471 }
472 } else if let Some(v) = resolve_field(&obj, field) {
473 minmax(bucket, out.clone(), &v, true);
474 }
475 }
476 for (out, field) in &req.maxs {
477 if let Some(values) = alias_field_values(
478 lmpg,
479 &snap,
480 lmpg.tenant,
481 ord,
482 &alias_specs,
483 field,
484 alias_budget,
485 )? {
486 for v in values {
487 minmax(bucket, out.clone(), &v, false);
488 }
489 } else if let Some(v) = resolve_field(&obj, field) {
490 minmax(bucket, out.clone(), &v, false);
491 }
492 }
493 retained_bytes = retained_bytes
494 .saturating_sub(previous_bucket_bytes)
495 .saturating_add(aggregate_budget::bucket_bytes(bucket));
496 aggregate_budget::check(budget, retained_bytes)?;
497 }
498
499 if !req.outputs.is_empty() || !req.avg_terms.is_empty() || !req.expr_avgs.is_empty() {
504 let output_bytes = req
505 .outputs
506 .iter()
507 .map(|(name, _)| name)
508 .chain(req.avg_terms.iter().map(|(name, _)| name))
509 .chain(req.expr_avgs.iter().map(|(name, _)| name))
510 .fold(0usize, |sum, name| {
511 sum.saturating_add(name.len()).saturating_add(256)
512 })
513 .saturating_mul(buckets.len());
514 aggregate_budget::check(
515 budget,
516 retained_bytes
517 .saturating_sub(candidate_bytes)
518 .saturating_add(output_bytes),
519 )?;
520 for b in &mut buckets {
521 if is_cancelled() {
522 return Err(QueryError::Cancelled);
523 }
524 let mut resolved: std::collections::BTreeMap<String, f64> = b
525 .sums
526 .clone()
527 .into_iter()
528 .filter_map(|(k, v)| v.as_f64().map(|x| (k, x)))
529 .collect();
530 resolved.insert("count_roots".into(), b.count_roots as f64);
531 for (out, field) in &req.counts {
536 if req.hidden_outputs.contains(out) {
537 continue;
538 }
539 let value = if field.as_str() == "*" {
540 b.count_roots as f64
541 } else {
542 b.counts
543 .get(out)
544 .and_then(serde_json::Value::as_u64)
545 .map_or(0.0, |n| n as f64)
546 };
547 resolved.insert(out.clone(), value);
548 }
549 for name in req
551 .avg_terms
552 .iter()
553 .map(|(n, _)| n)
554 .chain(req.expr_avgs.iter().map(|(n, _)| n))
555 {
556 let sum = resolved.get(name).copied().unwrap_or(0.0);
557 let n = b
558 .counts
559 .get(&format!("__avg_n_{name}"))
560 .and_then(serde_json::Value::as_u64)
561 .unwrap_or(0);
562 let avg = if n > 0 { sum / n as f64 } else { 0.0 };
563 let avg = match req.avg_precision.get(name) {
564 Some(&0) => avg.trunc(),
566 Some(&p) => {
567 let p = p.min(crate::MAX_AVG_PRECISION);
570 let m = 10f64.powi(i32::try_from(p).unwrap_or(0));
571 (avg * m).round() / m
572 }
573 None => avg,
574 };
575 resolved.insert(name.clone(), avg);
576 b.sums.insert(name.clone(), serde_json::json!(avg));
577 }
578 for (name, expr) in &req.outputs {
579 eval_output(&normalize(name), expr, &mut resolved)?;
580 }
581 for (k, v) in &resolved {
582 b.derived_outputs.insert(k.clone(), serde_json::json!(v));
583 b.sums
585 .entry(k.clone())
586 .or_insert_with(|| serde_json::json!(v));
587 }
588 }
589 }
590
591 Ok((order_and_page(buckets, req), total_matched))
592}
593
594fn order_and_page(mut buckets: Vec<GroupBucket>, req: &AggregateRequest) -> Vec<GroupBucket> {
597 for (name, desc) in req.order_by.iter().rev() {
598 buckets.sort_by(|a, b| {
599 let va = bucket_metric(a, name, req);
600 let vb = bucket_metric(b, name, req);
601 let ord = compare_json_metric(va.as_ref(), vb.as_ref());
602 if *desc {
603 ord.reverse()
604 } else {
605 ord
606 }
607 });
608 }
609 if buckets.is_empty() && is_global_rollup(req) {
610 buckets.push(GroupBucket::default());
612 }
613 apply_aggregate_pagination(&mut buckets, req);
614 buckets
615}
616
617fn fill_absent_metrics(row: &mut Map<String, Json>, req: &AggregateRequest) {
620 if !is_global_rollup(req) {
621 return;
622 }
623 let metrics = req
624 .sums
625 .iter()
626 .chain(&req.avg_terms)
627 .map(|(out, _)| out)
628 .chain(req.expr_avgs.iter().map(|(out, _)| out))
629 .chain(req.mins.iter().map(|(out, _)| out))
630 .chain(req.maxs.iter().map(|(out, _)| out))
631 .chain(req.outputs.iter().map(|(out, _)| out));
632 for out in metrics {
633 if !req.hidden_outputs.contains(out) {
634 row.entry(out.clone()).or_insert(Json::Null);
635 }
636 }
637}
638
639fn is_global_rollup(req: &AggregateRequest) -> bool {
643 let metrics = !req.counts.is_empty()
644 || !req.sums.is_empty()
645 || !req.mins.is_empty()
646 || !req.maxs.is_empty()
647 || !req.outputs.is_empty()
648 || !req.expr_sums.is_empty()
649 || !req.expr_avgs.is_empty()
650 || !req.avg_terms.is_empty();
651 metrics
652 && req.group_by.is_empty()
653 && req.time_bucket.is_none()
654 && req.plain_fields.is_empty()
655 && req.collects.is_empty()
656}
657
658fn is_count_only(req: &AggregateRequest) -> bool {
662 req.group_by.is_empty()
663 && req.plain_fields.is_empty()
664 && req.collects.is_empty()
665 && req.sums.is_empty()
666 && req.mins.is_empty()
667 && req.maxs.is_empty()
668 && req.outputs.is_empty()
669 && req.expr_sums.is_empty()
670 && req.expr_avgs.is_empty()
671 && req.avg_terms.is_empty()
672 && req.time_bucket.is_none()
673 && !req.counts.is_empty()
674 && req.counts.iter().all(|(_, field)| field == "*")
675}
676
677fn count_only_bucket(total: u64) -> Vec<GroupBucket> {
680 vec![GroupBucket {
681 count_roots: total,
682 ..GroupBucket::default()
683 }]
684}
685
686fn apply_aggregate_pagination(buckets: &mut Vec<GroupBucket>, req: &AggregateRequest) {
687 if req.skip > 0 || req.limit.is_some() {
688 let start = usize::try_from(req.skip)
689 .unwrap_or(usize::MAX)
690 .min(buckets.len());
691 *buckets = buckets[start..]
692 .iter()
693 .take(usize::try_from(req.limit.unwrap_or(u64::MAX)).unwrap_or(usize::MAX))
694 .cloned()
695 .collect();
696 }
697}
698
699fn catalog_count_total(
703 lmpg: &Lmpg,
704 req: &SearchRequest,
705 snap: &QuerySnapshot,
706) -> Result<Option<u64>, QueryError> {
707 if req.where_.is_some() || !req.related.is_empty() || !req.order_by.is_empty() {
708 return Ok(None);
709 }
710 if req.labels.len() != 1 {
711 return Ok(None);
712 }
713 Ok(Some(rushdb_lmpg::derived::label_count(
715 snap,
716 lmpg.tenant,
717 &req.labels[0],
718 )?))
719}
720
721pub fn aggregate_to_rows(
730 lmpg: &Lmpg,
731 buckets: &[GroupBucket],
732 req: &AggregateRequest,
733) -> Result<Vec<Json>, QueryError> {
734 let snap: QuerySnapshot = Arc::new(lmpg.store.read_view()?);
735 aggregate_to_rows_in_view(lmpg, &snap, buckets, req)
736}
737
738pub fn aggregate_to_rows_in_view(
747 lmpg: &Lmpg,
748 snap: &QuerySnapshot,
749 buckets: &[GroupBucket],
750 req: &AggregateRequest,
751) -> Result<Vec<Json>, QueryError> {
752 aggregate_to_rows_in_view_with_budget(lmpg, snap, buckets, req, None)
753}
754
755pub fn aggregate_to_rows_in_view_with_budget(
760 lmpg: &Lmpg,
761 snap: &QuerySnapshot,
762 buckets: &[GroupBucket],
763 req: &AggregateRequest,
764 budget: Option<usize>,
765) -> Result<Vec<Json>, QueryError> {
766 let configured = lmpg
767 .limits()
768 .query_memory_max_bytes()
769 .map(|bytes| usize::try_from(bytes).unwrap_or(usize::MAX));
770 let budget = match (budget, configured) {
771 (Some(a), Some(b)) => Some(a.min(b)),
772 (a, b) => a.or(b),
773 };
774 let snap = budget.map_or_else(
775 || Arc::clone(snap),
776 |bytes| snap.with_materialization_limit(bytes / 16),
777 );
778 let mut retained = buckets.iter().fold(0usize, |sum, bucket| {
779 sum.saturating_add(aggregate_budget::bucket_bytes(bucket))
780 });
781 aggregate_budget::check(budget, retained)?;
782 let mut rows = Vec::new();
783 for b in buckets {
784 aggregate_budget::check(
785 budget,
786 retained.saturating_add(aggregate_budget::bucket_bytes(b)),
787 )?;
788 let root_ordinal = b
789 .keys
790 .get("__rushdb_projection_root")
791 .and_then(serde_json::Value::as_u64);
792 let mut row = Map::new();
793 for (k, v) in &b.keys {
794 if k != "__rushdb_projection_root" {
795 row.insert(k.clone(), v.clone());
796 }
797 }
798 for (out, field) in &req.counts {
799 if field == "*" {
800 row.insert(out.clone(), Json::Number((b.count_roots).into()));
801 } else {
802 let n = map_metric(&b.counts, out).and_then(serde_json::Value::as_u64);
803 row.insert(out.clone(), Json::Number(n.unwrap_or(0).into()));
804 }
805 }
806 for (out, _field) in &req.sums {
807 if req.hidden_outputs.contains(out) {
808 continue;
809 }
810 if let Some(v) = b.sums.get(out) {
811 row.insert(out.clone(), v.clone());
812 }
813 }
814 for out in req
815 .avg_terms
816 .iter()
817 .map(|(out, _)| out)
818 .chain(req.expr_avgs.iter().map(|(out, _)| out))
819 {
820 if !req.hidden_outputs.contains(out) {
821 if let Some(v) = b.sums.get(out) {
822 row.insert(out.clone(), v.clone());
823 }
824 }
825 }
826 for (out, _) in &req.mins {
827 if !req.hidden_outputs.contains(out) {
828 if let Some(v) = b.mins.get(out) {
829 row.insert(out.clone(), v.clone());
830 }
831 }
832 }
833 for (out, _) in &req.maxs {
834 if !req.hidden_outputs.contains(out) {
835 if let Some(v) = b.maxs.get(out) {
836 row.insert(out.clone(), v.clone());
837 }
838 }
839 }
840 for (out, _) in &req.outputs {
841 if let Some(v) = b.derived_outputs.get(&normalize(out)) {
842 row.insert(out.clone(), v.clone());
843 }
844 }
845 fill_absent_metrics(&mut row, req);
846 let mut row_json = Json::Object(row);
849 if !req.collects.is_empty() {
850 if let Some(ord) = root_ordinal {
851 row_json = attach_collects_with_budget(
852 lmpg,
853 &snap,
854 ord,
855 row_json,
856 &req.collects,
857 budget.map(|limit| limit.saturating_sub(retained)),
858 &|| false,
859 )
860 .map_err(aggregate_budget::resource_error)?;
861 }
862 }
863 retained = retained.saturating_add(aggregate_budget::value_bytes(&row_json));
864 aggregate_budget::check(budget, retained)?;
865 rows.push(row_json);
866 }
867 Ok(rows)
868}
869
870fn referenced_fields(req: &AggregateRequest) -> BTreeSet<String> {
877 let mut names = BTreeSet::new();
878 let mut add = |field: &str| {
879 names.insert(field.strip_prefix("$record.").unwrap_or(field).to_owned());
880 };
881 for g in &req.group_by {
882 if req.time_bucket_alias.as_deref() != Some(g.as_str()) {
883 add(g);
884 }
885 }
886 if let Some(tb) = &req.time_bucket {
887 add(&tb.field);
888 }
889 for (_, field) in req
890 .counts
891 .iter()
892 .filter(|(_, field)| field != "*")
893 .chain(&req.sums)
894 .chain(&req.mins)
895 .chain(&req.maxs)
896 .chain(&req.avg_terms)
897 .chain(&req.plain_fields)
898 {
899 add(field);
900 }
901 for (_, expr) in req.expr_sums.iter().chain(&req.expr_avgs) {
902 expr_fields(expr, &mut add);
903 }
904 names
905}
906
907fn expr_fields(expr: &OutputExpr, add: &mut impl FnMut(&str)) {
908 match expr {
909 OutputExpr::Field(name) | OutputExpr::Ref(name) => add(name),
910 OutputExpr::Add(a, b) | OutputExpr::Subtract(a, b) | OutputExpr::Multiply(a, b) => {
911 expr_fields(a, add);
912 expr_fields(b, add);
913 }
914 OutputExpr::Divide {
915 dividend, divisor, ..
916 } => {
917 expr_fields(dividend, add);
918 expr_fields(divisor, add);
919 }
920 OutputExpr::Literal(_) => {}
921 }
922}
923
924fn new_bucket(buckets: &mut Vec<GroupBucket>, keys: Map<String, Json>) -> &mut GroupBucket {
925 buckets.push(GroupBucket {
926 keys,
927 count_roots: 0,
928 sums: Map::new(),
929 counts: Map::new(),
930 mins: Map::new(),
931 maxs: Map::new(),
932 derived_outputs: std::collections::BTreeMap::new(),
933 });
934 let last = buckets.len() - 1;
935 &mut buckets[last]
936}
937
938fn bump_count(bucket: &mut GroupBucket, key: &str) {
943 let entry = bucket
944 .counts
945 .entry(key.to_string())
946 .or_insert_with(|| Json::Number(0.into()));
947 let next = entry.as_u64().unwrap_or(0).saturating_add(1);
948 *entry = Json::Number(next.into());
949}
950
951fn bump_avg_count(bucket: &mut GroupBucket, key: String, delta: u64) {
955 let entry = bucket
956 .counts
957 .entry(key)
958 .or_insert_with(|| Json::Number(0.into()));
959 if let Json::Number(n) = entry {
960 *n = (n.as_u64().unwrap_or(0) + delta).into();
961 }
962}
963
964pub(super) fn minmax(bucket: &mut GroupBucket, key: String, v: &Json, is_min: bool) {
965 let entry = if is_min {
966 &mut bucket.mins
967 } else {
968 &mut bucket.maxs
969 };
970 let entry = entry
971 .entry(key)
972 .or_insert_with(|| aggregate_numeric_value(v));
973 let Some((_, next)) = infer_scalar(v) else {
974 return;
975 };
976 let Some((_, current)) = infer_scalar(entry) else {
977 return;
978 };
979 if !matches!(next, Scalar::Int64(_) | Scalar::Float64(_))
980 || !matches!(current, Scalar::Int64(_) | Scalar::Float64(_))
981 {
982 return;
983 }
984 let Some(ordering) = canonical_scalar_cmp(&next, ¤t) else {
985 return;
986 };
987 let replace = if is_min {
988 ordering == std::cmp::Ordering::Less
989 } else {
990 ordering == std::cmp::Ordering::Greater
991 };
992 if replace {
993 *entry = aggregate_numeric_value(v);
994 }
995}
996
997#[expect(
998 clippy::cast_precision_loss,
999 clippy::cast_possible_truncation,
1000 reason = "the paired integer round-trip check preserves exact large integers"
1001)]
1002fn aggregate_numeric_value(value: &Json) -> Json {
1003 let Some((_, scalar)) = infer_scalar(value) else {
1004 return value.clone();
1005 };
1006 match scalar {
1007 Scalar::Int64(integer) => {
1008 let float = integer as f64;
1009 if (float as i128) == i128::from(integer) {
1010 serde_json::Number::from_f64(float).map_or_else(|| value.clone(), Json::Number)
1011 } else {
1012 value.clone()
1013 }
1014 }
1015 _ => value.clone(),
1016 }
1017}
1018
1019fn map_metric<'a>(map: &'a Map<String, Json>, name: &str) -> Option<&'a Json> {
1020 let normalized = normalize(name);
1021 map.iter()
1022 .find(|(key, _)| normalize(key) == normalized)
1023 .map(|(_, value)| value)
1024}
1025
1026fn bucket_metric(b: &GroupBucket, name: &str, req: &AggregateRequest) -> Option<Json> {
1027 if normalize(name) == "count_roots" {
1028 return Some(Json::Number(b.count_roots.into()));
1029 }
1030 if let Some((output, field)) = req
1031 .counts
1032 .iter()
1033 .find(|(output, _)| normalize(output) == normalize(name))
1034 {
1035 if field == "*" {
1036 return Some(Json::Number(b.count_roots.into()));
1037 }
1038 return map_metric(&b.counts, output)
1039 .map(|values| Json::Number(values.as_u64().unwrap_or(0).into()));
1040 }
1041 map_metric(&b.sums, name)
1042 .or_else(|| map_metric(&b.mins, name))
1043 .or_else(|| map_metric(&b.maxs, name))
1044 .or_else(|| map_metric(&b.keys, name))
1045 .cloned()
1046}
1047
1048fn compare_json_metric(a: Option<&Json>, b: Option<&Json>) -> std::cmp::Ordering {
1049 match (a, b) {
1050 (None, None) => std::cmp::Ordering::Equal,
1051 (None, _) => std::cmp::Ordering::Less,
1052 (_, None) => std::cmp::Ordering::Greater,
1053 (Some(x), Some(y)) => json_value_cmp(x, y),
1054 }
1055}
1056
1057pub(super) fn eval_row_expr(expr: &OutputExpr, obj: &Map<String, Json>) -> Option<f64> {
1059 match expr {
1060 OutputExpr::Literal(l) => Some(*l),
1061 OutputExpr::Field(field) => resolve_field(obj, field).and_then(|value| exact_f64(&value)),
1062 OutputExpr::Ref(name) => obj.get(&normalize(name)).and_then(exact_f64),
1063 OutputExpr::Add(a, b) => Some(eval_row_expr(a, obj)? + eval_row_expr(b, obj)?),
1064 OutputExpr::Subtract(a, b) => Some(eval_row_expr(a, obj)? - eval_row_expr(b, obj)?),
1065 OutputExpr::Multiply(a, b) => Some(eval_row_expr(a, obj)? * eval_row_expr(b, obj)?),
1066 OutputExpr::Divide {
1067 dividend, divisor, ..
1068 } => {
1069 let d = eval_row_expr(dividend, obj)?;
1070 let q = eval_row_expr(divisor, obj)?;
1071 if q == 0.0 {
1072 None
1073 } else {
1074 Some(d / q)
1075 }
1076 }
1077 }
1078}
1079
1080#[expect(
1083 clippy::cast_precision_loss,
1084 clippy::cast_possible_truncation,
1085 reason = "the integer round-trip check rejects lossy widening"
1086)]
1087pub(super) fn exact_f64(value: &Json) -> Option<f64> {
1088 let number = value.as_number()?;
1089 if let Some(integer) = number.as_i64() {
1090 let float = integer as f64;
1091 return ((float as i128) == i128::from(integer)).then_some(float);
1092 }
1093 if let Some(integer) = number.as_u64() {
1094 let float = integer as f64;
1095 return ((float as i128) == i128::from(integer)).then_some(float);
1096 }
1097 number.as_f64()
1098}
1099
1100fn eval_output(
1101 name: &str,
1102 expr: &OutputExpr,
1103 results: &mut std::collections::BTreeMap<String, f64>,
1104) -> Result<f64, QueryError> {
1105 let v = match expr {
1106 OutputExpr::Literal(l) => *l,
1107 OutputExpr::Field(_) => {
1108 return Ok(0.0);
1111 }
1112 OutputExpr::Ref(other) => results
1113 .get(other)
1114 .copied()
1115 .ok_or_else(|| QueryError::InvalidPredicate(format!("unresolved ref `{other}`")))?,
1116 OutputExpr::Add(a, b) => eval_output(name, a, results)? + eval_output(name, b, results)?,
1117 OutputExpr::Subtract(a, b) => {
1118 eval_output(name, a, results)? - eval_output(name, b, results)?
1119 }
1120 OutputExpr::Multiply(a, b) => {
1121 eval_output(name, a, results)? * eval_output(name, b, results)?
1122 }
1123 OutputExpr::Divide {
1124 dividend, divisor, ..
1125 } => {
1126 let d = eval_output(name, dividend, results)?;
1127 let q = eval_output(name, divisor, results)?;
1128 if q == 0.0 {
1129 return Err(QueryError::InvalidPredicate("divide by zero".into()));
1130 }
1131 d / q
1132 }
1133 };
1134 results.insert(name.to_string(), v);
1135 Ok(v)
1136}