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