icydb_core/db/query/builder/
text_projection.rs1use 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#[derive(Clone, Debug, Eq, PartialEq)]
31pub struct TextProjectionExpr {
32 field: String,
33 expr: Expr,
34}
35
36impl TextProjectionExpr {
37 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 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 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 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 #[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#[must_use]
138pub fn trim(field: impl AsRef<str>) -> TextProjectionExpr {
139 TextProjectionExpr::unary(field.as_ref().to_string(), Function::Trim)
140}
141
142#[must_use]
144pub fn ltrim(field: impl AsRef<str>) -> TextProjectionExpr {
145 TextProjectionExpr::unary(field.as_ref().to_string(), Function::Ltrim)
146}
147
148#[must_use]
150pub fn rtrim(field: impl AsRef<str>) -> TextProjectionExpr {
151 TextProjectionExpr::unary(field.as_ref().to_string(), Function::Rtrim)
152}
153
154#[must_use]
156pub fn lower(field: impl AsRef<str>) -> TextProjectionExpr {
157 TextProjectionExpr::unary(field.as_ref().to_string(), Function::Lower)
158}
159
160#[must_use]
162pub fn upper(field: impl AsRef<str>) -> TextProjectionExpr {
163 TextProjectionExpr::unary(field.as_ref().to_string(), Function::Upper)
164}
165
166#[must_use]
168pub fn length(field: impl AsRef<str>) -> TextProjectionExpr {
169 TextProjectionExpr::unary(field.as_ref().to_string(), Function::Length)
170}
171
172#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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}