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