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    match expr {
39        Expr::Field(field) => field.as_str().to_string(),
40        Expr::Literal(value) => render_scalar_projection_literal(value),
41        Expr::FunctionCall { function, args } => {
42            let rendered_args = args
43                .iter()
44                .map(render_scalar_projection_expr_sql_label)
45                .collect::<Vec<_>>()
46                .join(", ");
47
48            format!("{}({rendered_args})", function.sql_label())
49        }
50        Expr::Binary { op, left, right } => {
51            let left = render_scalar_projection_expr_sql_label(left.as_ref());
52            let right = render_scalar_projection_expr_sql_label(right.as_ref());
53
54            format!("{left} {} {right}", binary_op_sql_label(*op))
55        }
56        Expr::Aggregate(aggregate) => {
57            let kind = aggregate.kind().sql_label();
58            let distinct = if aggregate.is_distinct() {
59                "DISTINCT "
60            } else {
61                ""
62            };
63
64            if let Some(field) = aggregate.target_field() {
65                return format!("{kind}({distinct}{field})");
66            }
67
68            format!("{kind}({distinct}*)")
69        }
70        #[cfg(test)]
71        Expr::Alias { expr, .. } => render_scalar_projection_expr_sql_label(expr.as_ref()),
72        #[cfg(test)]
73        Expr::Unary { .. } => "expr".to_string(),
74    }
75}
76
77const fn binary_op_sql_label(op: crate::db::query::plan::expr::BinaryOp) -> &'static str {
78    match op {
79        crate::db::query::plan::expr::BinaryOp::Add => "+",
80        crate::db::query::plan::expr::BinaryOp::Sub => "-",
81        crate::db::query::plan::expr::BinaryOp::Mul => "*",
82        crate::db::query::plan::expr::BinaryOp::Div => "/",
83        #[cfg(test)]
84        crate::db::query::plan::expr::BinaryOp::And => "AND",
85        #[cfg(test)]
86        crate::db::query::plan::expr::BinaryOp::Eq => "=",
87    }
88}
89
90fn render_scalar_projection_literal(value: &Value) -> String {
91    match value {
92        Value::Null => "NULL".to_string(),
93        Value::Text(text) => format!("'{}'", text.replace('\'', "''")),
94        Value::Int(value) => value.to_string(),
95        Value::Int128(value) => value.to_string(),
96        Value::IntBig(value) => value.to_string(),
97        Value::Uint(value) => value.to_string(),
98        Value::Uint128(value) => value.to_string(),
99        Value::UintBig(value) => value.to_string(),
100        Value::Decimal(value) => value.to_string(),
101        Value::Float32(value) => value.to_string(),
102        Value::Float64(value) => value.to_string(),
103        Value::Bool(value) => value.to_string().to_uppercase(),
104        other => format!("{other:?}"),
105    }
106}