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