Skip to main content

hyphae_query/
engine.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use 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/// Failure to validate or completely execute one structured query.
18#[derive(Clone, Debug, Error, Eq, PartialEq)]
19pub enum QueryError {
20    /// Record keys must be nonempty.
21    #[error("query input contains an empty record key")]
22    EmptyRecordKey,
23
24    /// Keys must be globally unique across shards.
25    #[error("duplicate global record key")]
26    DuplicateRecordKey,
27
28    /// A page must request at least one row.
29    #[error("query limit must be nonzero")]
30    ZeroLimit,
31
32    /// Requested page size exceeds policy.
33    #[error("query limit {requested} exceeds maximum {maximum}")]
34    ResultLimitExceeded {
35        /// Requested page size.
36        requested: usize,
37        /// Configured maximum.
38        maximum: usize,
39    },
40
41    /// Filter expression is too large.
42    #[error("filter has {actual} nodes; maximum is {maximum}")]
43    FilterNodesExceeded {
44        /// Observed filter nodes.
45        actual: usize,
46        /// Configured maximum.
47        maximum: usize,
48    },
49
50    /// Filter recursion is too deep.
51    #[error("filter depth is {actual}; maximum is {maximum}")]
52    FilterDepthExceeded {
53        /// Observed recursive depth, counting the root as one.
54        actual: usize,
55        /// Configured maximum.
56        maximum: usize,
57    },
58
59    /// Too many explicit sort fields were requested.
60    #[error("query has {actual} sort fields; maximum is {maximum}")]
61    SortFieldsExceeded {
62        /// Requested fields.
63        actual: usize,
64        /// Configured maximum.
65        maximum: usize,
66    },
67
68    /// Cursor shape does not match the sort plan.
69    #[error("cursor has {actual} sort values; query requires {expected}")]
70    CursorShape {
71        /// Cursor values.
72        actual: usize,
73        /// Query sort fields.
74        expected: usize,
75    },
76
77    /// Cursor keys must be nonempty.
78    #[error("cursor key must be nonempty")]
79    EmptyCursorKey,
80
81    /// Explicit null must use the canonical `None` cursor representation.
82    #[error("cursor contains a noncanonical explicit null sort value")]
83    NoncanonicalCursorNull,
84
85    /// A prefix filter literal is not a string or bytes value.
86    #[error("prefix filter requires a string or bytes literal")]
87    InvalidPrefixType,
88
89    /// Field-path segments must be nonempty UTF-8 strings.
90    #[error("field path contains an empty segment")]
91    InvalidFieldPath,
92
93    /// Too many group fields were requested.
94    #[error("aggregation has {actual} group fields; maximum is {maximum}")]
95    GroupFieldsExceeded {
96        /// Requested group fields.
97        actual: usize,
98        /// Configured maximum.
99        maximum: usize,
100    },
101
102    /// Too many metrics were requested.
103    #[error("aggregation has {actual} metrics; maximum is {maximum}")]
104    MetricsExceeded {
105        /// Requested metrics.
106        actual: usize,
107        /// Configured maximum.
108        maximum: usize,
109    },
110
111    /// Metric names must be nonempty.
112    #[error("aggregate metric name must be nonempty")]
113    EmptyMetricName,
114
115    /// Metric names must be unique within a plan.
116    #[error("duplicate aggregate metric name: {name}")]
117    DuplicateMetricName {
118        /// Duplicated name.
119        name: String,
120    },
121
122    /// Global scan budget was exhausted.
123    #[error("global scanned-record budget exceeded: {maximum}")]
124    ScannedBudgetExceeded {
125        /// Configured maximum.
126        maximum: u64,
127    },
128
129    /// Filtered-result memory budget was exhausted.
130    #[error("global matched-record budget exceeded: {maximum}")]
131    MatchedBudgetExceeded {
132        /// Configured maximum.
133        maximum: u64,
134    },
135
136    /// Aggregation group budget was exhausted.
137    #[error("aggregation group budget exceeded: {maximum}")]
138    GroupBudgetExceeded {
139        /// Configured maximum.
140        maximum: usize,
141    },
142
143    /// Cooperative monotonic deadline expired.
144    #[error("query execution timed out")]
145    TimedOut,
146
147    /// Sum encountered a non-integer value.
148    #[error("aggregate metric {name} requires integer values")]
149    MetricTypeMismatch {
150        /// Metric name.
151        name: String,
152    },
153
154    /// Checked aggregate arithmetic overflowed.
155    #[error("aggregate metric {name} overflowed")]
156    ArithmeticOverflow {
157        /// Metric name.
158        name: String,
159    },
160
161    /// Internal aggregate state did not match its validated metric plan.
162    #[error("aggregate metric state does not match its plan")]
163    MetricStateMismatch,
164}
165
166/// Injectable monotonic clock used to make timeout semantics testable.
167pub trait MonotonicClock {
168    /// Returns a nondecreasing duration in an arbitrary local epoch.
169    fn now(&mut self) -> Duration;
170}
171
172/// Production monotonic clock backed by [`Instant`].
173#[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
192/// Executes one query over complete logical shards using the system clock.
193///
194/// # Errors
195///
196/// Returns a validation, budget, timeout, duplicate-key, or aggregation error.
197pub 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
205/// Executes one query with an injectable monotonic clock.
206///
207/// This is public so conformance suites can prove timeout behavior without
208/// wall-clock sleeps.
209///
210/// # Errors
211///
212/// Returns a validation, budget, timeout, duplicate-key, or aggregation error.
213pub 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
322/// Validates query shape and cursor canonicality without scanning records.
323///
324/// # Errors
325///
326/// Returns the same pre-execution validation errors as [`execute`].
327pub 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}