1use std::{
4 cmp::Ordering,
5 collections::{BTreeMap, BTreeSet},
6 time::{Duration, Instant},
7};
8
9use thiserror::Error;
10
11use crate::{
12 AggregationPlan, AggregationResult, CompareOperator, Cursor, DocumentError, ExecutionLimits,
13 FieldPath, Filter, GroupResult, Metric, MetricValue, NamedMetricValue, NullPlacement, Query,
14 QueryResult, Record, SortDirection, SortField, Value, encoded_document_len,
15};
16
17pub const DEFAULT_QUERY_SCAN_BYTES: u64 = 256 * 1024 * 1024;
20
21#[derive(Clone, Debug, Error, Eq, PartialEq)]
23pub enum QueryError {
24 #[error("query input contains an empty record key")]
26 EmptyRecordKey,
27
28 #[error("duplicate global record key")]
30 DuplicateRecordKey,
31
32 #[error("query limit must be nonzero")]
34 ZeroLimit,
35
36 #[error("query limit {requested} exceeds maximum {maximum}")]
38 ResultLimitExceeded {
39 requested: usize,
41 maximum: usize,
43 },
44
45 #[error("filter has {actual} nodes; maximum is {maximum}")]
47 FilterNodesExceeded {
48 actual: usize,
50 maximum: usize,
52 },
53
54 #[error("filter depth is {actual}; maximum is {maximum}")]
56 FilterDepthExceeded {
57 actual: usize,
59 maximum: usize,
61 },
62
63 #[error("query has {actual} sort fields; maximum is {maximum}")]
65 SortFieldsExceeded {
66 actual: usize,
68 maximum: usize,
70 },
71
72 #[error("cursor has {actual} sort values; query requires {expected}")]
74 CursorShape {
75 actual: usize,
77 expected: usize,
79 },
80
81 #[error("cursor key must be nonempty")]
83 EmptyCursorKey,
84
85 #[error("cursor contains a noncanonical explicit null sort value")]
87 NoncanonicalCursorNull,
88
89 #[error("prefix filter requires a string or bytes literal")]
91 InvalidPrefixType,
92
93 #[error("field path contains an empty segment")]
95 InvalidFieldPath,
96
97 #[error("aggregation has {actual} group fields; maximum is {maximum}")]
99 GroupFieldsExceeded {
100 actual: usize,
102 maximum: usize,
104 },
105
106 #[error("aggregation has {actual} metrics; maximum is {maximum}")]
108 MetricsExceeded {
109 actual: usize,
111 maximum: usize,
113 },
114
115 #[error("aggregate metric name must be nonempty")]
117 EmptyMetricName,
118
119 #[error("duplicate aggregate metric name: {name}")]
121 DuplicateMetricName {
122 name: String,
124 },
125
126 #[error("global scanned-record budget exceeded: {maximum}")]
128 ScannedBudgetExceeded {
129 maximum: u64,
131 },
132
133 #[error("global matched-record budget exceeded: {maximum}")]
135 MatchedBudgetExceeded {
136 maximum: u64,
138 },
139
140 #[error("aggregation group budget exceeded: {maximum}")]
142 GroupBudgetExceeded {
143 maximum: usize,
145 },
146
147 #[error("query execution timed out")]
149 TimedOut,
150
151 #[error("aggregate metric {name} requires integer values")]
153 MetricTypeMismatch {
154 name: String,
156 },
157
158 #[error("aggregate metric {name} overflowed")]
160 ArithmeticOverflow {
161 name: String,
163 },
164
165 #[error("aggregate metric state does not match its plan")]
167 MetricStateMismatch,
168}
169
170#[derive(Clone, Debug, Error, Eq, PartialEq)]
177pub enum BoundedQueryError {
178 #[error(transparent)]
180 Query(#[from] QueryError),
181 #[error("global scanned-byte budget exceeded: {maximum}")]
183 ScannedByteBudgetExceeded {
184 maximum: u64,
186 },
187 #[error("query record is not a canonical document: {0}")]
189 RecordDocument(#[from] DocumentError),
190}
191
192pub trait MonotonicClock {
194 fn now(&mut self) -> Duration;
196}
197
198#[derive(Debug)]
200pub struct SystemClock {
201 origin: Instant,
202}
203
204impl Default for SystemClock {
205 fn default() -> Self {
206 Self {
207 origin: Instant::now(),
208 }
209 }
210}
211
212impl MonotonicClock for SystemClock {
213 fn now(&mut self) -> Duration {
214 self.origin.elapsed()
215 }
216}
217
218pub fn execute(
224 shards: &[&[Record]],
225 query: &Query,
226 limits: &ExecutionLimits,
227) -> Result<QueryResult, QueryError> {
228 execute_with_clock_without_byte_limit(shards, query, limits, &mut SystemClock::default())
229}
230
231pub fn execute_with_byte_limit(
238 shards: &[&[Record]],
239 query: &Query,
240 limits: &ExecutionLimits,
241 max_scanned_bytes: u64,
242) -> Result<QueryResult, BoundedQueryError> {
243 execute_with_clock_and_byte_limit(
244 shards,
245 query,
246 limits,
247 max_scanned_bytes,
248 &mut SystemClock::default(),
249 )
250}
251
252pub fn execute_with_clock(
261 shards: &[&[Record]],
262 query: &Query,
263 limits: &ExecutionLimits,
264 clock: &mut impl MonotonicClock,
265) -> Result<QueryResult, QueryError> {
266 execute_with_clock_without_byte_limit(shards, query, limits, clock)
267}
268
269pub fn execute_with_clock_and_byte_limit(
277 shards: &[&[Record]],
278 query: &Query,
279 limits: &ExecutionLimits,
280 max_scanned_bytes: u64,
281 clock: &mut impl MonotonicClock,
282) -> Result<QueryResult, BoundedQueryError> {
283 execute_with_clock_impl(shards, query, limits, Some(max_scanned_bytes), clock)
284}
285
286fn execute_with_clock_without_byte_limit(
287 shards: &[&[Record]],
288 query: &Query,
289 limits: &ExecutionLimits,
290 clock: &mut impl MonotonicClock,
291) -> Result<QueryResult, QueryError> {
292 match execute_with_clock_impl(shards, query, limits, None, clock) {
293 Ok(result) => Ok(result),
294 Err(BoundedQueryError::Query(source)) => Err(source),
295 Err(
296 BoundedQueryError::ScannedByteBudgetExceeded { .. }
297 | BoundedQueryError::RecordDocument(_),
298 ) => unreachable!("legacy query execution does not inspect canonical document bytes"),
299 }
300}
301
302fn execute_with_clock_impl(
303 shards: &[&[Record]],
304 query: &Query,
305 limits: &ExecutionLimits,
306 max_scanned_bytes: Option<u64>,
307 clock: &mut impl MonotonicClock,
308) -> Result<QueryResult, BoundedQueryError> {
309 validate_query(query, limits)?;
310 let started = clock.now();
311 let deadline = started.checked_add(limits.timeout).unwrap_or(Duration::MAX);
312 let mut budget = ExecutionBudget {
313 clock,
314 deadline,
315 limits,
316 scanned: 0,
317 scanned_bytes: 0,
318 max_scanned_bytes,
319 matched: 0,
320 };
321 budget.check_timeout()?;
322
323 let mut keys = BTreeSet::<&[u8]>::new();
324 let mut candidates = Vec::new();
325 for shard in shards {
326 for record in *shard {
327 budget.scan()?;
328 budget.account_record_bytes(record)?;
329 if record.key.is_empty() {
330 return Err(QueryError::EmptyRecordKey.into());
331 }
332 if !keys.insert(record.key.as_slice()) {
333 return Err(QueryError::DuplicateRecordKey.into());
334 }
335 if filter_matches(&query.filter, &record.value) {
336 budget.match_record()?;
337 candidates.push(record);
338 }
339 }
340 }
341
342 candidates.sort_by(|left, right| compare_records(left, right, &query.sort));
343 budget.check_timeout()?;
344 let aggregation = query
345 .aggregation
346 .as_ref()
347 .map(|plan| aggregate(&candidates, plan, &mut budget))
348 .transpose()?;
349
350 let page_start = query.cursor.as_ref().map_or(0, |cursor| {
351 candidates.partition_point(|record| {
352 compare_record_to_cursor(record, cursor, &query.sort) != Ordering::Greater
353 })
354 });
355 let remaining = &candidates[page_start..];
356 let page_length = remaining.len().min(query.limit);
357 let mut rows = Vec::with_capacity(page_length);
358 for record in &remaining[..page_length] {
359 budget.check_timeout()?;
360 rows.push((*record).clone());
361 }
362 let next_cursor = if remaining.len() > page_length {
363 rows.last().map(|record| cursor_for(record, &query.sort))
364 } else {
365 None
366 };
367 budget.check_timeout()?;
368
369 Ok(QueryResult {
370 rows,
371 next_cursor,
372 aggregation,
373 scanned_records: budget.scanned,
374 matched_records: budget.matched,
375 })
376}
377
378struct ExecutionBudget<'limits, 'clock, Clock> {
379 clock: &'clock mut Clock,
380 deadline: Duration,
381 limits: &'limits ExecutionLimits,
382 scanned: u64,
383 scanned_bytes: u64,
384 max_scanned_bytes: Option<u64>,
385 matched: u64,
386}
387
388impl<Clock: MonotonicClock> ExecutionBudget<'_, '_, Clock> {
389 fn check_timeout(&mut self) -> Result<(), QueryError> {
390 if self.clock.now() >= self.deadline {
391 Err(QueryError::TimedOut)
392 } else {
393 Ok(())
394 }
395 }
396
397 fn scan(&mut self) -> Result<(), QueryError> {
398 self.check_timeout()?;
399 if self.scanned >= self.limits.max_scanned_records {
400 return Err(QueryError::ScannedBudgetExceeded {
401 maximum: self.limits.max_scanned_records,
402 });
403 }
404 self.scanned = self.scanned.saturating_add(1);
405 Ok(())
406 }
407
408 fn account_record_bytes(&mut self, record: &Record) -> Result<(), BoundedQueryError> {
409 let Some(maximum) = self.max_scanned_bytes else {
410 return Ok(());
411 };
412 self.scanned_bytes =
413 checked_scanned_bytes(self.scanned_bytes, record_bytes(record, maximum)?, maximum)?;
414 Ok(())
415 }
416
417 fn match_record(&mut self) -> Result<(), QueryError> {
418 if self.matched >= self.limits.max_matched_records {
419 return Err(QueryError::MatchedBudgetExceeded {
420 maximum: self.limits.max_matched_records,
421 });
422 }
423 self.matched = self.matched.saturating_add(1);
424 Ok(())
425 }
426}
427
428fn checked_scanned_bytes(
429 consumed: u64,
430 record_bytes: u64,
431 maximum: u64,
432) -> Result<u64, BoundedQueryError> {
433 let next = consumed
434 .checked_add(record_bytes)
435 .ok_or(BoundedQueryError::ScannedByteBudgetExceeded { maximum })?;
436 if next > maximum {
437 Err(BoundedQueryError::ScannedByteBudgetExceeded { maximum })
438 } else {
439 Ok(next)
440 }
441}
442
443fn record_bytes(record: &Record, maximum: u64) -> Result<u64, BoundedQueryError> {
444 let key_bytes = u64::try_from(record.key.len())
445 .map_err(|_| BoundedQueryError::ScannedByteBudgetExceeded { maximum })?;
446 let document_bytes = u64::try_from(encoded_document_len(&record.value)?)
447 .map_err(|_| BoundedQueryError::ScannedByteBudgetExceeded { maximum })?;
448 key_bytes
449 .checked_add(document_bytes)
450 .ok_or(BoundedQueryError::ScannedByteBudgetExceeded { maximum })
451}
452
453pub fn validate_query(query: &Query, limits: &ExecutionLimits) -> Result<(), QueryError> {
459 if query.limit == 0 {
460 return Err(QueryError::ZeroLimit);
461 }
462 if query.limit > limits.max_returned_records {
463 return Err(QueryError::ResultLimitExceeded {
464 requested: query.limit,
465 maximum: limits.max_returned_records,
466 });
467 }
468 let (filter_nodes, filter_depth) = filter_shape(&query.filter);
469 if filter_nodes > limits.max_filter_nodes {
470 return Err(QueryError::FilterNodesExceeded {
471 actual: filter_nodes,
472 maximum: limits.max_filter_nodes,
473 });
474 }
475 if filter_depth > limits.max_filter_depth {
476 return Err(QueryError::FilterDepthExceeded {
477 actual: filter_depth,
478 maximum: limits.max_filter_depth,
479 });
480 }
481 validate_field_paths(query)?;
482 validate_prefixes(&query.filter)?;
483 if query.sort.len() > limits.max_sort_fields {
484 return Err(QueryError::SortFieldsExceeded {
485 actual: query.sort.len(),
486 maximum: limits.max_sort_fields,
487 });
488 }
489 if let Some(cursor) = &query.cursor {
490 validate_cursor(cursor, query.sort.len())?;
491 }
492 if let Some(plan) = &query.aggregation {
493 validate_aggregation(plan, limits)?;
494 }
495 Ok(())
496}
497
498fn validate_field_paths(query: &Query) -> Result<(), QueryError> {
499 let mut filters = vec![&query.filter];
500 while let Some(filter) = filters.pop() {
501 let path = match filter {
502 Filter::Exists(path)
503 | Filter::Compare { path, .. }
504 | Filter::Prefix { path, .. }
505 | Filter::Contains { path, .. } => Some(path),
506 Filter::All(children) | Filter::Any(children) => {
507 filters.extend(children);
508 None
509 }
510 Filter::Not(child) => {
511 filters.push(child);
512 None
513 }
514 Filter::MatchAll => None,
515 };
516 if path.is_some_and(path_has_empty_segment) {
517 return Err(QueryError::InvalidFieldPath);
518 }
519 }
520 if query
521 .sort
522 .iter()
523 .any(|field| path_has_empty_segment(&field.path))
524 {
525 return Err(QueryError::InvalidFieldPath);
526 }
527 if let Some(plan) = &query.aggregation
528 && (plan.group_by.iter().any(path_has_empty_segment)
529 || plan.metrics.iter().any(|metric| match &metric.metric {
530 Metric::Count => false,
531 Metric::Sum(path) | Metric::Min(path) | Metric::Max(path) => {
532 path_has_empty_segment(path)
533 }
534 }))
535 {
536 return Err(QueryError::InvalidFieldPath);
537 }
538 Ok(())
539}
540
541fn path_has_empty_segment(path: &FieldPath) -> bool {
542 path.segments().iter().any(String::is_empty)
543}
544
545fn filter_shape(filter: &Filter) -> (usize, usize) {
546 let mut count = 0_usize;
547 let mut maximum_depth = 0_usize;
548 let mut pending = vec![(filter, 1_usize)];
549 while let Some((current, depth)) = pending.pop() {
550 count = count.saturating_add(1);
551 maximum_depth = maximum_depth.max(depth);
552 match current {
553 Filter::All(children) | Filter::Any(children) => pending.extend(
554 children
555 .iter()
556 .map(|child| (child, depth.saturating_add(1))),
557 ),
558 Filter::Not(child) => pending.push((child, depth.saturating_add(1))),
559 Filter::MatchAll
560 | Filter::Exists(_)
561 | Filter::Compare { .. }
562 | Filter::Prefix { .. }
563 | Filter::Contains { .. } => {}
564 }
565 }
566 (count, maximum_depth)
567}
568
569fn validate_prefixes(filter: &Filter) -> Result<(), QueryError> {
570 let mut pending = vec![filter];
571 while let Some(current) = pending.pop() {
572 match current {
573 Filter::Prefix { prefix, .. } => {
574 if !matches!(prefix, Value::String(_) | Value::Bytes(_)) {
575 return Err(QueryError::InvalidPrefixType);
576 }
577 }
578 Filter::All(children) | Filter::Any(children) => pending.extend(children),
579 Filter::Not(child) => pending.push(child),
580 Filter::MatchAll
581 | Filter::Exists(_)
582 | Filter::Compare { .. }
583 | Filter::Contains { .. } => {}
584 }
585 }
586 Ok(())
587}
588
589fn validate_cursor(cursor: &Cursor, sort_fields: usize) -> Result<(), QueryError> {
590 if cursor.sort_values.len() != sort_fields {
591 return Err(QueryError::CursorShape {
592 actual: cursor.sort_values.len(),
593 expected: sort_fields,
594 });
595 }
596 if cursor.key.is_empty() {
597 return Err(QueryError::EmptyCursorKey);
598 }
599 if cursor
600 .sort_values
601 .iter()
602 .any(|value| value == &Some(Value::Null))
603 {
604 return Err(QueryError::NoncanonicalCursorNull);
605 }
606 Ok(())
607}
608
609fn validate_aggregation(
610 plan: &AggregationPlan,
611 limits: &ExecutionLimits,
612) -> Result<(), QueryError> {
613 if plan.group_by.len() > limits.max_group_fields {
614 return Err(QueryError::GroupFieldsExceeded {
615 actual: plan.group_by.len(),
616 maximum: limits.max_group_fields,
617 });
618 }
619 if plan.metrics.len() > limits.max_metrics {
620 return Err(QueryError::MetricsExceeded {
621 actual: plan.metrics.len(),
622 maximum: limits.max_metrics,
623 });
624 }
625 let mut names = BTreeSet::new();
626 for metric in &plan.metrics {
627 if metric.name.is_empty() {
628 return Err(QueryError::EmptyMetricName);
629 }
630 if !names.insert(metric.name.as_str()) {
631 return Err(QueryError::DuplicateMetricName {
632 name: metric.name.clone(),
633 });
634 }
635 }
636 Ok(())
637}
638
639fn filter_matches(filter: &Filter, root: &Value) -> bool {
640 match filter {
641 Filter::MatchAll => true,
642 Filter::Exists(path) => path.resolve(root).is_some(),
643 Filter::Compare {
644 path,
645 operator,
646 value,
647 } => path
648 .resolve(root)
649 .is_some_and(|actual| compare_filter(actual, value, *operator)),
650 Filter::Prefix { path, prefix } => path
651 .resolve(root)
652 .is_some_and(|actual| has_prefix(actual, prefix)),
653 Filter::Contains { path, needle } => path
654 .resolve(root)
655 .is_some_and(|actual| contains(actual, needle)),
656 Filter::All(children) => children.iter().all(|child| filter_matches(child, root)),
657 Filter::Any(children) => children.iter().any(|child| filter_matches(child, root)),
658 Filter::Not(child) => !filter_matches(child, root),
659 }
660}
661
662fn compare_filter(left: &Value, right: &Value, operator: CompareOperator) -> bool {
663 match operator {
664 CompareOperator::Equal => left == right,
665 CompareOperator::NotEqual => left != right,
666 CompareOperator::Less => same_variant(left, right) && left < right,
667 CompareOperator::LessOrEqual => same_variant(left, right) && left <= right,
668 CompareOperator::Greater => same_variant(left, right) && left > right,
669 CompareOperator::GreaterOrEqual => same_variant(left, right) && left >= right,
670 }
671}
672
673fn same_variant(left: &Value, right: &Value) -> bool {
674 std::mem::discriminant(left) == std::mem::discriminant(right)
675}
676
677fn has_prefix(actual: &Value, prefix: &Value) -> bool {
678 match (actual, prefix) {
679 (Value::String(actual), Value::String(prefix)) => actual.starts_with(prefix),
680 (Value::Bytes(actual), Value::Bytes(prefix)) => actual.starts_with(prefix),
681 _ => false,
682 }
683}
684
685fn contains(actual: &Value, needle: &Value) -> bool {
686 match (actual, needle) {
687 (Value::Array(values), needle) => values.contains(needle),
688 (Value::String(actual), Value::String(needle)) => actual.contains(needle),
689 (Value::Bytes(actual), Value::Bytes(needle)) => {
690 needle.is_empty() || actual.windows(needle.len()).any(|window| window == needle)
691 }
692 _ => false,
693 }
694}
695
696fn compare_records(left: &Record, right: &Record, sort: &[SortField]) -> Ordering {
697 for field in sort {
698 let ordering = compare_sort_values(
699 normalized_sort_value(field.path.resolve(&left.value)),
700 normalized_sort_value(field.path.resolve(&right.value)),
701 field,
702 );
703 if ordering != Ordering::Equal {
704 return ordering;
705 }
706 }
707 left.key.cmp(&right.key)
708}
709
710fn compare_record_to_cursor(record: &Record, cursor: &Cursor, sort: &[SortField]) -> Ordering {
711 for (field, cursor_value) in sort.iter().zip(&cursor.sort_values) {
712 let ordering = compare_sort_values(
713 normalized_sort_value(field.path.resolve(&record.value)),
714 cursor_value.as_ref(),
715 field,
716 );
717 if ordering != Ordering::Equal {
718 return ordering;
719 }
720 }
721 record.key.cmp(&cursor.key)
722}
723
724fn normalized_sort_value(value: Option<&Value>) -> Option<&Value> {
725 value.filter(|value| !matches!(value, Value::Null))
726}
727
728fn compare_sort_values(left: Option<&Value>, right: Option<&Value>, field: &SortField) -> Ordering {
729 match (left, right) {
730 (None, None) => Ordering::Equal,
731 (None, Some(_)) => match field.nulls {
732 NullPlacement::First => Ordering::Less,
733 NullPlacement::Last => Ordering::Greater,
734 },
735 (Some(_), None) => match field.nulls {
736 NullPlacement::First => Ordering::Greater,
737 NullPlacement::Last => Ordering::Less,
738 },
739 (Some(left), Some(right)) => match field.direction {
740 SortDirection::Ascending => left.cmp(right),
741 SortDirection::Descending => right.cmp(left),
742 },
743 }
744}
745
746fn cursor_for(record: &Record, sort: &[SortField]) -> Cursor {
747 Cursor {
748 sort_values: sort
749 .iter()
750 .map(|field| normalized_sort_value(field.path.resolve(&record.value)).cloned())
751 .collect(),
752 key: record.key.clone(),
753 }
754}
755
756#[derive(Clone, Debug)]
757enum MetricState {
758 Count(u64),
759 Sum(Option<i128>),
760 Min(Option<Value>),
761 Max(Option<Value>),
762}
763
764fn aggregate<Clock: MonotonicClock>(
765 records: &[&Record],
766 plan: &AggregationPlan,
767 budget: &mut ExecutionBudget<'_, '_, Clock>,
768) -> Result<AggregationResult, QueryError> {
769 let grouped = !plan.group_by.is_empty();
770 let mut groups: BTreeMap<Vec<Option<Value>>, Vec<MetricState>> = BTreeMap::new();
771 if !grouped {
772 ensure_group(&mut groups, Vec::new(), plan, budget.limits.max_groups)?;
773 }
774 for record in records {
775 budget.check_timeout()?;
776 let key = plan
777 .group_by
778 .iter()
779 .map(|path| path.resolve(&record.value).cloned())
780 .collect::<Vec<_>>();
781 let states = ensure_group(&mut groups, key, plan, budget.limits.max_groups)?;
782 update_metrics(states, &plan.metrics, &record.value)?;
783 }
784
785 let groups = groups
786 .into_iter()
787 .map(|(key, states)| GroupResult {
788 key,
789 metrics: plan
790 .metrics
791 .iter()
792 .zip(states)
793 .map(|(metric, state)| NamedMetricValue {
794 name: metric.name.clone(),
795 value: finish_metric(state),
796 })
797 .collect(),
798 })
799 .collect();
800 Ok(AggregationResult { grouped, groups })
801}
802
803fn ensure_group<'groups>(
804 groups: &'groups mut BTreeMap<Vec<Option<Value>>, Vec<MetricState>>,
805 key: Vec<Option<Value>>,
806 plan: &AggregationPlan,
807 maximum: usize,
808) -> Result<&'groups mut Vec<MetricState>, QueryError> {
809 if !groups.contains_key(&key) && groups.len() >= maximum {
810 return Err(QueryError::GroupBudgetExceeded { maximum });
811 }
812 Ok(groups.entry(key).or_insert_with(|| {
813 plan.metrics
814 .iter()
815 .map(|metric| match metric.metric {
816 Metric::Count => MetricState::Count(0),
817 Metric::Sum(_) => MetricState::Sum(None),
818 Metric::Min(_) => MetricState::Min(None),
819 Metric::Max(_) => MetricState::Max(None),
820 })
821 .collect()
822 }))
823}
824
825fn update_metrics(
826 states: &mut [MetricState],
827 metrics: &[crate::NamedMetric],
828 root: &Value,
829) -> Result<(), QueryError> {
830 for (state, named) in states.iter_mut().zip(metrics) {
831 match (&named.metric, state) {
832 (Metric::Count, MetricState::Count(count)) => {
833 *count = count
834 .checked_add(1)
835 .ok_or_else(|| QueryError::ArithmeticOverflow {
836 name: named.name.clone(),
837 })?;
838 }
839 (Metric::Sum(path), MetricState::Sum(sum)) => {
840 let Some(value) = path.resolve(root) else {
841 continue;
842 };
843 match value {
844 Value::Null => {}
845 Value::Integer(value) => {
846 *sum = Some(
847 sum.unwrap_or(0)
848 .checked_add(i128::from(*value))
849 .ok_or_else(|| QueryError::ArithmeticOverflow {
850 name: named.name.clone(),
851 })?,
852 );
853 }
854 _ => {
855 return Err(QueryError::MetricTypeMismatch {
856 name: named.name.clone(),
857 });
858 }
859 }
860 }
861 (Metric::Min(path), MetricState::Min(minimum)) => {
862 update_extreme(minimum, path, root, Ordering::Less);
863 }
864 (Metric::Max(path), MetricState::Max(maximum)) => {
865 update_extreme(maximum, path, root, Ordering::Greater);
866 }
867 _ => return Err(QueryError::MetricStateMismatch),
868 }
869 }
870 Ok(())
871}
872
873fn update_extreme(current: &mut Option<Value>, path: &FieldPath, root: &Value, desired: Ordering) {
874 let Some(candidate) = path
875 .resolve(root)
876 .filter(|value| !matches!(value, Value::Null))
877 else {
878 return;
879 };
880 if current
881 .as_ref()
882 .is_none_or(|existing| candidate.cmp(existing) == desired)
883 {
884 *current = Some(candidate.clone());
885 }
886}
887
888fn finish_metric(state: MetricState) -> MetricValue {
889 match state {
890 MetricState::Count(count) => MetricValue::Count(count),
891 MetricState::Sum(sum) => MetricValue::Integer(sum),
892 MetricState::Min(value) | MetricState::Max(value) => MetricValue::Value(value),
893 }
894}
895
896#[cfg(test)]
897mod tests {
898 use std::{collections::BTreeMap, time::Duration};
899
900 use super::{
901 BoundedQueryError, MonotonicClock, QueryError, checked_scanned_bytes, execute,
902 execute_with_byte_limit, execute_with_clock,
903 };
904 use crate::{
905 AggregationPlan, CompareOperator, ExecutionLimits, FieldPath, Filter, Metric, MetricValue,
906 NamedMetric, NullPlacement, Query, Record, SortDirection, SortField, Value,
907 };
908
909 fn object(fields: impl IntoIterator<Item = (&'static str, Value)>) -> Value {
910 Value::Object(
911 fields
912 .into_iter()
913 .map(|(name, value)| (name.to_owned(), value))
914 .collect::<BTreeMap<_, _>>(),
915 )
916 }
917
918 fn record(key: &'static [u8], score: i64, group: &'static str) -> Record {
919 Record::new(
920 key,
921 object([
922 ("score", Value::Integer(score)),
923 ("group", Value::String(group.to_owned())),
924 ]),
925 )
926 }
927
928 fn score_descending() -> SortField {
929 SortField {
930 path: FieldPath::field("score"),
931 direction: SortDirection::Descending,
932 nulls: NullPlacement::Last,
933 }
934 }
935
936 fn query(limit: usize) -> Query {
937 Query {
938 filter: Filter::MatchAll,
939 sort: vec![score_descending()],
940 cursor: None,
941 limit,
942 aggregation: None,
943 }
944 }
945
946 #[test]
947 fn global_merge_precedes_limit_and_cursor_is_stable() -> Result<(), QueryError> {
948 let first = vec![record(b"a", 10, "x"), record(b"d", 1, "x")];
949 let second = vec![
950 record(b"b", 9, "y"),
951 record(b"c", 8, "y"),
952 record(b"e", 8, "z"),
953 ];
954 let shards = [first.as_slice(), second.as_slice()];
955 let first_page = execute(&shards, &query(3), &ExecutionLimits::default())?;
956 assert_eq!(
957 first_page
958 .rows
959 .iter()
960 .map(|record| record.key.as_slice())
961 .collect::<Vec<_>>(),
962 [b"a".as_slice(), b"b".as_slice(), b"c".as_slice()]
963 );
964
965 let second_page = execute(
966 &shards,
967 &Query {
968 cursor: first_page.next_cursor,
969 ..query(3)
970 },
971 &ExecutionLimits::default(),
972 )?;
973 assert_eq!(
974 second_page
975 .rows
976 .iter()
977 .map(|record| record.key.as_slice())
978 .collect::<Vec<_>>(),
979 [b"e".as_slice(), b"d".as_slice()]
980 );
981 assert_eq!(second_page.next_cursor, None);
982 Ok(())
983 }
984
985 #[test]
986 fn filters_and_grouped_aggregates_use_the_full_match_set() -> Result<(), QueryError> {
987 let records = vec![
988 record(b"a", 10, "x"),
989 record(b"b", 8, "x"),
990 record(b"c", 7, "y"),
991 record(b"d", 2, "y"),
992 ];
993 let result = execute(
994 &[records.as_slice()],
995 &Query {
996 filter: Filter::Compare {
997 path: FieldPath::field("score"),
998 operator: CompareOperator::GreaterOrEqual,
999 value: Value::Integer(7),
1000 },
1001 sort: vec![score_descending()],
1002 cursor: None,
1003 limit: 1,
1004 aggregation: Some(AggregationPlan {
1005 group_by: vec![FieldPath::field("group")],
1006 metrics: vec![
1007 NamedMetric {
1008 name: "count".to_owned(),
1009 metric: Metric::Count,
1010 },
1011 NamedMetric {
1012 name: "sum".to_owned(),
1013 metric: Metric::Sum(FieldPath::field("score")),
1014 },
1015 ],
1016 }),
1017 },
1018 &ExecutionLimits::default(),
1019 )?;
1020 assert_eq!(result.rows.len(), 1);
1021 assert_eq!(result.matched_records, 3);
1022 let aggregation = result.aggregation.ok_or(QueryError::TimedOut)?;
1023 assert_eq!(aggregation.groups.len(), 2);
1024 assert_eq!(
1025 aggregation.groups[0].metrics[0].value,
1026 MetricValue::Count(2)
1027 );
1028 assert_eq!(
1029 aggregation.groups[0].metrics[1].value,
1030 MetricValue::Integer(Some(18))
1031 );
1032 assert_eq!(
1033 aggregation.groups[1].metrics[0].value,
1034 MetricValue::Count(1)
1035 );
1036 assert_eq!(
1037 aggregation.groups[1].metrics[1].value,
1038 MetricValue::Integer(Some(7))
1039 );
1040 Ok(())
1041 }
1042
1043 #[test]
1044 fn missing_and_null_have_explicit_filter_and_sort_semantics() -> Result<(), QueryError> {
1045 let records = vec![
1046 Record::new(b"missing", object([])),
1047 Record::new(b"null", object([("value", Value::Null)])),
1048 Record::new(b"integer", object([("value", Value::Integer(1))])),
1049 ];
1050 let result = execute(
1051 &[records.as_slice()],
1052 &Query {
1053 filter: Filter::Not(Box::new(Filter::Compare {
1054 path: FieldPath::field("value"),
1055 operator: CompareOperator::Equal,
1056 value: Value::Integer(1),
1057 })),
1058 sort: vec![SortField {
1059 path: FieldPath::field("value"),
1060 direction: SortDirection::Ascending,
1061 nulls: NullPlacement::First,
1062 }],
1063 cursor: None,
1064 limit: 10,
1065 aggregation: None,
1066 },
1067 &ExecutionLimits::default(),
1068 )?;
1069 assert_eq!(
1070 result
1071 .rows
1072 .iter()
1073 .map(|record| record.key.as_slice())
1074 .collect::<Vec<_>>(),
1075 [b"missing".as_slice(), b"null".as_slice()]
1076 );
1077 Ok(())
1078 }
1079
1080 #[test]
1081 fn global_work_budgets_fail_without_partial_results() {
1082 let records = vec![record(b"a", 1, "x"), record(b"b", 2, "x")];
1083 let limits = ExecutionLimits {
1084 max_scanned_records: 1,
1085 ..ExecutionLimits::default()
1086 };
1087 assert_eq!(
1088 execute(&[records.as_slice()], &query(1), &limits),
1089 Err(QueryError::ScannedBudgetExceeded { maximum: 1 })
1090 );
1091 }
1092
1093 #[test]
1094 fn scanned_byte_budget_is_exact_global_and_includes_nonmatches() -> Result<(), BoundedQueryError>
1095 {
1096 let first = vec![record(b"a", 1, "x")];
1097 let second = vec![record(b"b", 2, "y")];
1098 let shards = [first.as_slice(), second.as_slice()];
1099 let query = Query {
1100 filter: Filter::Compare {
1101 path: FieldPath::field("score"),
1102 operator: CompareOperator::Greater,
1103 value: Value::Integer(100),
1104 },
1105 sort: Vec::new(),
1106 cursor: None,
1107 limit: 1,
1108 aggregation: None,
1109 };
1110 let total = first
1111 .iter()
1112 .chain(&second)
1113 .map(|record| super::record_bytes(record, u64::MAX))
1114 .try_fold(0_u64, |total, bytes| {
1115 total
1116 .checked_add(bytes?)
1117 .ok_or(BoundedQueryError::ScannedByteBudgetExceeded { maximum: u64::MAX })
1118 })?;
1119 let result = execute_with_byte_limit(&shards, &query, &ExecutionLimits::default(), total)?;
1120 assert_eq!(result.scanned_records, 2);
1121 assert_eq!(result.matched_records, 0);
1122 assert!(result.rows.is_empty());
1123 assert_eq!(
1124 execute_with_byte_limit(&shards, &query, &ExecutionLimits::default(), total - 1,),
1125 Err(BoundedQueryError::ScannedByteBudgetExceeded { maximum: total - 1 })
1126 );
1127 assert_eq!(
1128 checked_scanned_bytes(u64::MAX, 1, u64::MAX),
1129 Err(BoundedQueryError::ScannedByteBudgetExceeded { maximum: u64::MAX })
1130 );
1131 Ok(())
1132 }
1133
1134 #[test]
1135 fn matched_and_group_budgets_are_global() {
1136 let records = vec![record(b"a", 1, "x"), record(b"b", 2, "y")];
1137 let matched_limits = ExecutionLimits {
1138 max_matched_records: 1,
1139 ..ExecutionLimits::default()
1140 };
1141 assert_eq!(
1142 execute(&[records.as_slice()], &query(1), &matched_limits),
1143 Err(QueryError::MatchedBudgetExceeded { maximum: 1 })
1144 );
1145
1146 let group_limits = ExecutionLimits {
1147 max_groups: 1,
1148 ..ExecutionLimits::default()
1149 };
1150 let grouped = Query {
1151 aggregation: Some(AggregationPlan {
1152 group_by: vec![FieldPath::field("group")],
1153 metrics: vec![NamedMetric {
1154 name: "count".to_owned(),
1155 metric: Metric::Count,
1156 }],
1157 }),
1158 ..query(1)
1159 };
1160 assert_eq!(
1161 execute(&[records.as_slice()], &grouped, &group_limits),
1162 Err(QueryError::GroupBudgetExceeded { maximum: 1 })
1163 );
1164 }
1165
1166 #[test]
1167 fn shape_and_cursor_validation_happen_before_execution() {
1168 let records = vec![record(b"a", 1, "x")];
1169 let filter_limits = ExecutionLimits {
1170 max_filter_nodes: 1,
1171 ..ExecutionLimits::default()
1172 };
1173 let nested = Query {
1174 filter: Filter::Not(Box::new(Filter::MatchAll)),
1175 ..query(1)
1176 };
1177 assert_eq!(
1178 execute(&[records.as_slice()], &nested, &filter_limits),
1179 Err(QueryError::FilterNodesExceeded {
1180 actual: 2,
1181 maximum: 1
1182 })
1183 );
1184
1185 let depth_limits = ExecutionLimits {
1186 max_filter_depth: 1,
1187 ..ExecutionLimits::default()
1188 };
1189 assert_eq!(
1190 execute(&[records.as_slice()], &nested, &depth_limits),
1191 Err(QueryError::FilterDepthExceeded {
1192 actual: 2,
1193 maximum: 1
1194 })
1195 );
1196
1197 let empty_segment = Query {
1198 filter: Filter::Exists(FieldPath::new(["nested", ""])),
1199 ..query(1)
1200 };
1201 assert_eq!(
1202 execute(
1203 &[records.as_slice()],
1204 &empty_segment,
1205 &ExecutionLimits::default()
1206 ),
1207 Err(QueryError::InvalidFieldPath)
1208 );
1209
1210 let malformed_cursor = Query {
1211 cursor: Some(crate::Cursor {
1212 sort_values: Vec::new(),
1213 key: b"a".to_vec(),
1214 }),
1215 ..query(1)
1216 };
1217 assert_eq!(
1218 execute(
1219 &[records.as_slice()],
1220 &malformed_cursor,
1221 &ExecutionLimits::default()
1222 ),
1223 Err(QueryError::CursorShape {
1224 actual: 0,
1225 expected: 1
1226 })
1227 );
1228 }
1229
1230 #[test]
1231 fn grouped_missing_and_null_are_distinct() -> Result<(), QueryError> {
1232 let records = vec![
1233 Record::new(b"missing", object([])),
1234 Record::new(b"null", object([("value", Value::Null)])),
1235 ];
1236 let result = execute(
1237 &[records.as_slice()],
1238 &Query {
1239 aggregation: Some(AggregationPlan {
1240 group_by: vec![FieldPath::field("value")],
1241 metrics: vec![NamedMetric {
1242 name: "count".to_owned(),
1243 metric: Metric::Count,
1244 }],
1245 }),
1246 ..query(10)
1247 },
1248 &ExecutionLimits::default(),
1249 )?;
1250 let aggregation = result.aggregation.ok_or(QueryError::MetricStateMismatch)?;
1251 assert_eq!(aggregation.groups[0].key, [None]);
1252 assert_eq!(aggregation.groups[1].key, [Some(Value::Null)]);
1253 Ok(())
1254 }
1255
1256 #[test]
1257 fn sum_rejects_non_integer_values() {
1258 let records = vec![Record::new(
1259 b"bad",
1260 object([("score", Value::String("not-an-integer".to_owned()))]),
1261 )];
1262 let request = Query {
1263 aggregation: Some(AggregationPlan {
1264 group_by: Vec::new(),
1265 metrics: vec![NamedMetric {
1266 name: "sum".to_owned(),
1267 metric: Metric::Sum(FieldPath::field("score")),
1268 }],
1269 }),
1270 ..query(1)
1271 };
1272 assert_eq!(
1273 execute(&[records.as_slice()], &request, &ExecutionLimits::default()),
1274 Err(QueryError::MetricTypeMismatch {
1275 name: "sum".to_owned()
1276 })
1277 );
1278 }
1279
1280 #[test]
1281 fn duplicate_keys_across_shards_are_rejected() {
1282 let first = vec![record(b"same", 1, "x")];
1283 let second = vec![record(b"same", 2, "y")];
1284 assert_eq!(
1285 execute(
1286 &[first.as_slice(), second.as_slice()],
1287 &query(1),
1288 &ExecutionLimits::default()
1289 ),
1290 Err(QueryError::DuplicateRecordKey)
1291 );
1292 }
1293
1294 struct StepClock {
1295 current: Duration,
1296 step: Duration,
1297 }
1298
1299 impl MonotonicClock for StepClock {
1300 fn now(&mut self) -> Duration {
1301 let current = self.current;
1302 self.current = self.current.saturating_add(self.step);
1303 current
1304 }
1305 }
1306
1307 #[test]
1308 fn timeout_uses_an_injectable_monotonic_clock() {
1309 let records = vec![record(b"a", 1, "x"), record(b"b", 2, "x")];
1310 let limits = ExecutionLimits {
1311 timeout: Duration::from_millis(3),
1312 ..ExecutionLimits::default()
1313 };
1314 let mut clock = StepClock {
1315 current: Duration::ZERO,
1316 step: Duration::from_millis(1),
1317 };
1318 assert_eq!(
1319 execute_with_clock(&[records.as_slice()], &query(1), &limits, &mut clock),
1320 Err(QueryError::TimedOut)
1321 );
1322 }
1323
1324 #[test]
1325 fn timeout_is_checked_before_returning_the_final_page() {
1326 let limits = ExecutionLimits {
1327 timeout: Duration::from_millis(3),
1328 ..ExecutionLimits::default()
1329 };
1330 let mut clock = StepClock {
1331 current: Duration::ZERO,
1332 step: Duration::from_millis(1),
1333 };
1334
1335 assert_eq!(
1336 execute_with_clock(&[&[]], &query(1), &limits, &mut clock),
1337 Err(QueryError::TimedOut)
1338 );
1339 }
1340}