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