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(crate) enum CoercionRuleFamily {
88 Any,
89 Family(CoercionFamily),
90}
91
92#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103pub(crate) struct CoercionRule {
104 pub left: CoercionRuleFamily,
105 pub right: CoercionRuleFamily,
106 pub id: CoercionId,
107}
108
109pub(crate) 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(crate) fn supports_coercion(
138 left: CoercionFamily,
139 right: CoercionFamily,
140 id: CoercionId,
141) -> bool {
142 COERCION_TABLE.iter().any(|rule| {
143 rule.id == id && family_matches(rule.left, left) && family_matches(rule.right, right)
144 })
145}
146
147fn family_matches(rule: CoercionRuleFamily, value: CoercionFamily) -> bool {
148 match rule {
149 CoercionRuleFamily::Any => true,
150 CoercionRuleFamily::Family(expected) => expected == value,
151 }
152}
153
154#[derive(Clone, Copy, Debug, Eq, PartialEq)]
159pub(crate) enum TextOp {
160 StartsWith,
161 EndsWith,
162}
163
164#[must_use]
169pub(crate) fn compare_eq(left: &Value, right: &Value, coercion: &CoercionSpec) -> Option<bool> {
170 match coercion.id {
171 CoercionId::Strict | CoercionId::CollectionElement => {
172 same_variant(left, right).then_some(left == right)
173 }
174 CoercionId::NumericWiden => {
175 if !left.supports_numeric_coercion() || !right.supports_numeric_coercion() {
176 return None;
177 }
178
179 left.cmp_numeric(right).map(|ord| ord == Ordering::Equal)
180 }
181 CoercionId::TextCasefold => compare_casefold(left, right),
182 }
183}
184
185#[must_use]
190pub(crate) fn compare_order(
191 left: &Value,
192 right: &Value,
193 coercion: &CoercionSpec,
194) -> Option<Ordering> {
195 match coercion.id {
196 CoercionId::Strict | CoercionId::CollectionElement => {
197 if !same_variant(left, right) {
198 return None;
199 }
200 Value::strict_order_cmp(left, right)
201 }
202 CoercionId::NumericWiden => {
203 if !left.supports_numeric_coercion() || !right.supports_numeric_coercion() {
204 return None;
205 }
206
207 left.cmp_numeric(right)
208 }
209 CoercionId::TextCasefold => {
210 let left = casefold_value(left)?;
211 let right = casefold_value(right)?;
212 Some(left.cmp(&right))
213 }
214 }
215}
216
217#[must_use]
224pub(crate) fn canonical_cmp(left: &Value, right: &Value) -> Ordering {
225 if let Some(ordering) = Value::strict_order_cmp(left, right) {
226 return ordering;
227 }
228
229 left.canonical_rank().cmp(&right.canonical_rank())
230}
231
232#[must_use]
237pub(crate) fn compare_text(
238 left: &Value,
239 right: &Value,
240 coercion: &CoercionSpec,
241 op: TextOp,
242) -> Option<bool> {
243 if !matches!(left, Value::Text(_)) || !matches!(right, Value::Text(_)) {
244 return None;
246 }
247
248 let mode = match coercion.id {
249 CoercionId::Strict => TextMode::Cs,
250 CoercionId::TextCasefold => TextMode::Ci,
251 _ => return None,
252 };
253
254 match op {
255 TextOp::StartsWith => left.text_starts_with(right, mode),
256 TextOp::EndsWith => left.text_ends_with(right, mode),
257 }
258}
259
260fn same_variant(left: &Value, right: &Value) -> bool {
261 discriminant(left) == discriminant(right)
262}
263
264fn compare_casefold(left: &Value, right: &Value) -> Option<bool> {
265 let left = casefold_value(left)?;
266 let right = casefold_value(right)?;
267 Some(left == right)
268}
269
270fn casefold_value(value: &Value) -> Option<String> {
273 match value {
274 Value::Text(text) => Some(casefold(text)),
275 _ => {
277 None
279 }
280 }
281}
282
283fn casefold(input: &str) -> String {
284 if input.is_ascii() {
285 return input.to_ascii_lowercase();
286 }
287
288 input.to_lowercase()
290}
291
292#[cfg(test)]
293mod tests {
294 use super::canonical_cmp;
295 use crate::{types::Account, value::Value};
296 use std::cmp::Ordering;
297
298 #[test]
299 fn canonical_cmp_orders_accounts() {
300 let left = Value::Account(Account::dummy(1));
301 let right = Value::Account(Account::dummy(2));
302
303 assert_eq!(canonical_cmp(&left, &right), Ordering::Less);
304 assert_eq!(canonical_cmp(&right, &left), Ordering::Greater);
305 }
306
307 #[test]
308 fn canonical_cmp_is_total_for_mixed_variants() {
309 let left = Value::Account(Account::dummy(1));
310 let right = Value::Text("x".to_string());
311
312 assert_ne!(canonical_cmp(&left, &right), Ordering::Equal);
313 assert_eq!(
314 canonical_cmp(&left, &right),
315 canonical_cmp(&right, &left).reverse()
316 );
317 }
318}