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