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}