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_binary_expr,
13        query::{
14            builder::{
15                ValueProjectionExpr, scalar_projection::render_scalar_projection_expr_sql_label,
16            },
17            plan::expr::{BinaryOp, Expr, FieldId},
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 currently stays on the narrow `field + literal` seam admitted by the
30/// shipped SQL and fluent 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 field-plus-literal numeric projection after validating that
41    // the literal stays on the admitted numeric seam.
42    pub(in crate::db) fn add_value(
43        field: impl Into<String>,
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: BinaryOp::Add,
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 field-plus-literal numeric projection from one typed numeric
79    // literal helper.
80    pub(in crate::db) fn add_numeric_literal(
81        field: impl Into<String>,
82        literal: impl FieldValue + NumericValue,
83    ) -> Self {
84        let literal = literal.to_value();
85
86        Self::add_value(field, literal)
87            .expect("typed numeric projection helpers should always produce numeric literals")
88    }
89
90    /// Borrow the canonical planner expression carried by this helper.
91    #[must_use]
92    pub(in crate::db) const fn expr(&self) -> &Expr {
93        &self.expr
94    }
95}
96
97impl ValueProjectionExpr for NumericProjectionExpr {
98    fn field(&self) -> &str {
99        self.field.as_str()
100    }
101
102    fn sql_label(&self) -> String {
103        render_scalar_projection_expr_sql_label(&self.expr)
104    }
105
106    fn apply_value(&self, value: Value) -> Result<Value, QueryError> {
107        let Expr::Binary { op, right, .. } = &self.expr else {
108            return Err(QueryError::invariant(
109                "numeric projection helper must retain one binary expression",
110            ));
111        };
112        let Expr::Literal(literal) = right.as_ref() else {
113            return Err(QueryError::invariant(
114                "numeric projection helper must retain one literal right operand",
115            ));
116        };
117
118        eval_binary_expr(*op, &value, literal)
119            .map_err(|err| QueryError::unsupported_query(err.to_string()))
120    }
121}
122
123/// Build `field + literal`.
124#[must_use]
125pub fn add(
126    field: impl AsRef<str>,
127    literal: impl FieldValue + NumericValue,
128) -> NumericProjectionExpr {
129    NumericProjectionExpr::add_numeric_literal(field.as_ref().to_string(), literal)
130}