Skip to main content

icydb_core/db/query/builder/
scalar_projection.rs

1//! Module: query::builder::scalar_projection
2//! Responsibility: shared outward scalar-projection contracts and stable SQL
3//! label rendering used by bounded projection helpers.
4//! Does not own: query planning, generic expression validation, or projection
5//! execution policy.
6//! Boundary: fluent helper projections share this contract so session and SQL
7//! surfaces can consume one stable projection-helper API.
8
9use crate::{
10    db::{QueryError, query::plan::expr::Expr},
11    value::Value,
12};
13
14///
15/// ValueProjectionExpr
16///
17/// Shared bounded scalar projection helper contract used by fluent
18/// value-projection terminals.
19/// Implementors stay intentionally narrow and do not imply a generic
20/// expression-builder surface.
21///
22
23pub trait ValueProjectionExpr {
24    /// Borrow the single source field used by this bounded helper.
25    fn field(&self) -> &str;
26
27    /// Render the stable SQL-style output label for this projection.
28    fn sql_label(&self) -> String;
29
30    /// Apply this projection to one already-loaded source value.
31    fn apply_value(&self, value: Value) -> Result<Value, QueryError>;
32}
33
34/// Render one canonical bounded scalar projection expression back into a
35/// stable SQL-style label.
36#[must_use]
37pub(in crate::db) fn render_scalar_projection_expr_sql_label(expr: &Expr) -> String {
38    render_scalar_projection_expr_sql_label_with_parent(expr, None, false)
39}
40
41fn render_scalar_projection_expr_sql_label_with_parent(
42    expr: &Expr,
43    parent_op: Option<crate::db::query::plan::expr::BinaryOp>,
44    is_right_child: bool,
45) -> String {
46    match expr {
47        Expr::Field(field) => field.as_str().to_string(),
48        Expr::Literal(value) => render_scalar_projection_literal(value),
49        Expr::FunctionCall { function, args } => {
50            let rendered_args = args
51                .iter()
52                .map(|arg| render_scalar_projection_expr_sql_label_with_parent(arg, None, false))
53                .collect::<Vec<_>>()
54                .join(", ");
55
56            format!("{}({rendered_args})", function.sql_label())
57        }
58        Expr::Case {
59            when_then_arms,
60            else_expr,
61        } => render_case_projection_expr_sql_label(when_then_arms, else_expr.as_ref()),
62        Expr::Binary { op, left, right } => {
63            let left = render_scalar_projection_expr_sql_label_with_parent(
64                left.as_ref(),
65                Some(*op),
66                false,
67            );
68            let right = render_scalar_projection_expr_sql_label_with_parent(
69                right.as_ref(),
70                Some(*op),
71                true,
72            );
73            let rendered = format!("{left} {} {right}", binary_op_sql_label(*op));
74
75            if binary_expr_requires_parentheses(*op, parent_op, is_right_child) {
76                format!("({rendered})")
77            } else {
78                rendered
79            }
80        }
81        Expr::Aggregate(aggregate) => {
82            // Preserve full aggregate identity, including FILTER semantics, so
83            // alias-normalized grouped HAVING/ORDER BY terms round-trip back
84            // onto the same planner aggregate expression shape.
85            let kind = aggregate.kind().sql_label();
86            let distinct = if aggregate.is_distinct() {
87                "DISTINCT "
88            } else {
89                ""
90            };
91            let filter = aggregate.filter_expr().map(|filter_expr| {
92                format!(
93                    " FILTER (WHERE {})",
94                    render_scalar_projection_expr_sql_label_with_parent(filter_expr, None, false,)
95                )
96            });
97
98            if let Some(input_expr) = aggregate.input_expr() {
99                let input =
100                    render_scalar_projection_expr_sql_label_with_parent(input_expr, None, false);
101
102                return format!("{kind}({distinct}{input}){}", filter.unwrap_or_default());
103            }
104
105            format!("{kind}({distinct}*){}", filter.unwrap_or_default())
106        }
107        #[cfg(test)]
108        Expr::Alias { expr, .. } => render_scalar_projection_expr_sql_label_with_parent(
109            expr.as_ref(),
110            parent_op,
111            is_right_child,
112        ),
113        Expr::Unary { op, expr } => {
114            let rendered =
115                render_scalar_projection_expr_sql_label_with_parent(expr.as_ref(), None, false);
116            match op {
117                crate::db::query::plan::expr::UnaryOp::Not => format!("NOT {rendered}"),
118            }
119        }
120    }
121}
122
123fn render_case_projection_expr_sql_label(
124    when_then_arms: &[crate::db::query::plan::expr::CaseWhenArm],
125    else_expr: &Expr,
126) -> String {
127    let mut rendered = String::from("CASE");
128
129    for arm in when_then_arms {
130        rendered.push_str(" WHEN ");
131        rendered.push_str(
132            render_scalar_projection_expr_sql_label_with_parent(arm.condition(), None, false)
133                .as_str(),
134        );
135        rendered.push_str(" THEN ");
136        rendered.push_str(
137            render_scalar_projection_expr_sql_label_with_parent(arm.result(), None, false).as_str(),
138        );
139    }
140
141    rendered.push_str(" ELSE ");
142    rendered.push_str(
143        render_scalar_projection_expr_sql_label_with_parent(else_expr, None, false).as_str(),
144    );
145    rendered.push_str(" END");
146
147    rendered
148}
149
150const fn binary_expr_requires_parentheses(
151    op: crate::db::query::plan::expr::BinaryOp,
152    parent_op: Option<crate::db::query::plan::expr::BinaryOp>,
153    is_right_child: bool,
154) -> bool {
155    let Some(parent_op) = parent_op else {
156        return false;
157    };
158    let precedence = binary_op_precedence(op);
159    let parent_precedence = binary_op_precedence(parent_op);
160
161    precedence < parent_precedence || (is_right_child && precedence == parent_precedence)
162}
163
164const fn binary_op_precedence(op: crate::db::query::plan::expr::BinaryOp) -> u8 {
165    match op {
166        crate::db::query::plan::expr::BinaryOp::Or => 0,
167        crate::db::query::plan::expr::BinaryOp::And => 1,
168        crate::db::query::plan::expr::BinaryOp::Eq
169        | crate::db::query::plan::expr::BinaryOp::Ne
170        | crate::db::query::plan::expr::BinaryOp::Lt
171        | crate::db::query::plan::expr::BinaryOp::Lte
172        | crate::db::query::plan::expr::BinaryOp::Gt
173        | crate::db::query::plan::expr::BinaryOp::Gte => 2,
174        crate::db::query::plan::expr::BinaryOp::Add
175        | crate::db::query::plan::expr::BinaryOp::Sub => 3,
176        crate::db::query::plan::expr::BinaryOp::Mul
177        | crate::db::query::plan::expr::BinaryOp::Div => 4,
178    }
179}
180
181const fn binary_op_sql_label(op: crate::db::query::plan::expr::BinaryOp) -> &'static str {
182    match op {
183        crate::db::query::plan::expr::BinaryOp::Or => "OR",
184        crate::db::query::plan::expr::BinaryOp::And => "AND",
185        crate::db::query::plan::expr::BinaryOp::Eq => "=",
186        crate::db::query::plan::expr::BinaryOp::Ne => "!=",
187        crate::db::query::plan::expr::BinaryOp::Lt => "<",
188        crate::db::query::plan::expr::BinaryOp::Lte => "<=",
189        crate::db::query::plan::expr::BinaryOp::Gt => ">",
190        crate::db::query::plan::expr::BinaryOp::Gte => ">=",
191        crate::db::query::plan::expr::BinaryOp::Add => "+",
192        crate::db::query::plan::expr::BinaryOp::Sub => "-",
193        crate::db::query::plan::expr::BinaryOp::Mul => "*",
194        crate::db::query::plan::expr::BinaryOp::Div => "/",
195    }
196}
197
198fn render_scalar_projection_literal(value: &Value) -> String {
199    match value {
200        Value::Null => "NULL".to_string(),
201        Value::Text(text) => format!("'{}'", text.replace('\'', "''")),
202        Value::Int(value) => value.to_string(),
203        Value::Int128(value) => value.to_string(),
204        Value::IntBig(value) => value.to_string(),
205        Value::Uint(value) => value.to_string(),
206        Value::Uint128(value) => value.to_string(),
207        Value::UintBig(value) => value.to_string(),
208        Value::Decimal(value) => value.to_string(),
209        Value::Float32(value) => value.to_string(),
210        Value::Float64(value) => value.to_string(),
211        Value::Bool(value) => value.to_string().to_uppercase(),
212        other => format!("{other:?}"),
213    }
214}