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