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