icydb_core/db/query/builder/
numeric_projection.rs1use 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 types::NumericValue,
21 value::Value,
22};
23
24#[derive(Clone, Debug, Eq, PartialEq)]
34pub struct NumericProjectionExpr {
35 field: String,
36 expr: Expr,
37}
38
39impl NumericProjectionExpr {
40 fn arithmetic_numeric_literal(
43 field: impl Into<String>,
44 op: BinaryOp,
45 literal: impl Into<Value> + NumericValue,
46 ) -> Self {
47 let literal = literal.into();
48 let field = field.into();
49
50 Self {
51 expr: Expr::Binary {
52 op,
53 left: Box::new(Expr::Field(FieldId::new(field.clone()))),
54 right: Box::new(Expr::Literal(literal)),
55 },
56 field,
57 }
58 }
59
60 pub(in crate::db) fn add_numeric_literal(
63 field: impl Into<String>,
64 literal: impl Into<Value> + NumericValue,
65 ) -> Self {
66 Self::arithmetic_numeric_literal(field, BinaryOp::Add, literal)
67 }
68
69 pub(in crate::db) fn sub_numeric_literal(
72 field: impl Into<String>,
73 literal: impl Into<Value> + NumericValue,
74 ) -> Self {
75 Self::arithmetic_numeric_literal(field, BinaryOp::Sub, literal)
76 }
77
78 pub(in crate::db) fn mul_numeric_literal(
81 field: impl Into<String>,
82 literal: impl Into<Value> + NumericValue,
83 ) -> Self {
84 Self::arithmetic_numeric_literal(field, BinaryOp::Mul, literal)
85 }
86
87 pub(in crate::db) fn div_numeric_literal(
90 field: impl Into<String>,
91 literal: impl Into<Value> + NumericValue,
92 ) -> Self {
93 Self::arithmetic_numeric_literal(field, BinaryOp::Div, literal)
94 }
95
96 #[must_use]
98 pub(in crate::db) const fn expr(&self) -> &Expr {
99 &self.expr
100 }
101
102 pub(in crate::db) fn round_with_scale(&self, scale: u32) -> RoundProjectionExpr {
105 RoundProjectionExpr::with_valid_scale(self.field.clone(), self.expr.clone(), scale)
106 }
107}
108
109impl super::scalar_projection::private::Sealed for NumericProjectionExpr {}
110
111impl ValueProjectionExpr for NumericProjectionExpr {
112 fn field(&self) -> &str {
113 self.field.as_str()
114 }
115
116 fn projection_plan(&self) -> ScalarProjectionPlan {
117 ScalarProjectionPlan::new(self.expr.clone())
118 }
119
120 fn projection_label(&self) -> String {
121 render_scalar_projection_expr_plan_label(&self.expr)
122 }
123
124 fn apply_value(&self, value: Value) -> Result<Value, QueryError> {
125 eval_builder_expr_for_value_preview(&self.expr, self.field.as_str(), &value)
126 }
127}
128
129#[derive(Clone, Debug, Eq, PartialEq)]
139pub struct RoundProjectionExpr {
140 field: String,
141 expr: Expr,
142}
143
144impl RoundProjectionExpr {
145 #[cfg(test)]
148 pub(in crate::db) fn new(
149 field: impl Into<String>,
150 inner: Expr,
151 scale: Value,
152 ) -> Result<Self, QueryError> {
153 match scale {
154 Value::Int64(value) if value < 0 => {
155 return Err(QueryError::unsupported_projection(
156 icydb_diagnostic_code::QueryProjectionCode::NumericScaleArguments,
157 ));
158 }
159 Value::Int64(_) | Value::Nat64(_) => {}
160 _ => {
161 return Err(QueryError::unsupported_projection(
162 icydb_diagnostic_code::QueryProjectionCode::NumericScaleArguments,
163 ));
164 }
165 }
166
167 Ok(Self {
168 field: field.into(),
169 expr: Expr::FunctionCall {
170 function: Function::Round,
171 args: vec![inner, Expr::Literal(scale)],
172 },
173 })
174 }
175
176 pub(in crate::db) fn field(field: impl Into<String>, scale: u32) -> Self {
179 let field = field.into();
180
181 Self::with_valid_scale(field.clone(), Expr::Field(FieldId::new(field)), scale)
182 }
183
184 fn with_valid_scale(field: String, inner: Expr, scale: u32) -> Self {
185 Self {
186 field,
187 expr: Expr::FunctionCall {
188 function: Function::Round,
189 args: vec![inner, Expr::Literal(Value::Nat64(u64::from(scale)))],
190 },
191 }
192 }
193
194 #[must_use]
196 pub(in crate::db) const fn expr(&self) -> &Expr {
197 &self.expr
198 }
199}
200
201impl super::scalar_projection::private::Sealed for RoundProjectionExpr {}
202
203impl ValueProjectionExpr for RoundProjectionExpr {
204 fn field(&self) -> &str {
205 self.field.as_str()
206 }
207
208 fn projection_plan(&self) -> ScalarProjectionPlan {
209 ScalarProjectionPlan::new(self.expr.clone())
210 }
211
212 fn projection_label(&self) -> String {
213 render_scalar_projection_expr_plan_label(&self.expr)
214 }
215
216 fn apply_value(&self, value: Value) -> Result<Value, QueryError> {
217 eval_builder_expr_for_value_preview(&self.expr, self.field.as_str(), &value)
218 }
219}
220
221#[must_use]
223pub fn add(
224 field: impl AsRef<str>,
225 literal: impl Into<Value> + NumericValue,
226) -> NumericProjectionExpr {
227 NumericProjectionExpr::add_numeric_literal(field.as_ref().to_string(), literal)
228}
229
230#[must_use]
232pub fn sub(
233 field: impl AsRef<str>,
234 literal: impl Into<Value> + NumericValue,
235) -> NumericProjectionExpr {
236 NumericProjectionExpr::sub_numeric_literal(field.as_ref().to_string(), literal)
237}
238
239#[must_use]
241pub fn mul(
242 field: impl AsRef<str>,
243 literal: impl Into<Value> + NumericValue,
244) -> NumericProjectionExpr {
245 NumericProjectionExpr::mul_numeric_literal(field.as_ref().to_string(), literal)
246}
247
248#[must_use]
250pub fn div(
251 field: impl AsRef<str>,
252 literal: impl Into<Value> + NumericValue,
253) -> NumericProjectionExpr {
254 NumericProjectionExpr::div_numeric_literal(field.as_ref().to_string(), literal)
255}
256
257pub fn round(field: impl AsRef<str>, scale: u32) -> RoundProjectionExpr {
260 RoundProjectionExpr::field(field.as_ref().to_string(), scale)
261}
262
263#[must_use]
266pub fn round_expr(projection: &NumericProjectionExpr, scale: u32) -> RoundProjectionExpr {
267 projection.round_with_scale(scale)
268}
269
270#[cfg(test)]
271mod tests {
272 use super::{RoundProjectionExpr, add};
273 use crate::{
274 db::{
275 QueryError,
276 query::plan::expr::{BinaryOp, Expr, FieldId},
277 },
278 types::U256,
279 value::Value,
280 };
281 use icydb_diagnostic_code::{DiagnosticCode, DiagnosticDetail, QueryProjectionCode};
282
283 fn assert_query_projection_error(err: QueryError, reason: QueryProjectionCode) {
284 let diagnostic = err.diagnostic();
285
286 assert_eq!(
287 diagnostic.code(),
288 DiagnosticCode::QueryUnsupportedProjection
289 );
290 assert_eq!(
291 diagnostic.detail(),
292 Some(&DiagnosticDetail::QueryProjection { reason }),
293 );
294 }
295
296 #[test]
297 fn fluent_numeric_projection_accepts_constructed_u256_literals() {
298 let projection = add("amount", U256::from(3_u64));
299
300 assert_eq!(
301 projection.expr(),
302 &Expr::Binary {
303 op: BinaryOp::Add,
304 left: Box::new(Expr::Field(FieldId::new("amount"))),
305 right: Box::new(Expr::Literal(Value::U256(U256::from(3_u64)))),
306 },
307 );
308 }
309
310 #[test]
311 fn round_projection_rejects_negative_scale_with_compact_projection_code() {
312 let err =
313 RoundProjectionExpr::new("age", Expr::Field(FieldId::new("age")), Value::Int64(-1))
314 .expect_err("negative ROUND scale should fail closed");
315
316 assert_query_projection_error(err, QueryProjectionCode::NumericScaleArguments);
317 }
318
319 #[test]
320 fn round_projection_rejects_non_integer_scale_with_compact_projection_code() {
321 let err = RoundProjectionExpr::new(
322 "age",
323 Expr::Field(FieldId::new("age")),
324 Value::Text("invalid".to_string()),
325 )
326 .expect_err("non-integer ROUND scale should fail closed");
327
328 assert_query_projection_error(err, QueryProjectionCode::NumericScaleArguments);
329 }
330}