icydb_core/db/executor/coerce/
family.rs1use crate::{
2 db::primitives::filter::Cmp,
3 types::{Account, Principal, Ulid},
4 value::{Value, ValueFamily, ValueFamilyExt},
5};
6use std::{cmp::Ordering, convert::TryFrom, str::FromStr};
7
8use super::text::coerce_text;
9
10pub struct FamilyPair {
14 pub left: ValueFamily,
15 pub right: ValueFamily,
16}
17
18impl FamilyPair {
19 #[must_use]
20 pub fn new(left: &Value, right: &Value) -> Self {
21 Self {
22 left: left.family(),
23 right: right.family(),
24 }
25 }
26}
27
28#[must_use]
32pub fn coerce_basic(left: &Value, right: &Value, cmp: Cmp) -> Option<bool> {
33 let pair = FamilyPair::new(left, right);
34
35 match (pair.left, pair.right) {
36 (ValueFamily::Numeric, ValueFamily::Numeric) => {
38 if let Some(ord) = left.cmp_numeric(right) {
39 return Some(cmp.compare_order(ord));
40 }
41
42 if let Some(ord) = cmp_collection_len(left, right) {
44 return Some(cmp.compare_order(ord));
45 }
46
47 None
48 }
49
50 (ValueFamily::Textual, ValueFamily::Textual) => coerce_text(left, right, cmp),
52
53 (ValueFamily::Enum, ValueFamily::Enum) => coerce_enum(left, right, cmp),
55
56 (ValueFamily::Collection, _) | (_, ValueFamily::Collection) => {
58 coerce_collection(left, right, cmp)
59 }
60
61 (ValueFamily::Identifier, ValueFamily::Textual)
63 | (ValueFamily::Textual, ValueFamily::Identifier) => {
64 coerce_identifier_text(left, right, cmp)
65 }
66
67 _ => None,
68 }
69}
70
71fn cmp_collection_len(left: &Value, right: &Value) -> Option<Ordering> {
75 match left {
76 Value::List(items) => {
77 let len = i64::try_from(items.len()).ok()?;
78 Value::Int(len).cmp_numeric(right)
79 }
80 _ => None,
81 }
82}
83
84fn coerce_identifier_text(left: &Value, right: &Value, cmp: Cmp) -> Option<bool> {
88 let parse_ulid = |s: &str| Ulid::from_str(s).ok();
89 let parse_principal = |s: &str| Principal::from_str(s).ok();
90 let parse_account = |s: &str| Account::from_str(s).ok();
91
92 let parsed_eq = match (left, right) {
93 (Value::Ulid(lhs), Value::Text(s)) | (Value::Text(s), Value::Ulid(lhs)) => {
94 parse_ulid(s).map(|rhs| rhs == *lhs)
95 }
96 (Value::Principal(lhs), Value::Text(s)) | (Value::Text(s), Value::Principal(lhs)) => {
97 parse_principal(s).map(|rhs| rhs == *lhs)
98 }
99 (Value::Account(lhs), Value::Text(s)) | (Value::Text(s), Value::Account(lhs)) => {
100 parse_account(s).map(|rhs| rhs == *lhs)
101 }
102 _ => None,
103 }?;
104
105 match cmp {
106 Cmp::Eq => Some(parsed_eq),
107 Cmp::Ne => Some(!parsed_eq),
108 _ => None,
109 }
110}
111
112fn coerce_enum(left: &Value, right: &Value, cmp: Cmp) -> Option<bool> {
116 let paths_compatible = |l: &Option<String>, r: &Option<String>| match (l, r) {
117 (Some(lp), Some(rp)) => lp == rp, _ => true, };
120 let variant_eq_ci = |a: &str, b: &str| {
121 if a.is_ascii() && b.is_ascii() {
122 a.eq_ignore_ascii_case(b)
123 } else {
124 a.to_lowercase() == b.to_lowercase()
125 }
126 };
127
128 match (left, right) {
129 (Value::Enum(l), Value::Enum(r)) if paths_compatible(&l.path, &r.path) => match cmp {
130 Cmp::Eq | Cmp::EqCi => Some(variant_eq_ci(&l.variant, &r.variant)),
131 Cmp::Ne | Cmp::NeCi => Some(!variant_eq_ci(&l.variant, &r.variant)),
132 _ => None,
133 },
134 _ => None,
135 }
136}
137
138fn coerce_collection(actual: &Value, expected: &Value, cmp: Cmp) -> Option<bool> {
142 match cmp {
143 Cmp::AllIn => actual.contains_all(expected),
144 Cmp::AnyIn => actual.contains_any(expected),
145 Cmp::Contains => actual.contains(expected),
146 Cmp::In => actual.in_list(expected),
147
148 Cmp::NotIn => actual.in_list(expected).map(|v| !v),
150
151 Cmp::AllInCi => actual.contains_all_ci(expected),
153 Cmp::AnyInCi => actual.contains_any_ci(expected),
154 Cmp::InCi => actual.in_list_ci(expected),
155
156 Cmp::IsEmpty => actual.is_empty(),
157 Cmp::IsNotEmpty => actual.is_not_empty(),
158 _ => None,
159 }
160}