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::Binary { op, left, right } => {
59            let left = render_scalar_projection_expr_sql_label_with_parent(
60                left.as_ref(),
61                Some(*op),
62                false,
63            );
64            let right = render_scalar_projection_expr_sql_label_with_parent(
65                right.as_ref(),
66                Some(*op),
67                true,
68            );
69            let rendered = format!("{left} {} {right}", binary_op_sql_label(*op));
70
71            if binary_expr_requires_parentheses(*op, parent_op, is_right_child) {
72                format!("({rendered})")
73            } else {
74                rendered
75            }
76        }
77        Expr::Aggregate(aggregate) => {
78            let kind = aggregate.kind().sql_label();
79            let distinct = if aggregate.is_distinct() {
80                "DISTINCT "
81            } else {
82                ""
83            };
84
85            if let Some(input_expr) = aggregate.input_expr() {
86                let input =
87                    render_scalar_projection_expr_sql_label_with_parent(input_expr, None, false);
88
89                return format!("{kind}({distinct}{input})");
90            }
91
92            format!("{kind}({distinct}*)")
93        }
94        #[cfg(test)]
95        Expr::Alias { expr, .. } => render_scalar_projection_expr_sql_label_with_parent(
96            expr.as_ref(),
97            parent_op,
98            is_right_child,
99        ),
100        #[cfg(test)]
101        Expr::Unary { .. } => "expr".to_string(),
102    }
103}
104
105const fn binary_expr_requires_parentheses(
106    op: crate::db::query::plan::expr::BinaryOp,
107    parent_op: Option<crate::db::query::plan::expr::BinaryOp>,
108    is_right_child: bool,
109) -> bool {
110    let Some(parent_op) = parent_op else {
111        return false;
112    };
113    let precedence = binary_op_precedence(op);
114    let parent_precedence = binary_op_precedence(parent_op);
115
116    precedence < parent_precedence || (is_right_child && precedence == parent_precedence)
117}
118
119const fn binary_op_precedence(op: crate::db::query::plan::expr::BinaryOp) -> u8 {
120    match op {
121        crate::db::query::plan::expr::BinaryOp::Add
122        | crate::db::query::plan::expr::BinaryOp::Sub => 1,
123        crate::db::query::plan::expr::BinaryOp::Mul
124        | crate::db::query::plan::expr::BinaryOp::Div => 2,
125        #[cfg(test)]
126        crate::db::query::plan::expr::BinaryOp::And
127        | crate::db::query::plan::expr::BinaryOp::Eq => 0,
128    }
129}
130
131const fn binary_op_sql_label(op: crate::db::query::plan::expr::BinaryOp) -> &'static str {
132    match op {
133        crate::db::query::plan::expr::BinaryOp::Add => "+",
134        crate::db::query::plan::expr::BinaryOp::Sub => "-",
135        crate::db::query::plan::expr::BinaryOp::Mul => "*",
136        crate::db::query::plan::expr::BinaryOp::Div => "/",
137        #[cfg(test)]
138        crate::db::query::plan::expr::BinaryOp::And => "AND",
139        #[cfg(test)]
140        crate::db::query::plan::expr::BinaryOp::Eq => "=",
141    }
142}
143
144fn render_scalar_projection_literal(value: &Value) -> String {
145    match value {
146        Value::Null => "NULL".to_string(),
147        Value::Text(text) => format!("'{}'", text.replace('\'', "''")),
148        Value::Int(value) => value.to_string(),
149        Value::Int128(value) => value.to_string(),
150        Value::IntBig(value) => value.to_string(),
151        Value::Uint(value) => value.to_string(),
152        Value::Uint128(value) => value.to_string(),
153        Value::UintBig(value) => value.to_string(),
154        Value::Decimal(value) => value.to_string(),
155        Value::Float32(value) => value.to_string(),
156        Value::Float64(value) => value.to_string(),
157        Value::Bool(value) => value.to_string().to_uppercase(),
158        other => format!("{other:?}"),
159    }
160}