icydb_core/db/executor/coerce/
family.rs

1use 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
10///
11/// FamilyPair
12///
13pub 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///
29/// Basic coercion dispatcher (no NEW behaviour yet)
30///
31#[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        // numeric <-> numeric
37        (ValueFamily::Numeric, ValueFamily::Numeric) => {
38            if let Some(ord) = left.cmp_numeric(right) {
39                return Some(cmp.compare_order(ord));
40            }
41
42            // existing length-based ordering for collections
43            if let Some(ord) = cmp_collection_len(left, right) {
44                return Some(cmp.compare_order(ord));
45            }
46
47            None
48        }
49
50        // text <-> text (CS/CI handled in text module)
51        (ValueFamily::Textual, ValueFamily::Textual) => coerce_text(left, right, cmp),
52
53        // enum <-> enum
54        (ValueFamily::Enum, ValueFamily::Enum) => coerce_enum(left, right, cmp),
55
56        // collection membership
57        (ValueFamily::Collection, _) | (_, ValueFamily::Collection) => {
58            coerce_collection(left, right, cmp)
59        }
60
61        // identifier/text special case — ULID <-> Text only (existing behaviour)
62        (ValueFamily::Identifier, ValueFamily::Textual)
63        | (ValueFamily::Textual, ValueFamily::Identifier) => {
64            coerce_identifier_text(left, right, cmp)
65        }
66
67        _ => None,
68    }
69}
70
71///
72/// Collection length comparison (existing behaviour)
73///
74fn 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
84///
85/// Identifier <-> Text equality/inequality (Ulid/Principal/Account)
86///
87fn 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
112///
113/// Enum checking
114///
115fn 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, // strict: both specified and must match
118        _ => true,                        // loose: either side missing path → match on variant only
119    };
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
138///
139/// Collection membership using Value helpers
140///
141fn 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        // Negated membership
149        Cmp::NotIn => actual.in_list(expected).map(|v| !v),
150
151        // CI variants
152        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}