icydb_core/db/query/builder/
text_projection.rs1use crate::{
9 db::{
10 QueryError,
11 executor::projection::eval_text_projection_expr_with_value,
12 query::plan::expr::{Expr, FieldId, Function},
13 },
14 traits::FieldValue,
15 value::Value,
16};
17
18#[derive(Clone, Debug, Eq, PartialEq)]
27pub struct TextProjectionExpr {
28 field: String,
29 expr: Expr,
30}
31
32impl TextProjectionExpr {
33 pub(in crate::db) fn unary(field: impl Into<String>, function: Function) -> Self {
35 let field = field.into();
36
37 Self {
38 expr: Expr::FunctionCall {
39 function,
40 args: vec![Expr::Field(FieldId::new(field.clone()))],
41 },
42 field,
43 }
44 }
45
46 pub(in crate::db) fn with_literal(
48 field: impl Into<String>,
49 function: Function,
50 literal: impl FieldValue,
51 ) -> Self {
52 let field = field.into();
53
54 Self {
55 expr: Expr::FunctionCall {
56 function,
57 args: vec![
58 Expr::Field(FieldId::new(field.clone())),
59 Expr::Literal(literal.to_value()),
60 ],
61 },
62 field,
63 }
64 }
65
66 pub(in crate::db) fn with_two_literals(
68 field: impl Into<String>,
69 function: Function,
70 literal: impl FieldValue,
71 literal2: impl FieldValue,
72 ) -> Self {
73 let field = field.into();
74
75 Self {
76 expr: Expr::FunctionCall {
77 function,
78 args: vec![
79 Expr::Field(FieldId::new(field.clone())),
80 Expr::Literal(literal.to_value()),
81 Expr::Literal(literal2.to_value()),
82 ],
83 },
84 field,
85 }
86 }
87
88 pub(in crate::db) fn position(field: impl Into<String>, literal: impl FieldValue) -> Self {
90 let field = field.into();
91
92 Self {
93 expr: Expr::FunctionCall {
94 function: Function::Position,
95 args: vec![
96 Expr::Literal(literal.to_value()),
97 Expr::Field(FieldId::new(field.clone())),
98 ],
99 },
100 field,
101 }
102 }
103
104 #[must_use]
106 pub const fn field(&self) -> &str {
107 self.field.as_str()
108 }
109
110 #[must_use]
112 pub(in crate::db) const fn expr(&self) -> &Expr {
113 &self.expr
114 }
115
116 #[must_use]
118 pub fn sql_label(&self) -> String {
119 render_text_projection_expr_sql_label(&self.expr)
120 }
121
122 pub fn apply_value(&self, value: Value) -> Result<Value, QueryError> {
124 eval_text_projection_expr_with_value(&self.expr, self.field.as_str(), &value)
125 }
126}
127
128#[must_use]
131pub(in crate::db) fn render_text_projection_expr_sql_label(expr: &Expr) -> String {
132 match expr {
133 Expr::Field(field) => field.as_str().to_string(),
134 Expr::Literal(value) => render_text_projection_literal(value),
135 Expr::FunctionCall { function, args } => {
136 let rendered_args = args
137 .iter()
138 .map(render_text_projection_expr_sql_label)
139 .collect::<Vec<_>>()
140 .join(", ");
141
142 format!("{}({rendered_args})", function.sql_label())
143 }
144 Expr::Aggregate(_) => "aggregate".to_string(),
145 #[cfg(test)]
146 Expr::Alias { expr, .. } => render_text_projection_expr_sql_label(expr.as_ref()),
147 #[cfg(test)]
148 Expr::Unary { .. } | Expr::Binary { .. } => "expr".to_string(),
149 }
150}
151
152#[must_use]
154pub fn trim(field: impl AsRef<str>) -> TextProjectionExpr {
155 TextProjectionExpr::unary(field.as_ref().to_string(), Function::Trim)
156}
157
158#[must_use]
160pub fn ltrim(field: impl AsRef<str>) -> TextProjectionExpr {
161 TextProjectionExpr::unary(field.as_ref().to_string(), Function::Ltrim)
162}
163
164#[must_use]
166pub fn rtrim(field: impl AsRef<str>) -> TextProjectionExpr {
167 TextProjectionExpr::unary(field.as_ref().to_string(), Function::Rtrim)
168}
169
170#[must_use]
172pub fn lower(field: impl AsRef<str>) -> TextProjectionExpr {
173 TextProjectionExpr::unary(field.as_ref().to_string(), Function::Lower)
174}
175
176#[must_use]
178pub fn upper(field: impl AsRef<str>) -> TextProjectionExpr {
179 TextProjectionExpr::unary(field.as_ref().to_string(), Function::Upper)
180}
181
182#[must_use]
184pub fn length(field: impl AsRef<str>) -> TextProjectionExpr {
185 TextProjectionExpr::unary(field.as_ref().to_string(), Function::Length)
186}
187
188#[must_use]
190pub fn left(field: impl AsRef<str>, length: impl FieldValue) -> TextProjectionExpr {
191 TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::Left, length)
192}
193
194#[must_use]
196pub fn right(field: impl AsRef<str>, length: impl FieldValue) -> TextProjectionExpr {
197 TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::Right, length)
198}
199
200#[must_use]
202pub fn starts_with(field: impl AsRef<str>, literal: impl FieldValue) -> TextProjectionExpr {
203 TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::StartsWith, literal)
204}
205
206#[must_use]
208pub fn ends_with(field: impl AsRef<str>, literal: impl FieldValue) -> TextProjectionExpr {
209 TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::EndsWith, literal)
210}
211
212#[must_use]
214pub fn contains(field: impl AsRef<str>, literal: impl FieldValue) -> TextProjectionExpr {
215 TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::Contains, literal)
216}
217
218#[must_use]
220pub fn position(field: impl AsRef<str>, literal: impl FieldValue) -> TextProjectionExpr {
221 TextProjectionExpr::position(field.as_ref().to_string(), literal)
222}
223
224#[must_use]
226pub fn replace(
227 field: impl AsRef<str>,
228 from: impl FieldValue,
229 to: impl FieldValue,
230) -> TextProjectionExpr {
231 TextProjectionExpr::with_two_literals(field.as_ref().to_string(), Function::Replace, from, to)
232}
233
234#[must_use]
236pub fn substring(field: impl AsRef<str>, start: impl FieldValue) -> TextProjectionExpr {
237 TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::Substring, start)
238}
239
240#[must_use]
242pub fn substring_with_length(
243 field: impl AsRef<str>,
244 start: impl FieldValue,
245 length: impl FieldValue,
246) -> TextProjectionExpr {
247 TextProjectionExpr::with_two_literals(
248 field.as_ref().to_string(),
249 Function::Substring,
250 start,
251 length,
252 )
253}
254
255fn render_text_projection_literal(value: &Value) -> String {
257 match value {
258 Value::Null => "NULL".to_string(),
259 Value::Text(text) => format!("'{}'", text.replace('\'', "''")),
260 Value::Int(value) => value.to_string(),
261 Value::Uint(value) => value.to_string(),
262 _ => "<invalid-text-literal>".to_string(),
263 }
264}
265
266#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn lower_text_projection_renders_sql_label() {
276 assert_eq!(lower("name").sql_label(), "LOWER(name)");
277 }
278
279 #[test]
280 fn replace_text_projection_applies_shared_transform() {
281 let value = replace("name", "Ada", "Eve")
282 .apply_value(Value::Text("Ada Ada".to_string()))
283 .expect("replace projection should apply");
284
285 assert_eq!(value, Value::Text("Eve Eve".to_string()));
286 }
287}