Skip to main content

icydb_core/value/ops/
text.rs

1//! Module: value::ops::text
2//!
3//! Responsibility: text and casefolded identifier operations for `Value`.
4//! Does not own: collection membership or predicate-level coercion policy.
5//! Boundary: representation-local text helpers used by query operators.
6
7use crate::value::{TextMode, Value};
8use std::borrow::Cow;
9
10pub(crate) fn fold_ci(s: &str) -> Cow<'_, str> {
11    if s.is_ascii() {
12        return Cow::Owned(s.to_ascii_lowercase());
13    }
14    // NOTE: Unicode fallback - temporary to_lowercase for non-ASCII.
15    // Future: replace with proper NFKC + full casefold when available.
16    Cow::Owned(s.to_lowercase())
17}
18
19fn text_with_mode(s: &'_ str, mode: TextMode) -> Cow<'_, str> {
20    match mode {
21        TextMode::Cs => Cow::Borrowed(s),
22        TextMode::Ci => fold_ci(s),
23    }
24}
25
26fn text_op(
27    left: &Value,
28    right: &Value,
29    mode: TextMode,
30    f: impl Fn(&str, &str) -> bool,
31) -> Option<bool> {
32    let (a, b) = (left.as_text()?, right.as_text()?);
33    let a = text_with_mode(a, mode);
34    let b = text_with_mode(b, mode);
35    Some(f(&a, &b))
36}
37
38pub(crate) fn ci_key(value: &Value) -> Option<String> {
39    match value {
40        Value::Text(s) => Some(fold_ci(s).into_owned()),
41        Value::Ulid(u) => Some(u.to_string().to_ascii_lowercase()),
42        Value::Principal(p) => Some(p.to_string().to_ascii_lowercase()),
43        Value::Account(a) => Some(a.to_string().to_ascii_lowercase()),
44        _ => None,
45    }
46}
47
48pub(crate) fn eq_ci(left: &Value, right: &Value) -> bool {
49    if let (Some(left_key), Some(right_key)) = (ci_key(left), ci_key(right)) {
50        return left_key == right_key;
51    }
52
53    left == right
54}
55
56/// Case-sensitive/insensitive equality check for text-like values.
57#[must_use]
58pub fn text_eq(left: &Value, right: &Value, mode: TextMode) -> Option<bool> {
59    text_op(left, right, mode, |a, b| a == b)
60}
61
62/// Check whether `needle` is a substring of `value` under the given text mode.
63#[must_use]
64pub fn text_contains(value: &Value, needle: &Value, mode: TextMode) -> Option<bool> {
65    text_op(value, needle, mode, |a, b| a.contains(b))
66}
67
68/// Check whether `value` starts with `needle` under the given text mode.
69#[must_use]
70pub fn text_starts_with(value: &Value, needle: &Value, mode: TextMode) -> Option<bool> {
71    text_op(value, needle, mode, |a, b| a.starts_with(b))
72}
73
74/// Check whether `value` ends with `needle` under the given text mode.
75#[must_use]
76pub fn text_ends_with(value: &Value, needle: &Value, mode: TextMode) -> Option<bool> {
77    text_op(value, needle, mode, |a, b| a.ends_with(b))
78}
79
80impl Value {
81    /// Case-sensitive/insensitive equality check for text-like values.
82    #[must_use]
83    pub fn text_eq(&self, other: &Self, mode: TextMode) -> Option<bool> {
84        text_eq(self, other, mode)
85    }
86
87    /// Check whether `other` is a substring of `self` under the given text mode.
88    #[must_use]
89    pub fn text_contains(&self, needle: &Self, mode: TextMode) -> Option<bool> {
90        text_contains(self, needle, mode)
91    }
92
93    /// Check whether `self` starts with `other` under the given text mode.
94    #[must_use]
95    pub fn text_starts_with(&self, needle: &Self, mode: TextMode) -> Option<bool> {
96        text_starts_with(self, needle, mode)
97    }
98
99    /// Check whether `self` ends with `other` under the given text mode.
100    #[must_use]
101    pub fn text_ends_with(&self, needle: &Self, mode: TextMode) -> Option<bool> {
102        text_ends_with(self, needle, mode)
103    }
104}