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
10/// Apply the canonical case-insensitive text fold.
11///
12/// Casefolding remains a distinct semantic contract from SQL `LOWER`, even
13/// while both use Unicode lowercase conversion today. A future full Unicode
14/// casefold must not silently change `LOWER` or persisted index expressions.
15#[must_use]
16pub(crate) fn casefold_text(input: &str) -> String {
17    lowercase_text(input)
18}
19
20/// Apply the canonical `LOWER` transform used by query and index expressions.
21#[must_use]
22pub(crate) fn lower_text(input: &str) -> String {
23    lowercase_text(input)
24}
25
26/// Apply the canonical `UPPER` transform used by query and index expressions.
27#[must_use]
28pub(crate) fn upper_text(input: &str) -> String {
29    if input.is_ascii() {
30        return input.to_ascii_uppercase();
31    }
32
33    input.to_uppercase()
34}
35
36fn lowercase_text(input: &str) -> String {
37    if input.is_ascii() {
38        return input.to_ascii_lowercase();
39    }
40
41    input.to_lowercase()
42}
43
44fn text_with_mode(s: &'_ str, mode: TextMode) -> Cow<'_, str> {
45    match mode {
46        TextMode::Cs => Cow::Borrowed(s),
47        TextMode::Ci => Cow::Owned(casefold_text(s)),
48    }
49}
50
51fn text_op(
52    left: &Value,
53    right: &Value,
54    mode: TextMode,
55    f: impl Fn(&str, &str) -> bool,
56) -> Option<bool> {
57    let (a, b) = (left.as_text()?, right.as_text()?);
58    let a = text_with_mode(a, mode);
59    let b = text_with_mode(b, mode);
60    Some(f(&a, &b))
61}
62
63fn ci_key(value: &Value) -> Option<String> {
64    match value {
65        Value::Text(s) => Some(casefold_text(s)),
66        Value::Ulid(u) => Some(u.to_string().to_ascii_lowercase()),
67        Value::Principal(p) => Some(p.to_string().to_ascii_lowercase()),
68        Value::Account(a) => Some(a.to_string().to_ascii_lowercase()),
69        _ => None,
70    }
71}
72
73pub(super) fn eq_ci(left: &Value, right: &Value) -> bool {
74    if let (Some(left_key), Some(right_key)) = (ci_key(left), ci_key(right)) {
75        return left_key == right_key;
76    }
77
78    left == right
79}
80
81/// Case-sensitive/insensitive equality check for text-like values.
82#[must_use]
83fn text_eq(left: &Value, right: &Value, mode: TextMode) -> Option<bool> {
84    text_op(left, right, mode, |a, b| a == b)
85}
86
87/// Check whether `needle` is a substring of `value` under the given text mode.
88#[must_use]
89fn text_contains(value: &Value, needle: &Value, mode: TextMode) -> Option<bool> {
90    text_op(value, needle, mode, |a, b| a.contains(b))
91}
92
93/// Check whether `value` starts with `needle` under the given text mode.
94#[must_use]
95fn text_starts_with(value: &Value, needle: &Value, mode: TextMode) -> Option<bool> {
96    text_op(value, needle, mode, |a, b| a.starts_with(b))
97}
98
99/// Check whether `value` ends with `needle` under the given text mode.
100#[must_use]
101fn text_ends_with(value: &Value, needle: &Value, mode: TextMode) -> Option<bool> {
102    text_op(value, needle, mode, |a, b| a.ends_with(b))
103}
104
105impl Value {
106    /// Case-sensitive/insensitive equality check for text-like values.
107    #[must_use]
108    pub fn text_eq(&self, other: &Self, mode: TextMode) -> Option<bool> {
109        text_eq(self, other, mode)
110    }
111
112    /// Check whether `other` is a substring of `self` under the given text mode.
113    #[must_use]
114    pub fn text_contains(&self, needle: &Self, mode: TextMode) -> Option<bool> {
115        text_contains(self, needle, mode)
116    }
117
118    /// Check whether `self` starts with `other` under the given text mode.
119    #[must_use]
120    pub fn text_starts_with(&self, needle: &Self, mode: TextMode) -> Option<bool> {
121        text_starts_with(self, needle, mode)
122    }
123
124    /// Check whether `self` ends with `other` under the given text mode.
125    #[must_use]
126    pub fn text_ends_with(&self, needle: &Self, mode: TextMode) -> Option<bool> {
127        text_ends_with(self, needle, mode)
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::{casefold_text, lower_text, upper_text};
134
135    #[test]
136    fn canonical_text_transforms_preserve_current_ascii_and_unicode_semantics() {
137        assert_eq!(casefold_text("IcYDB"), "icydb");
138        assert_eq!(lower_text("IcYDB"), "icydb");
139        assert_eq!(upper_text("IcYDB"), "ICYDB");
140
141        assert_eq!(casefold_text("Straße"), "straße");
142        assert_eq!(lower_text("Straße"), "straße");
143        assert_eq!(upper_text("Straße"), "STRASSE");
144    }
145}