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 structural 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        query::{
13            builder::{
14                ScalarProjectionPlan, ValueProjectionExpr,
15                scalar_projection::render_scalar_projection_expr_plan_label,
16            },
17            plan::expr::{BinaryOp, Expr, FieldId, Function, eval_builder_expr_for_value_preview},
18        },
19    },
20    traits::NumericValue,
21    value::{InputValue, 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/// 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 Into<InputValue> + NumericValue,
85    ) -> Self {
86        let literal = Value::from(literal.into());
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 Into<InputValue> + 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 Into<InputValue> + 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 Into<InputValue> + 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 Into<InputValue> + 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 projection_plan(&self) -> ScalarProjectionPlan {
186        ScalarProjectionPlan::new(self.expr.clone())
187    }
188
189    fn projection_label(&self) -> String {
190        render_scalar_projection_expr_plan_label(&self.expr)
191    }
192
193    fn apply_value(&self, value: Value) -> Result<Value, QueryError> {
194        eval_builder_expr_for_value_preview(&self.expr, self.field.as_str(), &value)
195    }
196}
197
198///
199/// RoundProjectionExpr
200///
201/// Shared bounded numeric rounding projection over one source field and one
202/// canonical scalar numeric expression.
203/// This keeps `ROUND` on the scalar projection seam without opening a generic
204/// function-builder surface.
205///
206
207#[derive(Clone, Debug, Eq, PartialEq)]
208pub struct RoundProjectionExpr {
209    field: String,
210    expr: Expr,
211}
212
213impl RoundProjectionExpr {
214    // Build one bounded `ROUND(expr, scale)` projection after validating that
215    // `scale` stays on the admitted non-negative integer seam.
216    pub(in crate::db) fn new(
217        field: impl Into<String>,
218        inner: Expr,
219        scale: Value,
220    ) -> Result<Self, QueryError> {
221        match scale {
222            Value::Int(value) if value < 0 => {
223                return Err(QueryError::unsupported_query(format!(
224                    "ROUND(...) requires non-negative integer scale, found {value}",
225                )));
226            }
227            Value::Int(_) | Value::Uint(_) => {}
228            other => {
229                return Err(QueryError::unsupported_query(format!(
230                    "ROUND(...) requires integer scale, found {other:?}",
231                )));
232            }
233        }
234
235        Ok(Self {
236            field: field.into(),
237            expr: Expr::FunctionCall {
238                function: Function::Round,
239                args: vec![inner, Expr::Literal(scale)],
240            },
241        })
242    }
243
244    // Build one rounded field projection.
245    pub(in crate::db) fn field(field: impl Into<String>, scale: u32) -> Result<Self, QueryError> {
246        let field = field.into();
247
248        Self::new(
249            field.clone(),
250            Expr::Field(FieldId::new(field)),
251            Value::Uint(u64::from(scale)),
252        )
253    }
254
255    /// Borrow the canonical planner expression carried by this helper.
256    #[must_use]
257    pub(in crate::db) const fn expr(&self) -> &Expr {
258        &self.expr
259    }
260}
261
262impl ValueProjectionExpr for RoundProjectionExpr {
263    fn field(&self) -> &str {
264        self.field.as_str()
265    }
266
267    fn projection_plan(&self) -> ScalarProjectionPlan {
268        ScalarProjectionPlan::new(self.expr.clone())
269    }
270
271    fn projection_label(&self) -> String {
272        render_scalar_projection_expr_plan_label(&self.expr)
273    }
274
275    fn apply_value(&self, value: Value) -> Result<Value, QueryError> {
276        eval_builder_expr_for_value_preview(&self.expr, self.field.as_str(), &value)
277    }
278}
279
280/// Build `field + literal`.
281#[must_use]
282pub fn add(
283    field: impl AsRef<str>,
284    literal: impl Into<InputValue> + NumericValue,
285) -> NumericProjectionExpr {
286    NumericProjectionExpr::add_numeric_literal(field.as_ref().to_string(), literal)
287}
288
289/// Build `field - literal`.
290#[must_use]
291pub fn sub(
292    field: impl AsRef<str>,
293    literal: impl Into<InputValue> + NumericValue,
294) -> NumericProjectionExpr {
295    NumericProjectionExpr::sub_numeric_literal(field.as_ref().to_string(), literal)
296}
297
298/// Build `field * literal`.
299#[must_use]
300pub fn mul(
301    field: impl AsRef<str>,
302    literal: impl Into<InputValue> + NumericValue,
303) -> NumericProjectionExpr {
304    NumericProjectionExpr::mul_numeric_literal(field.as_ref().to_string(), literal)
305}
306
307/// Build `field / literal`.
308#[must_use]
309pub fn div(
310    field: impl AsRef<str>,
311    literal: impl Into<InputValue> + NumericValue,
312) -> NumericProjectionExpr {
313    NumericProjectionExpr::div_numeric_literal(field.as_ref().to_string(), literal)
314}
315
316/// Build `ROUND(field, scale)`.
317pub fn round(field: impl AsRef<str>, scale: u32) -> RoundProjectionExpr {
318    RoundProjectionExpr::field(field.as_ref().to_string(), scale)
319        .expect("ROUND(field, scale) helper should always produce a bounded projection")
320}
321
322/// Build `ROUND(expr, scale)` for one existing bounded numeric projection.
323#[must_use]
324pub fn round_expr(projection: &NumericProjectionExpr, scale: u32) -> RoundProjectionExpr {
325    projection
326        .round_with_scale(scale)
327        .expect("ROUND(expr, scale) helper should always produce a bounded projection")
328}