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                ScalarProjectionPlan, ValueProjectionExpr,
14                scalar_projection::render_scalar_projection_expr_plan_label,
15            },
16            plan::expr::{Expr, FieldId, Function, eval_builder_expr_for_value_preview},
17        },
18    },
19    value::{InputValue, Value},
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 structural 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 Into<InputValue>,
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(Value::from(literal.into())),
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 Into<InputValue>,
75        literal2: impl Into<InputValue>,
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(Value::from(literal.into())),
85                    Expr::Literal(Value::from(literal2.into())),
86                ],
87            },
88            field,
89        }
90    }
91
92    // Build one `POSITION(literal, field)` projection.
93    pub(in crate::db) fn position(
94        field: impl Into<String>,
95        literal: impl Into<InputValue>,
96    ) -> Self {
97        let field = field.into();
98
99        Self {
100            expr: Expr::FunctionCall {
101                function: Function::Position,
102                args: vec![
103                    Expr::Literal(Value::from(literal.into())),
104                    Expr::Field(FieldId::new(field.clone())),
105                ],
106            },
107            field,
108        }
109    }
110
111    /// Borrow the canonical planner expression carried by this helper.
112    #[must_use]
113    pub(in crate::db) const fn expr(&self) -> &Expr {
114        &self.expr
115    }
116}
117
118impl ValueProjectionExpr for TextProjectionExpr {
119    fn field(&self) -> &str {
120        self.field.as_str()
121    }
122
123    fn projection_plan(&self) -> ScalarProjectionPlan {
124        ScalarProjectionPlan::new(self.expr.clone())
125    }
126
127    fn projection_label(&self) -> String {
128        render_scalar_projection_expr_plan_label(&self.expr)
129    }
130
131    fn apply_value(&self, value: crate::value::Value) -> Result<crate::value::Value, QueryError> {
132        eval_builder_expr_for_value_preview(&self.expr, self.field.as_str(), &value)
133    }
134}
135
136/// Build `TRIM(field)`.
137#[must_use]
138pub fn trim(field: impl AsRef<str>) -> TextProjectionExpr {
139    TextProjectionExpr::unary(field.as_ref().to_string(), Function::Trim)
140}
141
142/// Build `LTRIM(field)`.
143#[must_use]
144pub fn ltrim(field: impl AsRef<str>) -> TextProjectionExpr {
145    TextProjectionExpr::unary(field.as_ref().to_string(), Function::Ltrim)
146}
147
148/// Build `RTRIM(field)`.
149#[must_use]
150pub fn rtrim(field: impl AsRef<str>) -> TextProjectionExpr {
151    TextProjectionExpr::unary(field.as_ref().to_string(), Function::Rtrim)
152}
153
154/// Build `LOWER(field)`.
155#[must_use]
156pub fn lower(field: impl AsRef<str>) -> TextProjectionExpr {
157    TextProjectionExpr::unary(field.as_ref().to_string(), Function::Lower)
158}
159
160/// Build `UPPER(field)`.
161#[must_use]
162pub fn upper(field: impl AsRef<str>) -> TextProjectionExpr {
163    TextProjectionExpr::unary(field.as_ref().to_string(), Function::Upper)
164}
165
166/// Build `LENGTH(field)`.
167#[must_use]
168pub fn length(field: impl AsRef<str>) -> TextProjectionExpr {
169    TextProjectionExpr::unary(field.as_ref().to_string(), Function::Length)
170}
171
172/// Build `LEFT(field, length)`.
173#[must_use]
174pub fn left(field: impl AsRef<str>, length: impl Into<InputValue>) -> TextProjectionExpr {
175    TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::Left, length)
176}
177
178/// Build `RIGHT(field, length)`.
179#[must_use]
180pub fn right(field: impl AsRef<str>, length: impl Into<InputValue>) -> TextProjectionExpr {
181    TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::Right, length)
182}
183
184/// Build `STARTS_WITH(field, literal)`.
185#[must_use]
186pub fn starts_with(field: impl AsRef<str>, literal: impl Into<InputValue>) -> TextProjectionExpr {
187    TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::StartsWith, literal)
188}
189
190/// Build `ENDS_WITH(field, literal)`.
191#[must_use]
192pub fn ends_with(field: impl AsRef<str>, literal: impl Into<InputValue>) -> TextProjectionExpr {
193    TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::EndsWith, literal)
194}
195
196/// Build `CONTAINS(field, literal)`.
197#[must_use]
198pub fn contains(field: impl AsRef<str>, literal: impl Into<InputValue>) -> TextProjectionExpr {
199    TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::Contains, literal)
200}
201
202/// Build `POSITION(literal, field)`.
203#[must_use]
204pub fn position(field: impl AsRef<str>, literal: impl Into<InputValue>) -> TextProjectionExpr {
205    TextProjectionExpr::position(field.as_ref().to_string(), literal)
206}
207
208/// Build `REPLACE(field, from, to)`.
209#[must_use]
210pub fn replace(
211    field: impl AsRef<str>,
212    from: impl Into<InputValue>,
213    to: impl Into<InputValue>,
214) -> TextProjectionExpr {
215    TextProjectionExpr::with_two_literals(field.as_ref().to_string(), Function::Replace, from, to)
216}
217
218/// Build `SUBSTRING(field, start)`.
219#[must_use]
220pub fn substring(field: impl AsRef<str>, start: impl Into<InputValue>) -> TextProjectionExpr {
221    TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::Substring, start)
222}
223
224/// Build `SUBSTRING(field, start, length)`.
225#[must_use]
226pub fn substring_with_length(
227    field: impl AsRef<str>,
228    start: impl Into<InputValue>,
229    length: impl Into<InputValue>,
230) -> TextProjectionExpr {
231    TextProjectionExpr::with_two_literals(
232        field.as_ref().to_string(),
233        Function::Substring,
234        start,
235        length,
236    )
237}
238
239///
240/// TESTS
241///
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246    use crate::value::Value;
247
248    #[test]
249    fn lower_text_projection_renders_projection_label() {
250        assert_eq!(lower("name").projection_label(), "LOWER(name)");
251    }
252
253    #[test]
254    fn replace_text_projection_applies_shared_transform() {
255        let value = replace("name", "Ada", "Eve")
256            .apply_value(Value::Text("Ada Ada".to_string()))
257            .expect("replace projection should apply");
258
259        assert_eq!(value, Value::Text("Eve Eve".to_string()));
260    }
261}