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, DocumentError, ExecutionLimits,
13    FieldPath, Filter, GroupResult, Metric, MetricValue, NamedMetricValue, NullPlacement, Query,
14    QueryResult, Record, SortDirection, SortField, Value, encoded_document_len,
15};
16
17/// Fixed aggregate key and canonical-document byte budget used by the
18/// compatibility-preserving query entry points.
19pub const DEFAULT_QUERY_SCAN_BYTES: u64 = 256 * 1024 * 1024;
20
21/// Failure to validate or completely execute one structured query.
22#[derive(Clone, Debug, Error, Eq, PartialEq)]
23pub enum QueryError {
24    /// Record keys must be nonempty.
25    #[error("query input contains an empty record key")]
26    EmptyRecordKey,
27
28    /// Keys must be globally unique across shards.
29    #[error("duplicate global record key")]
30    DuplicateRecordKey,
31
32    /// A page must request at least one row.
33    #[error("query limit must be nonzero")]
34    ZeroLimit,
35
36    /// Requested page size exceeds policy.
37    #[error("query limit {requested} exceeds maximum {maximum}")]
38    ResultLimitExceeded {
39        /// Requested page size.
40        requested: usize,
41        /// Configured maximum.
42        maximum: usize,
43    },
44
45    /// Filter expression is too large.
46    #[error("filter has {actual} nodes; maximum is {maximum}")]
47    FilterNodesExceeded {
48        /// Observed filter nodes.
49        actual: usize,
50        /// Configured maximum.
51        maximum: usize,
52    },
53
54    /// Filter recursion is too deep.
55    #[error("filter depth is {actual}; maximum is {maximum}")]
56    FilterDepthExceeded {
57        /// Observed recursive depth, counting the root as one.
58        actual: usize,
59        /// Configured maximum.
60        maximum: usize,
61    },
62
63    /// Too many explicit sort fields were requested.
64    #[error("query has {actual} sort fields; maximum is {maximum}")]
65    SortFieldsExceeded {
66        /// Requested fields.
67        actual: usize,
68        /// Configured maximum.
69        maximum: usize,
70    },
71
72    /// Cursor shape does not match the sort plan.
73    #[error("cursor has {actual} sort values; query requires {expected}")]
74    CursorShape {
75        /// Cursor values.
76        actual: usize,
77        /// Query sort fields.
78        expected: usize,
79    },
80
81    /// Cursor keys must be nonempty.
82    #[error("cursor key must be nonempty")]
83    EmptyCursorKey,
84
85    /// Explicit null must use the canonical `None` cursor representation.
86    #[error("cursor contains a noncanonical explicit null sort value")]
87    NoncanonicalCursorNull,
88
89    /// A prefix filter literal is not a string or bytes value.
90    #[error("prefix filter requires a string or bytes literal")]
91    InvalidPrefixType,
92
93    /// Field-path segments must be nonempty UTF-8 strings.
94    #[error("field path contains an empty segment")]
95    InvalidFieldPath,
96
97    /// Too many group fields were requested.
98    #[error("aggregation has {actual} group fields; maximum is {maximum}")]
99    GroupFieldsExceeded {
100        /// Requested group fields.
101        actual: usize,
102        /// Configured maximum.
103        maximum: usize,
104    },
105
106    /// Too many metrics were requested.
107    #[error("aggregation has {actual} metrics; maximum is {maximum}")]
108    MetricsExceeded {
109        /// Requested metrics.
110        actual: usize,
111        /// Configured maximum.
112        maximum: usize,
113    },
114
115    /// Metric names must be nonempty.
116    #[error("aggregate metric name must be nonempty")]
117    EmptyMetricName,
118
119    /// Metric names must be unique within a plan.
120    #[error("duplicate aggregate metric name: {name}")]
121    DuplicateMetricName {
122        /// Duplicated name.
123        name: String,
124    },
125
126    /// Global scan budget was exhausted.
127    #[error("global scanned-record budget exceeded: {maximum}")]
128    ScannedBudgetExceeded {
129        /// Configured maximum.
130        maximum: u64,
131    },
132
133    /// Filtered-result memory budget was exhausted.
134    #[error("global matched-record budget exceeded: {maximum}")]
135    MatchedBudgetExceeded {
136        /// Configured maximum.
137        maximum: u64,
138    },
139
140    /// Aggregation group budget was exhausted.
141    #[error("aggregation group budget exceeded: {maximum}")]
142    GroupBudgetExceeded {
143        /// Configured maximum.
144        maximum: usize,
145    },
146
147    /// Cooperative monotonic deadline expired.
148    #[error("query execution timed out")]
149    TimedOut,
150
151    /// Sum encountered a non-integer value.
152    #[error("aggregate metric {name} requires integer values")]
153    MetricTypeMismatch {
154        /// Metric name.
155        name: String,
156    },
157
158    /// Checked aggregate arithmetic overflowed.
159    #[error("aggregate metric {name} overflowed")]
160    ArithmeticOverflow {
161        /// Metric name.
162        name: String,
163    },
164
165    /// Internal aggregate state did not match its validated metric plan.
166    #[error("aggregate metric state does not match its plan")]
167    MetricStateMismatch,
168}
169
170/// Failure from the additive query entry points that enforce an aggregate
171/// key/canonical-document byte budget.
172///
173/// [`QueryError`] remains the exhaustive error surface published in 0.2.0.
174/// This separate type adds byte-policy failures without invalidating legacy
175/// downstream matches.
176#[derive(Clone, Debug, Error, Eq, PartialEq)]
177pub enum BoundedQueryError {
178    /// Ordinary query validation or execution failed.
179    #[error(transparent)]
180    Query(#[from] QueryError),
181    /// Aggregate key and canonical document bytes exceeded policy.
182    #[error("global scanned-byte budget exceeded: {maximum}")]
183    ScannedByteBudgetExceeded {
184        /// Configured maximum.
185        maximum: u64,
186    },
187    /// An in-memory record cannot be represented as one canonical document.
188    #[error("query record is not a canonical document: {0}")]
189    RecordDocument(#[from] DocumentError),
190}
191
192/// Injectable monotonic clock used to make timeout semantics testable.
193pub trait MonotonicClock {
194    /// Returns a nondecreasing duration in an arbitrary local epoch.
195    fn now(&mut self) -> Duration;
196}
197
198/// Production monotonic clock backed by [`Instant`].
199#[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
218/// Executes one query over complete logical shards using the system clock.
219///
220/// # Errors
221///
222/// Returns a validation, budget, timeout, duplicate-key, or aggregation error.
223pub 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
231/// Executes one query under an explicit aggregate key/document byte budget.
232///
233/// # Errors
234///
235/// Returns a validation, budget, timeout, duplicate-key, document, or
236/// aggregation error.
237pub 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
252/// Executes one query with an injectable monotonic clock.
253///
254/// This is public so conformance suites can prove timeout behavior without
255/// wall-clock sleeps.
256///
257/// # Errors
258///
259/// Returns a validation, budget, timeout, duplicate-key, or aggregation error.
260pub 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
269/// Executes one query with an injectable monotonic clock and explicit
270/// aggregate key/document byte budget.
271///
272/// # Errors
273///
274/// Returns a validation, budget, timeout, duplicate-key, document, or
275/// aggregation error.
276pub 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
453/// Validates query shape and cursor canonicality without scanning records.
454///
455/// # Errors
456///
457/// Returns the same pre-execution validation errors as [`execute`].
458pub 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}