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 plan
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 adapter surfaces
7//! can consume one stable projection-helper API.
8
9use crate::{
10    db::{QueryError, query::plan::expr::Expr},
11    value::Value,
12};
13
14pub(super) mod private {
15    pub trait Sealed {}
16}
17
18///
19/// ScalarProjectionPlan
20///
21/// Public opaque projection-plan token carried by bounded fluent projection
22/// helpers.
23/// The expression stays private to the query/executor boundary, while the token
24/// lets fluent terminals move projection work below the public terminal layer.
25///
26
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct ScalarProjectionPlan {
29    expr: Expr,
30}
31
32impl ScalarProjectionPlan {
33    pub(in crate::db) const fn new(expr: Expr) -> Self {
34        Self { expr }
35    }
36}
37
38///
39/// ValueProjectionExpr
40///
41/// Shared bounded scalar projection helper contract used by fluent
42/// value-projection terminals.
43/// Implementors are sealed to the maintained numeric, rounded, and text helper
44/// types and do not imply a generic expression-builder surface.
45///
46
47pub trait ValueProjectionExpr: private::Sealed {
48    /// Borrow the single source field used by this bounded helper.
49    fn field(&self) -> &str;
50
51    /// Borrow the canonical planner expression carried by this helper.
52    fn projection_plan(&self) -> ScalarProjectionPlan;
53
54    /// Render the stable canonical output label for this projection.
55    fn projection_label(&self) -> String;
56
57    /// Apply this projection to one already-loaded source value.
58    fn apply_value(&self, value: Value) -> Result<Value, QueryError>;
59}
60
61/// Render one canonical bounded scalar projection expression back into a
62/// stable plan label.
63#[must_use]
64pub(in crate::db) fn render_scalar_projection_expr_plan_label(expr: &Expr) -> String {
65    render_scalar_projection_expr_plan_label_with_parent(expr, None, false)
66}
67
68fn render_scalar_projection_expr_plan_label_with_parent(
69    expr: &Expr,
70    parent_op: Option<crate::db::query::plan::expr::BinaryOp>,
71    is_right_child: bool,
72) -> String {
73    match expr {
74        Expr::Field(field) => field.as_str().to_string(),
75        Expr::FieldPath(path) => path.path_spec().dotted_label(),
76        Expr::Literal(value) => render_scalar_projection_literal(value),
77        Expr::FunctionCall { function, args } => {
78            let rendered_args = args
79                .iter()
80                .map(|arg| render_scalar_projection_expr_plan_label_with_parent(arg, None, false))
81                .collect::<Vec<_>>()
82                .join(", ");
83
84            format!("{}({rendered_args})", function.canonical_label())
85        }
86        Expr::Case {
87            when_then_arms,
88            else_expr,
89        } => render_case_projection_expr_plan_label(when_then_arms, else_expr.as_ref()),
90        Expr::Binary { op, left, right } => {
91            let left = render_scalar_projection_expr_plan_label_with_parent(
92                left.as_ref(),
93                Some(*op),
94                false,
95            );
96            let right = render_scalar_projection_expr_plan_label_with_parent(
97                right.as_ref(),
98                Some(*op),
99                true,
100            );
101            let rendered = format!("{left} {} {right}", binary_op_symbol(*op));
102
103            if binary_expr_requires_parentheses(*op, parent_op, is_right_child) {
104                format!("({rendered})")
105            } else {
106                rendered
107            }
108        }
109        Expr::Aggregate(aggregate) => {
110            // Preserve full aggregate identity, including FILTER semantics, so
111            // alias-normalized grouped HAVING/ORDER BY terms round-trip back
112            // onto the same planner aggregate expression shape.
113            let kind = aggregate.kind().canonical_label();
114            let distinct = if aggregate.is_distinct() {
115                "DISTINCT "
116            } else {
117                ""
118            };
119            let filter = aggregate.filter_expr().map(|filter_expr| {
120                format!(
121                    " FILTER (WHERE {})",
122                    render_scalar_projection_expr_plan_label_with_parent(filter_expr, None, false,)
123                )
124            });
125
126            if let Some(input_expr) = aggregate.input_expr() {
127                let input =
128                    render_scalar_projection_expr_plan_label_with_parent(input_expr, None, false);
129
130                return format!("{kind}({distinct}{input}){}", filter.unwrap_or_default());
131            }
132
133            format!("{kind}({distinct}*){}", filter.unwrap_or_default())
134        }
135        #[cfg(test)]
136        Expr::Alias { expr, .. } => render_scalar_projection_expr_plan_label_with_parent(
137            expr.as_ref(),
138            parent_op,
139            is_right_child,
140        ),
141        Expr::Unary { op, expr } => {
142            let rendered =
143                render_scalar_projection_expr_plan_label_with_parent(expr.as_ref(), None, false);
144            match op {
145                crate::db::query::plan::expr::UnaryOp::Not => format!("NOT {rendered}"),
146            }
147        }
148    }
149}
150
151fn render_case_projection_expr_plan_label(
152    when_then_arms: &[crate::db::query::plan::expr::CaseWhenArm],
153    else_expr: &Expr,
154) -> String {
155    let mut rendered = String::from("CASE");
156
157    for arm in when_then_arms {
158        rendered.push_str(" WHEN ");
159        rendered.push_str(
160            render_scalar_projection_expr_plan_label_with_parent(arm.condition(), None, false)
161                .as_str(),
162        );
163        rendered.push_str(" THEN ");
164        rendered.push_str(
165            render_scalar_projection_expr_plan_label_with_parent(arm.result(), None, false)
166                .as_str(),
167        );
168    }
169
170    rendered.push_str(" ELSE ");
171    rendered.push_str(
172        render_scalar_projection_expr_plan_label_with_parent(else_expr, None, false).as_str(),
173    );
174    rendered.push_str(" END");
175
176    rendered
177}
178
179const fn binary_expr_requires_parentheses(
180    op: crate::db::query::plan::expr::BinaryOp,
181    parent_op: Option<crate::db::query::plan::expr::BinaryOp>,
182    is_right_child: bool,
183) -> bool {
184    let Some(parent_op) = parent_op else {
185        return false;
186    };
187    let precedence = binary_op_precedence(op);
188    let parent_precedence = binary_op_precedence(parent_op);
189
190    precedence < parent_precedence || (is_right_child && precedence == parent_precedence)
191}
192
193const fn binary_op_precedence(op: crate::db::query::plan::expr::BinaryOp) -> u8 {
194    match op {
195        crate::db::query::plan::expr::BinaryOp::Or => 0,
196        crate::db::query::plan::expr::BinaryOp::And => 1,
197        crate::db::query::plan::expr::BinaryOp::Eq
198        | crate::db::query::plan::expr::BinaryOp::Ne
199        | crate::db::query::plan::expr::BinaryOp::Lt
200        | crate::db::query::plan::expr::BinaryOp::Lte
201        | crate::db::query::plan::expr::BinaryOp::Gt
202        | crate::db::query::plan::expr::BinaryOp::Gte => 2,
203        crate::db::query::plan::expr::BinaryOp::Add
204        | crate::db::query::plan::expr::BinaryOp::Sub => 3,
205        crate::db::query::plan::expr::BinaryOp::Mul
206        | crate::db::query::plan::expr::BinaryOp::Div => 4,
207    }
208}
209
210const fn binary_op_symbol(op: crate::db::query::plan::expr::BinaryOp) -> &'static str {
211    match op {
212        crate::db::query::plan::expr::BinaryOp::Or => "OR",
213        crate::db::query::plan::expr::BinaryOp::And => "AND",
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 => ">=",
220        crate::db::query::plan::expr::BinaryOp::Add => "+",
221        crate::db::query::plan::expr::BinaryOp::Sub => "-",
222        crate::db::query::plan::expr::BinaryOp::Mul => "*",
223        crate::db::query::plan::expr::BinaryOp::Div => "/",
224    }
225}
226
227fn render_scalar_projection_literal(value: &Value) -> String {
228    match value {
229        Value::Null => "NULL".to_string(),
230        Value::Text(text) => format!("'{}'", text.replace('\'', "''")),
231        Value::Int64(value) => value.to_string(),
232        Value::Int128(value) => value.to_string(),
233        Value::IntBig(value) => value.to_string(),
234        Value::Nat64(value) => value.to_string(),
235        Value::Nat128(value) => value.to_string(),
236        Value::NatBig(value) => value.to_string(),
237        Value::U256(value) => value.to_string(),
238        Value::Decimal(value) => value.to_string(),
239        Value::Float32(value) => value.to_string(),
240        Value::Float64(value) => value.to_string(),
241        Value::Bool(value) => value.to_string().to_uppercase(),
242        other => format!("{other:?}"),
243    }
244}