Skip to main content

icydb_core/db/query/builder/
text_projection.rs

1//! Module: query::builder::text_projection
2//! Responsibility: shared bounded text-function builder surface and function
3//! semantics used by fluent terminals and canonical SQL projection execution.
4//! Does not own: generic query planning, grouped semantics, or SQL parsing.
5//! Boundary: models the admitted text-function family on top of canonical
6//! planner expressions without reopening a generic function registry.
7
8use crate::{
9    db::{
10        QueryError,
11        executor::projection::eval_value_projection_expr_with_value,
12        query::{
13            builder::{
14                ValueProjectionExpr, scalar_projection::render_scalar_projection_expr_sql_label,
15            },
16            plan::expr::{Expr, FieldId, Function},
17        },
18    },
19    traits::FieldValue,
20};
21
22///
23/// TextProjectionExpr
24///
25/// Shared bounded text-function projection over one source field.
26/// This stays intentionally narrow even though it now lowers through the same
27/// canonical `Expr` surface used by SQL projection planning.
28///
29
30#[derive(Clone, Debug, Eq, PartialEq)]
31pub struct TextProjectionExpr {
32    field: String,
33    expr: Expr,
34}
35
36impl TextProjectionExpr {
37    // Build one field-function projection carrying only the source field.
38    pub(in crate::db) fn unary(field: impl Into<String>, function: Function) -> Self {
39        let field = field.into();
40
41        Self {
42            expr: Expr::FunctionCall {
43                function,
44                args: vec![Expr::Field(FieldId::new(field.clone()))],
45            },
46            field,
47        }
48    }
49
50    // Build one field-function projection carrying one literal argument.
51    pub(in crate::db) fn with_literal(
52        field: impl Into<String>,
53        function: Function,
54        literal: impl FieldValue,
55    ) -> Self {
56        let field = field.into();
57
58        Self {
59            expr: Expr::FunctionCall {
60                function,
61                args: vec![
62                    Expr::Field(FieldId::new(field.clone())),
63                    Expr::Literal(literal.to_value()),
64                ],
65            },
66            field,
67        }
68    }
69
70    // Build one field-function projection carrying two literal arguments.
71    pub(in crate::db) fn with_two_literals(
72        field: impl Into<String>,
73        function: Function,
74        literal: impl FieldValue,
75        literal2: impl FieldValue,
76    ) -> Self {
77        let field = field.into();
78
79        Self {
80            expr: Expr::FunctionCall {
81                function,
82                args: vec![
83                    Expr::Field(FieldId::new(field.clone())),
84                    Expr::Literal(literal.to_value()),
85                    Expr::Literal(literal2.to_value()),
86                ],
87            },
88            field,
89        }
90    }
91
92    // Build one `POSITION(literal, field)` projection.
93    pub(in crate::db) fn position(field: impl Into<String>, literal: impl FieldValue) -> Self {
94        let field = field.into();
95
96        Self {
97            expr: Expr::FunctionCall {
98                function: Function::Position,
99                args: vec![
100                    Expr::Literal(literal.to_value()),
101                    Expr::Field(FieldId::new(field.clone())),
102                ],
103            },
104            field,
105        }
106    }
107
108    /// Borrow the canonical planner expression carried by this helper.
109    #[must_use]
110    pub(in crate::db) const fn expr(&self) -> &Expr {
111        &self.expr
112    }
113}
114
115impl ValueProjectionExpr for TextProjectionExpr {
116    fn field(&self) -> &str {
117        self.field.as_str()
118    }
119
120    fn sql_label(&self) -> String {
121        render_scalar_projection_expr_sql_label(&self.expr)
122    }
123
124    fn apply_value(&self, value: crate::value::Value) -> Result<crate::value::Value, QueryError> {
125        eval_value_projection_expr_with_value(&self.expr, self.field.as_str(), &value)
126    }
127}
128
129/// Build `TRIM(field)`.
130#[must_use]
131pub fn trim(field: impl AsRef<str>) -> TextProjectionExpr {
132    TextProjectionExpr::unary(field.as_ref().to_string(), Function::Trim)
133}
134
135/// Build `LTRIM(field)`.
136#[must_use]
137pub fn ltrim(field: impl AsRef<str>) -> TextProjectionExpr {
138    TextProjectionExpr::unary(field.as_ref().to_string(), Function::Ltrim)
139}
140
141/// Build `RTRIM(field)`.
142#[must_use]
143pub fn rtrim(field: impl AsRef<str>) -> TextProjectionExpr {
144    TextProjectionExpr::unary(field.as_ref().to_string(), Function::Rtrim)
145}
146
147/// Build `LOWER(field)`.
148#[must_use]
149pub fn lower(field: impl AsRef<str>) -> TextProjectionExpr {
150    TextProjectionExpr::unary(field.as_ref().to_string(), Function::Lower)
151}
152
153/// Build `UPPER(field)`.
154#[must_use]
155pub fn upper(field: impl AsRef<str>) -> TextProjectionExpr {
156    TextProjectionExpr::unary(field.as_ref().to_string(), Function::Upper)
157}
158
159/// Build `LENGTH(field)`.
160#[must_use]
161pub fn length(field: impl AsRef<str>) -> TextProjectionExpr {
162    TextProjectionExpr::unary(field.as_ref().to_string(), Function::Length)
163}
164
165/// Build `LEFT(field, length)`.
166#[must_use]
167pub fn left(field: impl AsRef<str>, length: impl FieldValue) -> TextProjectionExpr {
168    TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::Left, length)
169}
170
171/// Build `RIGHT(field, length)`.
172#[must_use]
173pub fn right(field: impl AsRef<str>, length: impl FieldValue) -> TextProjectionExpr {
174    TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::Right, length)
175}
176
177/// Build `STARTS_WITH(field, literal)`.
178#[must_use]
179pub fn starts_with(field: impl AsRef<str>, literal: impl FieldValue) -> TextProjectionExpr {
180    TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::StartsWith, literal)
181}
182
183/// Build `ENDS_WITH(field, literal)`.
184#[must_use]
185pub fn ends_with(field: impl AsRef<str>, literal: impl FieldValue) -> TextProjectionExpr {
186    TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::EndsWith, literal)
187}
188
189/// Build `CONTAINS(field, literal)`.
190#[must_use]
191pub fn contains(field: impl AsRef<str>, literal: impl FieldValue) -> TextProjectionExpr {
192    TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::Contains, literal)
193}
194
195/// Build `POSITION(literal, field)`.
196#[must_use]
197pub fn position(field: impl AsRef<str>, literal: impl FieldValue) -> TextProjectionExpr {
198    TextProjectionExpr::position(field.as_ref().to_string(), literal)
199}
200
201/// Build `REPLACE(field, from, to)`.
202#[must_use]
203pub fn replace(
204    field: impl AsRef<str>,
205    from: impl FieldValue,
206    to: impl FieldValue,
207) -> TextProjectionExpr {
208    TextProjectionExpr::with_two_literals(field.as_ref().to_string(), Function::Replace, from, to)
209}
210
211/// Build `SUBSTRING(field, start)`.
212#[must_use]
213pub fn substring(field: impl AsRef<str>, start: impl FieldValue) -> TextProjectionExpr {
214    TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::Substring, start)
215}
216
217/// Build `SUBSTRING(field, start, length)`.
218#[must_use]
219pub fn substring_with_length(
220    field: impl AsRef<str>,
221    start: impl FieldValue,
222    length: impl FieldValue,
223) -> TextProjectionExpr {
224    TextProjectionExpr::with_two_literals(
225        field.as_ref().to_string(),
226        Function::Substring,
227        start,
228        length,
229    )
230}
231
232///
233/// TESTS
234///
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use crate::value::Value;
240
241    #[test]
242    fn lower_text_projection_renders_sql_label() {
243        assert_eq!(lower("name").sql_label(), "LOWER(name)");
244    }
245
246    #[test]
247    fn replace_text_projection_applies_shared_transform() {
248        let value = replace("name", "Ada", "Eve")
249            .apply_value(Value::Text("Ada Ada".to_string()))
250            .expect("replace projection should apply");
251
252        assert_eq!(value, Value::Text("Eve Eve".to_string()));
253    }
254}