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