icydb_core/db/query/predicate/
coercion.rs1use crate::value::{CoercionFamily, TextMode, Value};
2use std::{cmp::Ordering, collections::BTreeMap, mem::discriminant};
3
4#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
28pub enum CoercionId {
29 Strict,
30 NumericWiden,
31 TextCasefold,
32 CollectionElement,
33}
34
35impl CoercionId {
36 #[must_use]
38 pub const fn plan_hash_tag(self) -> u8 {
39 match self {
40 Self::Strict => 0x01,
41 Self::NumericWiden => 0x02,
42 Self::TextCasefold => 0x04,
43 Self::CollectionElement => 0x05,
44 }
45 }
46}
47
48#[derive(Clone, Debug, Eq, PartialEq)]
59pub struct CoercionSpec {
60 pub id: CoercionId,
61 pub params: BTreeMap<String, String>,
62}
63
64impl CoercionSpec {
65 #[must_use]
66 pub const fn new(id: CoercionId) -> Self {
67 Self {
68 id,
69 params: BTreeMap::new(),
70 }
71 }
72}
73
74impl Default for CoercionSpec {
75 fn default() -> Self {
76 Self::new(CoercionId::Strict)
77 }
78}
79
80#[derive(Clone, Copy, Debug, Eq, PartialEq)]
87pub enum CoercionRuleFamily {
88 Any,
89 Family(CoercionFamily),
90}
91
92#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103pub struct CoercionRule {
104 pub left: CoercionRuleFamily,
105 pub right: CoercionRuleFamily,
106 pub id: CoercionId,
107}
108
109pub const COERCION_TABLE: &[CoercionRule] = &[
113 CoercionRule {
114 left: CoercionRuleFamily::Any,
115 right: CoercionRuleFamily::Any,
116 id: CoercionId::Strict,
117 },
118 CoercionRule {
119 left: CoercionRuleFamily::Family(CoercionFamily::Numeric),
120 right: CoercionRuleFamily::Family(CoercionFamily::Numeric),
121 id: CoercionId::NumericWiden,
122 },
123 CoercionRule {
124 left: CoercionRuleFamily::Family(CoercionFamily::Textual),
125 right: CoercionRuleFamily::Family(CoercionFamily::Textual),
126 id: CoercionId::TextCasefold,
127 },
128 CoercionRule {
129 left: CoercionRuleFamily::Any,
130 right: CoercionRuleFamily::Any,
131 id: CoercionId::CollectionElement,
132 },
133];
134
135#[must_use]
137pub fn supports_coercion(left: CoercionFamily, right: CoercionFamily, id: CoercionId) -> bool {
138 COERCION_TABLE.iter().any(|rule| {
139 rule.id == id && family_matches(rule.left, left) && family_matches(rule.right, right)
140 })
141}
142
143fn family_matches(rule: CoercionRuleFamily, value: CoercionFamily) -> bool {
144 match rule {
145 CoercionRuleFamily::Any => true,
146 CoercionRuleFamily::Family(expected) => expected == value,
147 }
148}
149
150#[derive(Clone, Copy, Debug, Eq, PartialEq)]
155pub enum TextOp {
156 Eq,
157 Contains,
158 StartsWith,
159 EndsWith,
160}
161
162#[must_use]
167pub fn compare_eq(left: &Value, right: &Value, coercion: &CoercionSpec) -> Option<bool> {
168 match coercion.id {
169 CoercionId::Strict | CoercionId::CollectionElement => {
170 same_variant(left, right).then_some(left == right)
171 }
172 CoercionId::NumericWiden => {
173 if !left.supports_numeric_coercion() || !right.supports_numeric_coercion() {
174 return None;
175 }
176
177 left.cmp_numeric(right).map(|ord| ord == Ordering::Equal)
178 }
179 CoercionId::TextCasefold => compare_casefold(left, right),
180 }
181}
182
183#[must_use]
188pub fn compare_order(left: &Value, right: &Value, coercion: &CoercionSpec) -> Option<Ordering> {
189 match coercion.id {
190 CoercionId::Strict | CoercionId::CollectionElement => {
191 if !same_variant(left, right) {
192 return None;
193 }
194 Value::strict_order_cmp(left, right)
195 }
196 CoercionId::NumericWiden => {
197 if !left.supports_numeric_coercion() || !right.supports_numeric_coercion() {
198 return None;
199 }
200
201 left.cmp_numeric(right)
202 }
203 CoercionId::TextCasefold => {
204 let left = casefold_value(left)?;
205 let right = casefold_value(right)?;
206 Some(left.cmp(&right))
207 }
208 }
209}
210
211#[must_use]
218pub(crate) fn canonical_cmp(left: &Value, right: &Value) -> Ordering {
219 if let Some(ordering) = Value::strict_order_cmp(left, right) {
220 return ordering;
221 }
222
223 left.canonical_rank().cmp(&right.canonical_rank())
224}
225
226#[must_use]
231pub fn compare_text(
232 left: &Value,
233 right: &Value,
234 coercion: &CoercionSpec,
235 op: TextOp,
236) -> Option<bool> {
237 if !matches!(left, Value::Text(_)) || !matches!(right, Value::Text(_)) {
238 return None;
240 }
241
242 let mode = match coercion.id {
243 CoercionId::Strict => TextMode::Cs,
244 CoercionId::TextCasefold => TextMode::Ci,
245 _ => return None,
246 };
247
248 match op {
249 TextOp::Eq => left.text_eq(right, mode),
250 TextOp::Contains => left.text_contains(right, mode),
251 TextOp::StartsWith => left.text_starts_with(right, mode),
252 TextOp::EndsWith => left.text_ends_with(right, mode),
253 }
254}
255
256fn same_variant(left: &Value, right: &Value) -> bool {
257 discriminant(left) == discriminant(right)
258}
259
260fn compare_casefold(left: &Value, right: &Value) -> Option<bool> {
261 let left = casefold_value(left)?;
262 let right = casefold_value(right)?;
263 Some(left == right)
264}
265
266fn casefold_value(value: &Value) -> Option<String> {
269 match value {
270 Value::Text(text) => Some(casefold(text)),
271 _ => {
273 None
275 }
276 }
277}
278
279fn casefold(input: &str) -> String {
280 if input.is_ascii() {
281 return input.to_ascii_lowercase();
282 }
283
284 input.to_lowercase()
286}
287
288#[cfg(test)]
289mod tests {
290 use super::canonical_cmp;
291 use crate::{types::Account, value::Value};
292 use std::cmp::Ordering;
293
294 #[test]
295 fn canonical_cmp_orders_accounts() {
296 let left = Value::Account(Account::dummy(1));
297 let right = Value::Account(Account::dummy(2));
298
299 assert_eq!(canonical_cmp(&left, &right), Ordering::Less);
300 assert_eq!(canonical_cmp(&right, &left), Ordering::Greater);
301 }
302
303 #[test]
304 fn canonical_cmp_is_total_for_mixed_variants() {
305 let left = Value::Account(Account::dummy(1));
306 let right = Value::Text("x".to_string());
307
308 assert_ne!(canonical_cmp(&left, &right), Ordering::Equal);
309 assert_eq!(
310 canonical_cmp(&left, &right),
311 canonical_cmp(&right, &left).reverse()
312 );
313 }
314}