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