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