icydb_core/db/query/builder/
scalar_projection.rs1use crate::{
10 db::{
11 QueryError,
12 query::plan::expr::{Expr, FieldPath},
13 },
14 value::Value,
15};
16
17pub(super) mod private {
18 pub trait Sealed {}
19}
20
21#[derive(Clone, Debug, Eq, PartialEq)]
31pub struct ScalarProjectionPlan {
32 expr: Expr,
33}
34
35impl ScalarProjectionPlan {
36 pub(in crate::db) const fn new(expr: Expr) -> Self {
37 Self { expr }
38 }
39
40 pub(in crate::db) fn into_expr(self) -> Expr {
41 self.expr
42 }
43}
44
45pub trait ValueProjectionExpr: private::Sealed {
55 fn field(&self) -> &str;
57
58 fn projection_plan(&self) -> ScalarProjectionPlan;
60
61 fn projection_label(&self) -> String;
63
64 fn apply_value(&self, value: Value) -> Result<Value, QueryError>;
66}
67
68#[must_use]
71pub(in crate::db) fn render_scalar_projection_expr_plan_label(expr: &Expr) -> String {
72 render_scalar_projection_expr_plan_label_with_parent(expr, None, false)
73}
74
75fn render_scalar_projection_expr_plan_label_with_parent(
76 expr: &Expr,
77 parent_op: Option<crate::db::query::plan::expr::BinaryOp>,
78 is_right_child: bool,
79) -> String {
80 match expr {
81 Expr::Field(field) => field.as_str().to_string(),
82 Expr::FieldPath(path) => render_field_path_plan_label(path),
83 Expr::Literal(value) => render_scalar_projection_literal(value),
84 Expr::FunctionCall { function, args } => {
85 let rendered_args = args
86 .iter()
87 .map(|arg| render_scalar_projection_expr_plan_label_with_parent(arg, None, false))
88 .collect::<Vec<_>>()
89 .join(", ");
90
91 format!("{}({rendered_args})", function.canonical_label())
92 }
93 Expr::Case {
94 when_then_arms,
95 else_expr,
96 } => render_case_projection_expr_plan_label(when_then_arms, else_expr.as_ref()),
97 Expr::Binary { op, left, right } => {
98 let left = render_scalar_projection_expr_plan_label_with_parent(
99 left.as_ref(),
100 Some(*op),
101 false,
102 );
103 let right = render_scalar_projection_expr_plan_label_with_parent(
104 right.as_ref(),
105 Some(*op),
106 true,
107 );
108 let rendered = format!("{left} {} {right}", binary_op_symbol(*op));
109
110 if binary_expr_requires_parentheses(*op, parent_op, is_right_child) {
111 format!("({rendered})")
112 } else {
113 rendered
114 }
115 }
116 Expr::Aggregate(aggregate) => {
117 let kind = aggregate.kind().canonical_label();
121 let distinct = if aggregate.is_distinct() {
122 "DISTINCT "
123 } else {
124 ""
125 };
126 let filter = aggregate.filter_expr().map(|filter_expr| {
127 format!(
128 " FILTER (WHERE {})",
129 render_scalar_projection_expr_plan_label_with_parent(filter_expr, None, false,)
130 )
131 });
132
133 if let Some(input_expr) = aggregate.input_expr() {
134 let input =
135 render_scalar_projection_expr_plan_label_with_parent(input_expr, None, false);
136
137 return format!("{kind}({distinct}{input}){}", filter.unwrap_or_default());
138 }
139
140 format!("{kind}({distinct}*){}", filter.unwrap_or_default())
141 }
142 #[cfg(test)]
143 Expr::Alias { expr, .. } => render_scalar_projection_expr_plan_label_with_parent(
144 expr.as_ref(),
145 parent_op,
146 is_right_child,
147 ),
148 Expr::Unary { op, expr } => {
149 let rendered =
150 render_scalar_projection_expr_plan_label_with_parent(expr.as_ref(), None, false);
151 match op {
152 crate::db::query::plan::expr::UnaryOp::Not => format!("NOT {rendered}"),
153 }
154 }
155 }
156}
157
158fn render_field_path_plan_label(path: &FieldPath) -> String {
159 let mut label = path.root().as_str().to_string();
160 for segment in path.segments() {
161 label.push('.');
162 label.push_str(segment);
163 }
164
165 label
166}
167
168fn render_case_projection_expr_plan_label(
169 when_then_arms: &[crate::db::query::plan::expr::CaseWhenArm],
170 else_expr: &Expr,
171) -> String {
172 let mut rendered = String::from("CASE");
173
174 for arm in when_then_arms {
175 rendered.push_str(" WHEN ");
176 rendered.push_str(
177 render_scalar_projection_expr_plan_label_with_parent(arm.condition(), None, false)
178 .as_str(),
179 );
180 rendered.push_str(" THEN ");
181 rendered.push_str(
182 render_scalar_projection_expr_plan_label_with_parent(arm.result(), None, false)
183 .as_str(),
184 );
185 }
186
187 rendered.push_str(" ELSE ");
188 rendered.push_str(
189 render_scalar_projection_expr_plan_label_with_parent(else_expr, None, false).as_str(),
190 );
191 rendered.push_str(" END");
192
193 rendered
194}
195
196const fn binary_expr_requires_parentheses(
197 op: crate::db::query::plan::expr::BinaryOp,
198 parent_op: Option<crate::db::query::plan::expr::BinaryOp>,
199 is_right_child: bool,
200) -> bool {
201 let Some(parent_op) = parent_op else {
202 return false;
203 };
204 let precedence = binary_op_precedence(op);
205 let parent_precedence = binary_op_precedence(parent_op);
206
207 precedence < parent_precedence || (is_right_child && precedence == parent_precedence)
208}
209
210const fn binary_op_precedence(op: crate::db::query::plan::expr::BinaryOp) -> u8 {
211 match op {
212 crate::db::query::plan::expr::BinaryOp::Or => 0,
213 crate::db::query::plan::expr::BinaryOp::And => 1,
214 crate::db::query::plan::expr::BinaryOp::Eq
215 | crate::db::query::plan::expr::BinaryOp::Ne
216 | crate::db::query::plan::expr::BinaryOp::Lt
217 | crate::db::query::plan::expr::BinaryOp::Lte
218 | crate::db::query::plan::expr::BinaryOp::Gt
219 | crate::db::query::plan::expr::BinaryOp::Gte => 2,
220 crate::db::query::plan::expr::BinaryOp::Add
221 | crate::db::query::plan::expr::BinaryOp::Sub => 3,
222 crate::db::query::plan::expr::BinaryOp::Mul
223 | crate::db::query::plan::expr::BinaryOp::Div => 4,
224 }
225}
226
227const fn binary_op_symbol(op: crate::db::query::plan::expr::BinaryOp) -> &'static str {
228 match op {
229 crate::db::query::plan::expr::BinaryOp::Or => "OR",
230 crate::db::query::plan::expr::BinaryOp::And => "AND",
231 crate::db::query::plan::expr::BinaryOp::Eq => "=",
232 crate::db::query::plan::expr::BinaryOp::Ne => "!=",
233 crate::db::query::plan::expr::BinaryOp::Lt => "<",
234 crate::db::query::plan::expr::BinaryOp::Lte => "<=",
235 crate::db::query::plan::expr::BinaryOp::Gt => ">",
236 crate::db::query::plan::expr::BinaryOp::Gte => ">=",
237 crate::db::query::plan::expr::BinaryOp::Add => "+",
238 crate::db::query::plan::expr::BinaryOp::Sub => "-",
239 crate::db::query::plan::expr::BinaryOp::Mul => "*",
240 crate::db::query::plan::expr::BinaryOp::Div => "/",
241 }
242}
243
244fn render_scalar_projection_literal(value: &Value) -> String {
245 match value {
246 Value::Null => "NULL".to_string(),
247 Value::Text(text) => format!("'{}'", text.replace('\'', "''")),
248 Value::Int64(value) => value.to_string(),
249 Value::Int128(value) => value.to_string(),
250 Value::IntBig(value) => value.to_string(),
251 Value::Nat64(value) => value.to_string(),
252 Value::Nat128(value) => value.to_string(),
253 Value::NatBig(value) => value.to_string(),
254 Value::Decimal(value) => value.to_string(),
255 Value::Float32(value) => value.to_string(),
256 Value::Float64(value) => value.to_string(),
257 Value::Bool(value) => value.to_string().to_uppercase(),
258 other => format!("{other:?}"),
259 }
260}