Skip to main content

vantage_vista/
eval.rs

1//! Evaluating a [`VistaQuery`] over rows held in memory.
2//!
3//! Every caller that applies clauses to rows itself (a shell over a store it
4//! holds, a cache answering what its master refused, a client-side refine)
5//! uses this one evaluator, so a clause means the same thing wherever it is
6//! evaluated:
7//!
8//! - a cell is read by [`cell_get`], for conditions and order alike: a flat
9//!   key, or a dotted path into nested maps;
10//! - values compare by [`cbor_cmp`]: integers and floats numerically, a
11//!   datetime (`Tag(0, Text)`) as its text, and `3 == 3.0` for equality and
12//!   set membership;
13//! - a sort puts absent and null cells last in either direction, and breaks
14//!   ties on id;
15//! - search is a case-insensitive substring of the text of a column
16//!   [`VistaMetadata::search_columns`] names (every cell when the metadata
17//!   declares no columns): text, numbers, bools, tagged values such as
18//!   record ids, and the values nested in arrays and maps. An empty needle
19//!   matches every row.
20
21use std::cmp::Ordering;
22
23use ciborium::Value as CborValue;
24use vantage_core::{Result, error};
25use vantage_types::Record;
26
27use crate::{
28    filter::{FilterOp, operand_text},
29    metadata::VistaMetadata,
30    query::{Condition, VistaQuery},
31    sort::SortDirection,
32};
33
34mod compare;
35mod path;
36mod search;
37
38use compare::datetime_as_text;
39use search::matches_search;
40
41pub use compare::cbor_cmp;
42pub use path::cell_get;
43pub(crate) use path::path_get;
44
45type Rec = Record<CborValue>;
46
47impl Condition {
48    /// Whether `row` satisfies this condition, its cell read by
49    /// [`cell_get`]. Equality and set membership take an integer and a float
50    /// of the same number as equal (`3 == 3.0`); the ordered operators
51    /// compare by [`cbor_cmp`]. An absent cell matches only `Ne` and
52    /// `NotInSet`.
53    pub fn matches(&self, row: &Rec, metadata: &VistaMetadata) -> bool {
54        let cell = cell_get(row, &self.column, metadata);
55        let operand = &self.value;
56        match self.op {
57            FilterOp::Eq => cell.is_some_and(|c| same(c, operand)),
58            FilterOp::Ne => !cell.is_some_and(|c| same(c, operand)),
59            FilterOp::InSet => in_set(cell, operand),
60            FilterOp::NotInSet => !in_set(cell, operand),
61            FilterOp::Like => cell.is_some_and(|c| {
62                operand_text(c)
63                    .to_lowercase()
64                    .contains(&operand_text(operand).to_lowercase())
65            }),
66            op => cell.is_some_and(|c| op.matches_ordering(cbor_cmp(c, operand))),
67        }
68    }
69}
70
71impl VistaQuery {
72    /// Whether `row` satisfies every condition and the search. The order
73    /// doesn't narrow, so it is ignored.
74    ///
75    /// A native condition is opaque and can't be evaluated in memory: a
76    /// query holding any is an `IncorrectUsage` error. Check
77    /// [`is_locally_evaluable`](Self::is_locally_evaluable) first.
78    pub fn matches(&self, row: &Rec, metadata: &VistaMetadata) -> Result<bool> {
79        if !self.is_locally_evaluable() {
80            return Err(error!(
81                "native conditions can't be evaluated in memory",
82                native = self.native.join(", ")
83            )
84            .mark_incorrect_usage());
85        }
86        Ok(self.conditions.iter().all(|c| c.matches(row, metadata))
87            && self
88                .search
89                .as_deref()
90                .is_none_or(|needle| matches_search(needle, row, metadata)))
91    }
92
93    /// Sort `rows` by this query's order; without one, `rows` keep theirs.
94    /// A cell is read by [`cell_get`], as [`matches`](Self::matches) reads
95    /// it. An absent or null cell sorts last in both directions, and ties
96    /// break on id, so the same rows always come back in the same order.
97    pub fn sort_rows(&self, rows: &mut [(String, Rec)], metadata: &VistaMetadata) {
98        if self.order.is_some() {
99            rows.sort_by(|(a_id, a), (b_id, b)| self.cmp_rows((a_id, a), (b_id, b), metadata));
100        }
101    }
102
103    /// The order [`sort_rows`](Self::sort_rows) sorts by: the order column,
104    /// absent and null cells last in either direction, then ascending id.
105    /// Without an order only the id decides, so `Equal` means the same id.
106    /// Placing one row by a binary search over rows `sort_rows` sorted puts
107    /// it where a fresh sort would.
108    pub fn cmp_rows(
109        &self,
110        (a_id, a): (&str, &Rec),
111        (b_id, b): (&str, &Rec),
112        metadata: &VistaMetadata,
113    ) -> Ordering {
114        fn cell<'a>(row: &'a Rec, column: &str, metadata: &VistaMetadata) -> Option<&'a CborValue> {
115            cell_get(row, column, metadata).filter(|v| !v.is_null())
116        }
117        let ordering = match &self.order {
118            None => Ordering::Equal,
119            Some((column, dir)) => match (cell(a, column, metadata), cell(b, column, metadata)) {
120                (None, None) => Ordering::Equal,
121                (None, Some(_)) => Ordering::Greater,
122                (Some(_), None) => Ordering::Less,
123                (Some(l), Some(r)) => match dir {
124                    SortDirection::Ascending => cbor_cmp(l, r),
125                    SortDirection::Descending => cbor_cmp(r, l),
126                },
127            },
128        };
129        ordering.then_with(|| a_id.cmp(b_id))
130    }
131
132    /// Whether [`matches`](Self::matches) can evaluate this query: it holds
133    /// no native conditions.
134    pub fn is_locally_evaluable(&self) -> bool {
135        self.native.is_empty()
136    }
137}
138
139/// Equal values, an integer and a float of the same number, or a datetime
140/// and the same text.
141fn same(a: &CborValue, b: &CborValue) -> bool {
142    let (a, b) = (datetime_as_text(a), datetime_as_text(b));
143    let int_and_float = matches!(
144        (a, b),
145        (CborValue::Integer(_), CborValue::Float(_)) | (CborValue::Float(_), CborValue::Integer(_))
146    );
147    a == b || (int_and_float && cbor_cmp(a, b) == Ordering::Equal)
148}
149
150fn in_set(cell: Option<&CborValue>, operand: &CborValue) -> bool {
151    match (cell, operand) {
152        (Some(cell), CborValue::Array(items)) => items.iter().any(|item| same(cell, item)),
153        _ => false,
154    }
155}