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};
23use icydb_diagnostic_code::QueryProjectionCode;
24
25#[derive(Clone, Debug, Eq, PartialEq)]
35pub struct NumericProjectionExpr {
36 field: String,
37 expr: Expr,
38}
39
40impl NumericProjectionExpr {
41 fn arithmetic_value(
44 field: impl Into<String>,
45 op: BinaryOp,
46 literal: Value,
47 ) -> Result<Self, QueryError> {
48 if !matches!(
49 literal,
50 Value::Int64(_)
51 | Value::Int128(_)
52 | Value::IntBig(_)
53 | Value::Nat64(_)
54 | Value::Nat128(_)
55 | Value::NatBig(_)
56 | Value::U256(_)
57 | Value::Decimal(_)
58 | Value::Float32(_)
59 | Value::Float64(_)
60 | Value::Duration(_)
61 | Value::Timestamp(_)
62 | Value::Date(_)
63 ) {
64 return Err(QueryError::unsupported_projection(
65 QueryProjectionCode::NumericLiteralRequired,
66 ));
67 }
68
69 let field = field.into();
70
71 Ok(Self {
72 expr: Expr::Binary {
73 op,
74 left: Box::new(Expr::Field(FieldId::new(field.clone()))),
75 right: Box::new(Expr::Literal(literal)),
76 },
77 field,
78 })
79 }
80
81 fn arithmetic_numeric_literal(
84 field: impl Into<String>,
85 op: BinaryOp,
86 literal: impl Into<Value> + NumericValue,
87 ) -> Self {
88 let literal = literal.into();
89 let field = field.into();
90
91 Self {
92 expr: Expr::Binary {
93 op,
94 left: Box::new(Expr::Field(FieldId::new(field.clone()))),
95 right: Box::new(Expr::Literal(literal)),
96 },
97 field,
98 }
99 }
100
101 #[cfg(feature = "sql")]
103 pub(in crate::db) fn add_value(
104 field: impl Into<String>,
105 literal: Value,
106 ) -> Result<Self, QueryError> {
107 Self::arithmetic_value(field, BinaryOp::Add, literal)
108 }
109
110 #[cfg(feature = "sql")]
112 pub(in crate::db) fn sub_value(
113 field: impl Into<String>,
114 literal: Value,
115 ) -> Result<Self, QueryError> {
116 Self::arithmetic_value(field, BinaryOp::Sub, literal)
117 }
118
119 #[cfg(feature = "sql")]
121 pub(in crate::db) fn mul_value(
122 field: impl Into<String>,
123 literal: Value,
124 ) -> Result<Self, QueryError> {
125 Self::arithmetic_value(field, BinaryOp::Mul, literal)
126 }
127
128 #[cfg(feature = "sql")]
130 pub(in crate::db) fn div_value(
131 field: impl Into<String>,
132 literal: Value,
133 ) -> Result<Self, QueryError> {
134 Self::arithmetic_value(field, BinaryOp::Div, literal)
135 }
136
137 pub(in crate::db) fn add_numeric_literal(
140 field: impl Into<String>,
141 literal: impl Into<Value> + NumericValue,
142 ) -> Self {
143 Self::arithmetic_numeric_literal(field, BinaryOp::Add, literal)
144 }
145
146 pub(in crate::db) fn sub_numeric_literal(
149 field: impl Into<String>,
150 literal: impl Into<Value> + NumericValue,
151 ) -> Self {
152 Self::arithmetic_numeric_literal(field, BinaryOp::Sub, literal)
153 }
154
155 pub(in crate::db) fn mul_numeric_literal(
158 field: impl Into<String>,
159 literal: impl Into<Value> + NumericValue,
160 ) -> Self {
161 Self::arithmetic_numeric_literal(field, BinaryOp::Mul, literal)
162 }
163
164 pub(in crate::db) fn div_numeric_literal(
167 field: impl Into<String>,
168 literal: impl Into<Value> + NumericValue,
169 ) -> Self {
170 Self::arithmetic_numeric_literal(field, BinaryOp::Div, literal)
171 }
172
173 #[must_use]
175 pub(in crate::db) const fn expr(&self) -> &Expr {
176 &self.expr
177 }
178
179 pub(in crate::db) fn round_with_scale(&self, scale: u32) -> RoundProjectionExpr {
182 RoundProjectionExpr::with_valid_scale(self.field.clone(), self.expr.clone(), scale)
183 }
184}
185
186impl super::scalar_projection::private::Sealed for NumericProjectionExpr {}
187
188impl ValueProjectionExpr for NumericProjectionExpr {
189 fn field(&self) -> &str {
190 self.field.as_str()
191 }
192
193 fn projection_plan(&self) -> ScalarProjectionPlan {
194 ScalarProjectionPlan::new(self.expr.clone())
195 }
196
197 fn projection_label(&self) -> String {
198 render_scalar_projection_expr_plan_label(&self.expr)
199 }
200
201 fn apply_value(&self, value: Value) -> Result<Value, QueryError> {
202 eval_builder_expr_for_value_preview(&self.expr, self.field.as_str(), &value)
203 }
204}
205
206#[derive(Clone, Debug, Eq, PartialEq)]
216pub struct RoundProjectionExpr {
217 field: String,
218 expr: Expr,
219}
220
221impl RoundProjectionExpr {
222 #[cfg(test)]
225 pub(in crate::db) fn new(
226 field: impl Into<String>,
227 inner: Expr,
228 scale: Value,
229 ) -> Result<Self, QueryError> {
230 match scale {
231 Value::Int64(value) if value < 0 => {
232 return Err(QueryError::unsupported_projection(
233 QueryProjectionCode::NumericScaleArguments,
234 ));
235 }
236 Value::Int64(_) | Value::Nat64(_) => {}
237 _ => {
238 return Err(QueryError::unsupported_projection(
239 QueryProjectionCode::NumericScaleArguments,
240 ));
241 }
242 }
243
244 Ok(Self {
245 field: field.into(),
246 expr: Expr::FunctionCall {
247 function: Function::Round,
248 args: vec![inner, Expr::Literal(scale)],
249 },
250 })
251 }
252
253 pub(in crate::db) fn field(field: impl Into<String>, scale: u32) -> Self {
256 let field = field.into();
257
258 Self::with_valid_scale(field.clone(), Expr::Field(FieldId::new(field)), scale)
259 }
260
261 fn with_valid_scale(field: String, inner: Expr, scale: u32) -> Self {
262 Self {
263 field,
264 expr: Expr::FunctionCall {
265 function: Function::Round,
266 args: vec![inner, Expr::Literal(Value::Nat64(u64::from(scale)))],
267 },
268 }
269 }
270
271 #[must_use]
273 pub(in crate::db) const fn expr(&self) -> &Expr {
274 &self.expr
275 }
276}
277
278impl super::scalar_projection::private::Sealed for RoundProjectionExpr {}
279
280impl ValueProjectionExpr for RoundProjectionExpr {
281 fn field(&self) -> &str {
282 self.field.as_str()
283 }
284
285 fn projection_plan(&self) -> ScalarProjectionPlan {
286 ScalarProjectionPlan::new(self.expr.clone())
287 }
288
289 fn projection_label(&self) -> String {
290 render_scalar_projection_expr_plan_label(&self.expr)
291 }
292
293 fn apply_value(&self, value: Value) -> Result<Value, QueryError> {
294 eval_builder_expr_for_value_preview(&self.expr, self.field.as_str(), &value)
295 }
296}
297
298#[must_use]
300pub fn add(
301 field: impl AsRef<str>,
302 literal: impl Into<Value> + NumericValue,
303) -> NumericProjectionExpr {
304 NumericProjectionExpr::add_numeric_literal(field.as_ref().to_string(), literal)
305}
306
307#[must_use]
309pub fn sub(
310 field: impl AsRef<str>,
311 literal: impl Into<Value> + NumericValue,
312) -> NumericProjectionExpr {
313 NumericProjectionExpr::sub_numeric_literal(field.as_ref().to_string(), literal)
314}
315
316#[must_use]
318pub fn mul(
319 field: impl AsRef<str>,
320 literal: impl Into<Value> + NumericValue,
321) -> NumericProjectionExpr {
322 NumericProjectionExpr::mul_numeric_literal(field.as_ref().to_string(), literal)
323}
324
325#[must_use]
327pub fn div(
328 field: impl AsRef<str>,
329 literal: impl Into<Value> + NumericValue,
330) -> NumericProjectionExpr {
331 NumericProjectionExpr::div_numeric_literal(field.as_ref().to_string(), literal)
332}
333
334pub fn round(field: impl AsRef<str>, scale: u32) -> RoundProjectionExpr {
337 RoundProjectionExpr::field(field.as_ref().to_string(), scale)
338}
339
340#[must_use]
343pub fn round_expr(projection: &NumericProjectionExpr, scale: u32) -> RoundProjectionExpr {
344 projection.round_with_scale(scale)
345}
346
347#[cfg(test)]
348mod tests {
349 use super::{NumericProjectionExpr, RoundProjectionExpr, add};
350 use crate::{
351 db::{
352 QueryError,
353 query::plan::expr::{BinaryOp, Expr, FieldId},
354 },
355 types::U256,
356 value::Value,
357 };
358 use icydb_diagnostic_code::{DiagnosticCode, DiagnosticDetail, QueryProjectionCode};
359
360 fn assert_query_projection_error(err: QueryError, reason: QueryProjectionCode) {
361 let diagnostic = err.diagnostic();
362
363 assert_eq!(
364 diagnostic.code(),
365 DiagnosticCode::QueryUnsupportedProjection
366 );
367 assert_eq!(
368 diagnostic.detail(),
369 Some(&DiagnosticDetail::QueryProjection { reason }),
370 );
371 }
372
373 #[test]
374 fn numeric_projection_rejects_non_numeric_literal_with_compact_projection_code() {
375 let err = NumericProjectionExpr::arithmetic_value("age", BinaryOp::Add, Value::Bool(true))
376 .expect_err("non-numeric projection literal should fail closed");
377
378 assert_query_projection_error(err, QueryProjectionCode::NumericLiteralRequired);
379 }
380
381 #[test]
382 fn fluent_numeric_projection_accepts_constructed_u256_literals() {
383 let projection = add("amount", U256::from(3_u64));
384
385 assert_eq!(
386 projection.expr(),
387 &Expr::Binary {
388 op: BinaryOp::Add,
389 left: Box::new(Expr::Field(FieldId::new("amount"))),
390 right: Box::new(Expr::Literal(Value::U256(U256::from(3_u64)))),
391 },
392 );
393 }
394
395 #[test]
396 fn round_projection_rejects_negative_scale_with_compact_projection_code() {
397 let err =
398 RoundProjectionExpr::new("age", Expr::Field(FieldId::new("age")), Value::Int64(-1))
399 .expect_err("negative ROUND scale should fail closed");
400
401 assert_query_projection_error(err, QueryProjectionCode::NumericScaleArguments);
402 }
403
404 #[test]
405 fn round_projection_rejects_non_integer_scale_with_compact_projection_code() {
406 let err = RoundProjectionExpr::new(
407 "age",
408 Expr::Field(FieldId::new("age")),
409 Value::Text("invalid".to_string()),
410 )
411 .expect_err("non-integer ROUND scale should fail closed");
412
413 assert_query_projection_error(err, QueryProjectionCode::NumericScaleArguments);
414 }
415}