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            let kind = aggregate.kind().sql_label();
83            let distinct = if aggregate.is_distinct() {
84                "DISTINCT "
85            } else {
86                ""
87            };
88
89            if let Some(input_expr) = aggregate.input_expr() {
90                let input =
91                    render_scalar_projection_expr_sql_label_with_parent(input_expr, None, false);
92
93                return format!("{kind}({distinct}{input})");
94            }
95
96            format!("{kind}({distinct}*)")
97        }
98        #[cfg(test)]
99        Expr::Alias { expr, .. } => render_scalar_projection_expr_sql_label_with_parent(
100            expr.as_ref(),
101            parent_op,
102            is_right_child,
103        ),
104        Expr::Unary { op, expr } => {
105            let rendered =
106                render_scalar_projection_expr_sql_label_with_parent(expr.as_ref(), None, false);
107            match op {
108                crate::db::query::plan::expr::UnaryOp::Not => format!("NOT {rendered}"),
109            }
110        }
111    }
112}
113
114fn render_case_projection_expr_sql_label(
115    when_then_arms: &[crate::db::query::plan::expr::CaseWhenArm],
116    else_expr: &Expr,
117) -> String {
118    let mut rendered = String::from("CASE");
119
120    for arm in when_then_arms {
121        rendered.push_str(" WHEN ");
122        rendered.push_str(
123            render_scalar_projection_expr_sql_label_with_parent(arm.condition(), None, false)
124                .as_str(),
125        );
126        rendered.push_str(" THEN ");
127        rendered.push_str(
128            render_scalar_projection_expr_sql_label_with_parent(arm.result(), None, false).as_str(),
129        );
130    }
131
132    rendered.push_str(" ELSE ");
133    rendered.push_str(
134        render_scalar_projection_expr_sql_label_with_parent(else_expr, None, false).as_str(),
135    );
136    rendered.push_str(" END");
137
138    rendered
139}
140
141const fn binary_expr_requires_parentheses(
142    op: crate::db::query::plan::expr::BinaryOp,
143    parent_op: Option<crate::db::query::plan::expr::BinaryOp>,
144    is_right_child: bool,
145) -> bool {
146    let Some(parent_op) = parent_op else {
147        return false;
148    };
149    let precedence = binary_op_precedence(op);
150    let parent_precedence = binary_op_precedence(parent_op);
151
152    precedence < parent_precedence || (is_right_child && precedence == parent_precedence)
153}
154
155const fn binary_op_precedence(op: crate::db::query::plan::expr::BinaryOp) -> u8 {
156    match op {
157        crate::db::query::plan::expr::BinaryOp::Or => 0,
158        crate::db::query::plan::expr::BinaryOp::And => 1,
159        crate::db::query::plan::expr::BinaryOp::Eq
160        | crate::db::query::plan::expr::BinaryOp::Ne
161        | crate::db::query::plan::expr::BinaryOp::Lt
162        | crate::db::query::plan::expr::BinaryOp::Lte
163        | crate::db::query::plan::expr::BinaryOp::Gt
164        | crate::db::query::plan::expr::BinaryOp::Gte => 2,
165        crate::db::query::plan::expr::BinaryOp::Add
166        | crate::db::query::plan::expr::BinaryOp::Sub => 3,
167        crate::db::query::plan::expr::BinaryOp::Mul
168        | crate::db::query::plan::expr::BinaryOp::Div => 4,
169    }
170}
171
172const fn binary_op_sql_label(op: crate::db::query::plan::expr::BinaryOp) -> &'static str {
173    match op {
174        crate::db::query::plan::expr::BinaryOp::Or => "OR",
175        crate::db::query::plan::expr::BinaryOp::And => "AND",
176        crate::db::query::plan::expr::BinaryOp::Eq => "=",
177        crate::db::query::plan::expr::BinaryOp::Ne => "!=",
178        crate::db::query::plan::expr::BinaryOp::Lt => "<",
179        crate::db::query::plan::expr::BinaryOp::Lte => "<=",
180        crate::db::query::plan::expr::BinaryOp::Gt => ">",
181        crate::db::query::plan::expr::BinaryOp::Gte => ">=",
182        crate::db::query::plan::expr::BinaryOp::Add => "+",
183        crate::db::query::plan::expr::BinaryOp::Sub => "-",
184        crate::db::query::plan::expr::BinaryOp::Mul => "*",
185        crate::db::query::plan::expr::BinaryOp::Div => "/",
186    }
187}
188
189fn render_scalar_projection_literal(value: &Value) -> String {
190    match value {
191        Value::Null => "NULL".to_string(),
192        Value::Text(text) => format!("'{}'", text.replace('\'', "''")),
193        Value::Int(value) => value.to_string(),
194        Value::Int128(value) => value.to_string(),
195        Value::IntBig(value) => value.to_string(),
196        Value::Uint(value) => value.to_string(),
197        Value::Uint128(value) => value.to_string(),
198        Value::UintBig(value) => value.to_string(),
199        Value::Decimal(value) => value.to_string(),
200        Value::Float32(value) => value.to_string(),
201        Value::Float64(value) => value.to_string(),
202        Value::Bool(value) => value.to_string().to_uppercase(),
203        other => format!("{other:?}"),
204    }
205}