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::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<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 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 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 #[must_use]
110 pub(in crate::db) const fn expr(&self) -> &Expr {
111 &self.expr
112 }
113}
114
115impl super::scalar_projection::private::Sealed for TextProjectionExpr {}
116
117impl ValueProjectionExpr for TextProjectionExpr {
118 fn field(&self) -> &str {
119 self.field.as_str()
120 }
121
122 fn projection_plan(&self) -> ScalarProjectionPlan {
123 ScalarProjectionPlan::new(self.expr.clone())
124 }
125
126 fn projection_label(&self) -> String {
127 render_scalar_projection_expr_plan_label(&self.expr)
128 }
129
130 fn apply_value(&self, value: crate::value::Value) -> Result<crate::value::Value, QueryError> {
131 eval_builder_expr_for_value_preview(&self.expr, self.field.as_str(), &value)
132 }
133}
134
135#[must_use]
137pub fn trim(field: impl AsRef<str>) -> TextProjectionExpr {
138 TextProjectionExpr::unary(field.as_ref().to_string(), Function::Trim)
139}
140
141#[must_use]
143pub fn ltrim(field: impl AsRef<str>) -> TextProjectionExpr {
144 TextProjectionExpr::unary(field.as_ref().to_string(), Function::Ltrim)
145}
146
147#[must_use]
149pub fn rtrim(field: impl AsRef<str>) -> TextProjectionExpr {
150 TextProjectionExpr::unary(field.as_ref().to_string(), Function::Rtrim)
151}
152
153#[must_use]
155pub fn lower(field: impl AsRef<str>) -> TextProjectionExpr {
156 TextProjectionExpr::unary(field.as_ref().to_string(), Function::Lower)
157}
158
159#[must_use]
161pub fn upper(field: impl AsRef<str>) -> TextProjectionExpr {
162 TextProjectionExpr::unary(field.as_ref().to_string(), Function::Upper)
163}
164
165#[must_use]
167pub fn length(field: impl AsRef<str>) -> TextProjectionExpr {
168 TextProjectionExpr::unary(field.as_ref().to_string(), Function::Length)
169}
170
171#[must_use]
173pub fn left(field: impl AsRef<str>, length: impl Into<Value>) -> TextProjectionExpr {
174 TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::Left, length)
175}
176
177#[must_use]
179pub fn right(field: impl AsRef<str>, length: impl Into<Value>) -> TextProjectionExpr {
180 TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::Right, length)
181}
182
183#[must_use]
185pub fn starts_with(field: impl AsRef<str>, literal: impl Into<Value>) -> TextProjectionExpr {
186 TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::StartsWith, literal)
187}
188
189#[must_use]
191pub fn ends_with(field: impl AsRef<str>, literal: impl Into<Value>) -> TextProjectionExpr {
192 TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::EndsWith, literal)
193}
194
195#[must_use]
197pub fn contains(field: impl AsRef<str>, literal: impl Into<Value>) -> TextProjectionExpr {
198 TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::Contains, literal)
199}
200
201#[must_use]
203pub fn position(field: impl AsRef<str>, literal: impl Into<Value>) -> TextProjectionExpr {
204 TextProjectionExpr::position(field.as_ref().to_string(), literal)
205}
206
207#[must_use]
209pub fn replace(
210 field: impl AsRef<str>,
211 from: impl Into<Value>,
212 to: impl Into<Value>,
213) -> TextProjectionExpr {
214 TextProjectionExpr::with_two_literals(field.as_ref().to_string(), Function::Replace, from, to)
215}
216
217#[must_use]
219pub fn substring(field: impl AsRef<str>, start: impl Into<Value>) -> TextProjectionExpr {
220 TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::Substring, start)
221}
222
223#[must_use]
225pub fn substring_with_length(
226 field: impl AsRef<str>,
227 start: impl Into<Value>,
228 length: impl Into<Value>,
229) -> TextProjectionExpr {
230 TextProjectionExpr::with_two_literals(
231 field.as_ref().to_string(),
232 Function::Substring,
233 start,
234 length,
235 )
236}
237
238#[cfg(test)]
243mod tests {
244 use super::*;
245 use crate::value::Value;
246
247 #[test]
248 fn lower_text_projection_renders_projection_label() {
249 assert_eq!(lower("name").projection_label(), "LOWER(name)");
250 }
251
252 #[test]
253 fn replace_text_projection_applies_shared_transform() {
254 let value = replace("name", "Ada", "Eve")
255 .apply_value(Value::Text("Ada Ada".to_string()))
256 .expect("replace projection should apply");
257
258 assert_eq!(value, Value::Text("Eve Eve".to_string()));
259 }
260}