Skip to main content

icydb_core/db/query/builder/
numeric_projection.rs

1//! Module: query::builder::numeric_projection
2//! Responsibility: shared bounded numeric projection helpers used by fluent
3//! terminals and SQL lowering.
4//! Does not own: generic arithmetic expression parsing, grouped semantics, or
5//! executor routing.
6//! Boundary: this models the admitted scalar arithmetic surface without
7//! opening a general expression-builder API.
8
9use crate::{
10    db::{
11        QueryError,
12        executor::projection::eval_value_projection_expr_with_value,
13        query::{
14            builder::{
15                ValueProjectionExpr, scalar_projection::render_scalar_projection_expr_sql_label,
16            },
17            plan::expr::{BinaryOp, Expr, FieldId, Function},
18        },
19    },
20    traits::{FieldValue, NumericValue},
21    value::Value,
22};
23
24///
25/// NumericProjectionExpr
26///
27/// Shared bounded numeric projection over one source field and one numeric
28/// literal.
29/// This stays on the narrow `field op literal` seam admitted by the shipped
30/// SQL and fluent scalar projection surfaces.
31///
32
33#[derive(Clone, Debug, Eq, PartialEq)]
34pub struct NumericProjectionExpr {
35    field: String,
36    expr: Expr,
37}
38
39impl NumericProjectionExpr {
40    // Build one bounded field-op-literal numeric projection after validating
41    // that the literal stays on the admitted numeric seam.
42    fn arithmetic_value(
43        field: impl Into<String>,
44        op: BinaryOp,
45        literal: Value,
46    ) -> Result<Self, QueryError> {
47        if !matches!(
48            literal,
49            Value::Int(_)
50                | Value::Int128(_)
51                | Value::IntBig(_)
52                | Value::Uint(_)
53                | Value::Uint128(_)
54                | Value::UintBig(_)
55                | Value::Decimal(_)
56                | Value::Float32(_)
57                | Value::Float64(_)
58                | Value::Duration(_)
59                | Value::Timestamp(_)
60                | Value::Date(_)
61        ) {
62            return Err(QueryError::unsupported_query(format!(
63                "scalar numeric projection requires a numeric literal, found {literal:?}",
64            )));
65        }
66
67        let field = field.into();
68
69        Ok(Self {
70            expr: Expr::Binary {
71                op,
72                left: Box::new(Expr::Field(FieldId::new(field.clone()))),
73                right: Box::new(Expr::Literal(literal)),
74            },
75            field,
76        })
77    }
78
79    // Build one bounded field-op-literal numeric projection from one typed
80    // numeric literal helper.
81    fn arithmetic_numeric_literal(
82        field: impl Into<String>,
83        op: BinaryOp,
84        literal: impl FieldValue + NumericValue,
85    ) -> Self {
86        let literal = literal.to_value();
87
88        Self::arithmetic_value(field, op, literal)
89            .expect("typed numeric projection helpers should always produce numeric literals")
90    }
91
92    // Build one field-plus-literal numeric projection.
93    pub(in crate::db) fn add_value(
94        field: impl Into<String>,
95        literal: Value,
96    ) -> Result<Self, QueryError> {
97        Self::arithmetic_value(field, BinaryOp::Add, literal)
98    }
99
100    // Build one field-minus-literal numeric projection.
101    pub(in crate::db) fn sub_value(
102        field: impl Into<String>,
103        literal: Value,
104    ) -> Result<Self, QueryError> {
105        Self::arithmetic_value(field, BinaryOp::Sub, literal)
106    }
107
108    // Build one field-times-literal numeric projection.
109    pub(in crate::db) fn mul_value(
110        field: impl Into<String>,
111        literal: Value,
112    ) -> Result<Self, QueryError> {
113        Self::arithmetic_value(field, BinaryOp::Mul, literal)
114    }
115
116    // Build one field-divided-by-literal numeric projection.
117    pub(in crate::db) fn div_value(
118        field: impl Into<String>,
119        literal: Value,
120    ) -> Result<Self, QueryError> {
121        Self::arithmetic_value(field, BinaryOp::Div, literal)
122    }
123
124    // Build one field-plus-literal numeric projection from one typed numeric
125    // literal helper.
126    pub(in crate::db) fn add_numeric_literal(
127        field: impl Into<String>,
128        literal: impl FieldValue + NumericValue,
129    ) -> Self {
130        Self::arithmetic_numeric_literal(field, BinaryOp::Add, literal)
131    }
132
133    // Build one field-minus-literal numeric projection from one typed numeric
134    // literal helper.
135    pub(in crate::db) fn sub_numeric_literal(
136        field: impl Into<String>,
137        literal: impl FieldValue + NumericValue,
138    ) -> Self {
139        Self::arithmetic_numeric_literal(field, BinaryOp::Sub, literal)
140    }
141
142    // Build one field-times-literal numeric projection from one typed numeric
143    // literal helper.
144    pub(in crate::db) fn mul_numeric_literal(
145        field: impl Into<String>,
146        literal: impl FieldValue + NumericValue,
147    ) -> Self {
148        Self::arithmetic_numeric_literal(field, BinaryOp::Mul, literal)
149    }
150
151    // Build one field-divided-by-literal numeric projection from one typed
152    // numeric literal helper.
153    pub(in crate::db) fn div_numeric_literal(
154        field: impl Into<String>,
155        literal: impl FieldValue + NumericValue,
156    ) -> Self {
157        Self::arithmetic_numeric_literal(field, BinaryOp::Div, literal)
158    }
159
160    /// Borrow the canonical planner expression carried by this helper.
161    #[must_use]
162    pub(in crate::db) const fn expr(&self) -> &Expr {
163        &self.expr
164    }
165
166    // Build one rounded projection over either a plain field or one existing
167    // bounded numeric expression rooted in the same source field.
168    pub(in crate::db) fn round_with_scale(
169        &self,
170        scale: u32,
171    ) -> Result<RoundProjectionExpr, QueryError> {
172        RoundProjectionExpr::new(
173            self.field.clone(),
174            self.expr.clone(),
175            Value::Uint(u64::from(scale)),
176        )
177    }
178}
179
180impl ValueProjectionExpr for NumericProjectionExpr {
181    fn field(&self) -> &str {
182        self.field.as_str()
183    }
184
185    fn sql_label(&self) -> String {
186        render_scalar_projection_expr_sql_label(&self.expr)
187    }
188
189    fn apply_value(&self, value: Value) -> Result<Value, QueryError> {
190        eval_value_projection_expr_with_value(&self.expr, self.field.as_str(), &value)
191    }
192}
193
194///
195/// RoundProjectionExpr
196///
197/// Shared bounded numeric rounding projection over one source field and one
198/// canonical scalar numeric expression.
199/// This keeps `ROUND` on the scalar projection seam without opening a generic
200/// function-builder surface.
201///
202
203#[derive(Clone, Debug, Eq, PartialEq)]
204pub struct RoundProjectionExpr {
205    field: String,
206    expr: Expr,
207}
208
209impl RoundProjectionExpr {
210    // Build one bounded `ROUND(expr, scale)` projection after validating that
211    // `scale` stays on the admitted non-negative integer seam.
212    pub(in crate::db) fn new(
213        field: impl Into<String>,
214        inner: Expr,
215        scale: Value,
216    ) -> Result<Self, QueryError> {
217        match scale {
218            Value::Int(value) if value < 0 => {
219                return Err(QueryError::unsupported_query(format!(
220                    "ROUND(...) requires non-negative integer scale, found {value}",
221                )));
222            }
223            Value::Int(_) | Value::Uint(_) => {}
224            other => {
225                return Err(QueryError::unsupported_query(format!(
226                    "ROUND(...) requires integer scale, found {other:?}",
227                )));
228            }
229        }
230
231        Ok(Self {
232            field: field.into(),
233            expr: Expr::FunctionCall {
234                function: Function::Round,
235                args: vec![inner, Expr::Literal(scale)],
236            },
237        })
238    }
239
240    // Build one rounded field projection.
241    pub(in crate::db) fn field(field: impl Into<String>, scale: u32) -> Result<Self, QueryError> {
242        let field = field.into();
243
244        Self::new(
245            field.clone(),
246            Expr::Field(FieldId::new(field)),
247            Value::Uint(u64::from(scale)),
248        )
249    }
250
251    /// Borrow the canonical planner expression carried by this helper.
252    #[must_use]
253    pub(in crate::db) const fn expr(&self) -> &Expr {
254        &self.expr
255    }
256}
257
258impl ValueProjectionExpr for RoundProjectionExpr {
259    fn field(&self) -> &str {
260        self.field.as_str()
261    }
262
263    fn sql_label(&self) -> String {
264        render_scalar_projection_expr_sql_label(&self.expr)
265    }
266
267    fn apply_value(&self, value: Value) -> Result<Value, QueryError> {
268        eval_value_projection_expr_with_value(&self.expr, self.field.as_str(), &value)
269    }
270}
271
272/// Build `field + literal`.
273#[must_use]
274pub fn add(
275    field: impl AsRef<str>,
276    literal: impl FieldValue + NumericValue,
277) -> NumericProjectionExpr {
278    NumericProjectionExpr::add_numeric_literal(field.as_ref().to_string(), literal)
279}
280
281/// Build `field - literal`.
282#[must_use]
283pub fn sub(
284    field: impl AsRef<str>,
285    literal: impl FieldValue + NumericValue,
286) -> NumericProjectionExpr {
287    NumericProjectionExpr::sub_numeric_literal(field.as_ref().to_string(), literal)
288}
289
290/// Build `field * literal`.
291#[must_use]
292pub fn mul(
293    field: impl AsRef<str>,
294    literal: impl FieldValue + NumericValue,
295) -> NumericProjectionExpr {
296    NumericProjectionExpr::mul_numeric_literal(field.as_ref().to_string(), literal)
297}
298
299/// Build `field / literal`.
300#[must_use]
301pub fn div(
302    field: impl AsRef<str>,
303    literal: impl FieldValue + NumericValue,
304) -> NumericProjectionExpr {
305    NumericProjectionExpr::div_numeric_literal(field.as_ref().to_string(), literal)
306}
307
308/// Build `ROUND(field, scale)`.
309pub fn round(field: impl AsRef<str>, scale: u32) -> RoundProjectionExpr {
310    RoundProjectionExpr::field(field.as_ref().to_string(), scale)
311        .expect("ROUND(field, scale) helper should always produce a bounded projection")
312}
313
314/// Build `ROUND(expr, scale)` for one existing bounded numeric projection.
315#[must_use]
316pub fn round_expr(projection: &NumericProjectionExpr, scale: u32) -> RoundProjectionExpr {
317    projection
318        .round_with_scale(scale)
319        .expect("ROUND(expr, scale) helper should always produce a bounded projection")
320}