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    match (left, right) {
117        (Value::Enum(l), Value::Enum(r)) if l.path == r.path => match cmp {
118            Cmp::Eq => Some(l.variant == r.variant),
119            Cmp::Ne => Some(l.variant != r.variant),
120            _ => None,
121        },
122        _ => None,
123    }
124}
125
126///
127/// Collection membership using Value helpers
128///
129fn coerce_collection(actual: &Value, expected: &Value, cmp: Cmp) -> Option<bool> {
130    match cmp {
131        Cmp::AllIn => actual.contains_all(expected),
132        Cmp::AnyIn => actual.contains_any(expected),
133        Cmp::Contains => actual.contains(expected),
134        Cmp::In => actual.in_list(expected),
135
136        // Negated membership
137        Cmp::NotIn => actual.in_list(expected).map(|v| !v),
138
139        // CI variants
140        Cmp::AllInCi => actual.contains_all_ci(expected),
141        Cmp::AnyInCi => actual.contains_any_ci(expected),
142        Cmp::InCi => actual.in_list_ci(expected),
143
144        Cmp::IsEmpty => actual.is_empty(),
145        Cmp::IsNotEmpty => actual.is_not_empty(),
146        _ => None,
147    }
148}