icydb_core/db/query/builder/
text_projection.rs1use 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#[derive(Clone, Debug, Eq, PartialEq)]
30pub struct TextProjectionExpr {
31 field: String,
32 expr: Expr,
33}
34
35impl TextProjectionExpr {
36 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 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 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 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 #[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#[must_use]
133pub fn trim(field: impl AsRef<str>) -> TextProjectionExpr {
134 TextProjectionExpr::unary(field.as_ref().to_string(), Function::Trim)
135}
136
137#[must_use]
139pub fn ltrim(field: impl AsRef<str>) -> TextProjectionExpr {
140 TextProjectionExpr::unary(field.as_ref().to_string(), Function::Ltrim)
141}
142
143#[must_use]
145pub fn rtrim(field: impl AsRef<str>) -> TextProjectionExpr {
146 TextProjectionExpr::unary(field.as_ref().to_string(), Function::Rtrim)
147}
148
149#[must_use]
151pub fn lower(field: impl AsRef<str>) -> TextProjectionExpr {
152 TextProjectionExpr::unary(field.as_ref().to_string(), Function::Lower)
153}
154
155#[must_use]
157pub fn upper(field: impl AsRef<str>) -> TextProjectionExpr {
158 TextProjectionExpr::unary(field.as_ref().to_string(), Function::Upper)
159}
160
161#[must_use]
163pub fn length(field: impl AsRef<str>) -> TextProjectionExpr {
164 TextProjectionExpr::unary(field.as_ref().to_string(), Function::Length)
165}
166
167#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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}