1use std::sync::LazyLock;
23
24use nodedb_types::Value;
25use sonic_rs;
26
27use crate::error::SqlError;
28use crate::functions::registry::{FunctionCategory, FunctionRegistry};
29use crate::types::{BinaryOp, SqlExpr, SqlValue, UnaryOp};
30
31pub type FoldResult = Result<Option<SqlValue>, SqlError>;
39
40static DEFAULT_REGISTRY: LazyLock<FunctionRegistry> = LazyLock::new(FunctionRegistry::new);
43
44pub fn default_registry() -> &'static FunctionRegistry {
46 &DEFAULT_REGISTRY
47}
48
49pub fn fold_constant_default(expr: &SqlExpr) -> FoldResult {
51 fold_constant(expr, default_registry())
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum FoldScope {
62 Reuse,
64 Once,
66}
67
68pub fn fold_constant(expr: &SqlExpr, registry: &FunctionRegistry) -> FoldResult {
71 fold_constant_scoped(expr, registry, FoldScope::Reuse)
72}
73
74pub fn fold_constant_scoped(
76 expr: &SqlExpr,
77 registry: &FunctionRegistry,
78 scope: FoldScope,
79) -> FoldResult {
80 match expr {
81 SqlExpr::Literal(v) => Ok(Some(v.clone())),
82 SqlExpr::ArrayLiteral(items) => {
83 let mut folded = Vec::with_capacity(items.len());
84 for item in items {
85 match fold_constant_scoped(item, registry, scope)? {
86 Some(v) => folded.push(v),
87 None => return Ok(None),
88 }
89 }
90 Ok(Some(SqlValue::Array(folded)))
91 }
92 SqlExpr::UnaryOp {
93 op: UnaryOp::Neg,
94 expr,
95 } => Ok(match fold_constant_scoped(expr, registry, scope)? {
96 Some(SqlValue::Int(i)) => i.checked_neg().map(SqlValue::Int),
99 Some(SqlValue::Float(f)) => Some(SqlValue::Float(-f)),
100 Some(SqlValue::Decimal(d))
104 if d.scale() == 0 && -d == rust_decimal::Decimal::from(i64::MIN) =>
105 {
106 Some(SqlValue::Int(i64::MIN))
107 }
108 Some(SqlValue::Decimal(d)) => Some(SqlValue::Decimal(-d)),
109 _ => None,
110 }),
111 SqlExpr::BinaryOp { left, op, right } => {
112 let (Some(l), Some(r)) = (
113 fold_constant_scoped(left, registry, scope)?,
114 fold_constant_scoped(right, registry, scope)?,
115 ) else {
116 return Ok(None);
117 };
118 fold_binary(l, *op, r)
119 }
120 SqlExpr::Function { name, args, .. } => {
121 fold_function_call_scoped(name, args, registry, scope)
122 }
123 SqlExpr::Cast { expr, to_type } => Ok(fold_constant_scoped(expr, registry, scope)?
124 .and_then(|inner| fold_cast(inner, to_type))),
125 _ => Ok(None),
126 }
127}
128
129fn fold_cast(inner: SqlValue, to_type: &str) -> Option<SqlValue> {
133 let upper = to_type.to_uppercase();
134 let base = upper
136 .split('(')
137 .next()
138 .map(str::trim)
139 .unwrap_or(upper.as_str());
140
141 match base {
142 "REGCLASS" => None,
145 "NUMERIC" | "DECIMAL" => match inner {
146 SqlValue::Decimal(d) => Some(SqlValue::Decimal(d)),
147 SqlValue::Int(i) => Some(SqlValue::Decimal(rust_decimal::Decimal::from(i))),
148 SqlValue::Float(f) => rust_decimal::Decimal::try_from(f)
149 .ok()
150 .map(SqlValue::Decimal),
151 SqlValue::String(s) => rust_decimal::Decimal::from_str_exact(&s)
152 .ok()
153 .map(SqlValue::Decimal),
154 _ => None,
155 },
156 "INTEGER" | "INT" | "BIGINT" | "SMALLINT" | "INT2" | "INT4" | "INT8" => match inner {
157 SqlValue::Int(i) => Some(SqlValue::Int(i)),
158 SqlValue::Decimal(d) => {
159 rust_decimal::prelude::ToPrimitive::to_i64(&d).map(SqlValue::Int)
160 }
161 SqlValue::Float(f) => {
162 if f.is_finite() {
163 Some(SqlValue::Int(f as i64))
164 } else {
165 None
166 }
167 }
168 SqlValue::String(s) => s.parse::<i64>().ok().map(SqlValue::Int),
169 _ => None,
170 },
171 "FLOAT" | "DOUBLE" | "REAL" | "FLOAT4" | "FLOAT8" | "DOUBLE PRECISION" => match inner {
172 SqlValue::Float(f) => Some(SqlValue::Float(f)),
173 SqlValue::Int(i) => Some(SqlValue::Float(i as f64)),
174 SqlValue::Decimal(d) => {
175 rust_decimal::prelude::ToPrimitive::to_f64(&d).map(SqlValue::Float)
176 }
177 SqlValue::String(s) => s.parse::<f64>().ok().map(SqlValue::Float),
178 _ => None,
179 },
180 "TEXT" | "VARCHAR" | "CHAR" | "CHARACTER VARYING" | "CHARACTER" | "BPCHAR" => match inner {
181 SqlValue::String(s) => Some(SqlValue::String(s)),
182 SqlValue::Int(i) => Some(SqlValue::String(i.to_string())),
183 SqlValue::Float(f) => Some(SqlValue::String(f.to_string())),
184 SqlValue::Decimal(d) => Some(SqlValue::String(d.to_string())),
185 SqlValue::Bool(b) => Some(SqlValue::String(b.to_string())),
186 _ => None,
187 },
188 "BOOL" | "BOOLEAN" => match inner {
189 SqlValue::Bool(b) => Some(SqlValue::Bool(b)),
190 SqlValue::Int(i) => Some(SqlValue::Bool(i != 0)),
191 SqlValue::String(s) => match s.to_lowercase().as_str() {
192 "true" | "t" | "yes" | "1" | "on" => Some(SqlValue::Bool(true)),
193 "false" | "f" | "no" | "0" | "off" => Some(SqlValue::Bool(false)),
194 _ => None,
195 },
196 _ => None,
197 },
198 "JSON" | "JSONB" => match inner {
203 SqlValue::String(s) => Some(SqlValue::String(s)),
204 SqlValue::Int(i) => Some(SqlValue::String(i.to_string())),
205 SqlValue::Float(f) => Some(SqlValue::String(f.to_string())),
206 SqlValue::Decimal(d) => Some(SqlValue::String(d.to_string())),
207 SqlValue::Bool(b) => Some(SqlValue::String(b.to_string())),
208 SqlValue::Null => Some(SqlValue::Null),
209 _ => None,
210 },
211 _ => None,
212 }
213}
214
215fn is_zero(v: &SqlValue) -> bool {
220 match v {
221 SqlValue::Int(0) => true,
222 SqlValue::Float(f) => *f == 0.0,
223 SqlValue::Decimal(d) => d.is_zero(),
224 _ => false,
225 }
226}
227
228fn overflowed(detail: &str) -> SqlError {
229 SqlError::ConstantOverflow {
230 detail: detail.to_owned(),
231 }
232}
233
234fn fold_binary(l: SqlValue, op: BinaryOp, r: SqlValue) -> FoldResult {
235 if matches!(op, BinaryOp::Div | BinaryOp::Mod) && is_zero(&r) {
236 return Err(SqlError::DivisionByZero);
237 }
238 Ok(Some(match (l, op, r) {
239 (SqlValue::Int(a), BinaryOp::Add, SqlValue::Int(b)) => {
241 SqlValue::Int(a.checked_add(b).ok_or_else(|| overflowed("integer add"))?)
242 }
243 (SqlValue::Int(a), BinaryOp::Sub, SqlValue::Int(b)) => SqlValue::Int(
244 a.checked_sub(b)
245 .ok_or_else(|| overflowed("integer subtract"))?,
246 ),
247 (SqlValue::Int(a), BinaryOp::Mul, SqlValue::Int(b)) => SqlValue::Int(
248 a.checked_mul(b)
249 .ok_or_else(|| overflowed("integer multiply"))?,
250 ),
251 (SqlValue::Int(a), BinaryOp::Div, SqlValue::Int(b)) => SqlValue::Int(
254 a.checked_div(b)
255 .ok_or_else(|| overflowed("integer divide"))?,
256 ),
257 (SqlValue::Int(a), BinaryOp::Mod, SqlValue::Int(b)) => SqlValue::Int(
258 a.checked_rem(b)
259 .ok_or_else(|| overflowed("integer modulo"))?,
260 ),
261 (SqlValue::Float(a), BinaryOp::Add, SqlValue::Float(b)) => SqlValue::Float(a + b),
263 (SqlValue::Float(a), BinaryOp::Sub, SqlValue::Float(b)) => SqlValue::Float(a - b),
264 (SqlValue::Float(a), BinaryOp::Mul, SqlValue::Float(b)) => SqlValue::Float(a * b),
265 (SqlValue::Float(a), BinaryOp::Div, SqlValue::Float(b)) => SqlValue::Float(a / b),
266 (SqlValue::Float(a), BinaryOp::Mod, SqlValue::Float(b)) => SqlValue::Float(a % b),
267 (SqlValue::Decimal(a), BinaryOp::Add, SqlValue::Decimal(b)) => {
269 SqlValue::Decimal(a.checked_add(b).ok_or_else(|| overflowed("decimal add"))?)
270 }
271 (SqlValue::Decimal(a), BinaryOp::Sub, SqlValue::Decimal(b)) => SqlValue::Decimal(
272 a.checked_sub(b)
273 .ok_or_else(|| overflowed("decimal subtract"))?,
274 ),
275 (SqlValue::Decimal(a), BinaryOp::Mul, SqlValue::Decimal(b)) => SqlValue::Decimal(
276 a.checked_mul(b)
277 .ok_or_else(|| overflowed("decimal multiply"))?,
278 ),
279 (SqlValue::Decimal(a), BinaryOp::Div, SqlValue::Decimal(b)) => SqlValue::Decimal(
280 a.checked_div(b)
281 .ok_or_else(|| overflowed("decimal divide"))?,
282 ),
283 (SqlValue::Decimal(a), BinaryOp::Mod, SqlValue::Decimal(b)) => SqlValue::Decimal(
284 a.checked_rem(b)
285 .ok_or_else(|| overflowed("decimal modulo"))?,
286 ),
287 (SqlValue::Decimal(a), BinaryOp::Add, SqlValue::Int(b)) => SqlValue::Decimal(
289 a.checked_add(rust_decimal::Decimal::from(b))
290 .ok_or_else(|| overflowed("decimal add"))?,
291 ),
292 (SqlValue::Int(a), BinaryOp::Add, SqlValue::Decimal(b)) => SqlValue::Decimal(
293 rust_decimal::Decimal::from(a)
294 .checked_add(b)
295 .ok_or_else(|| overflowed("decimal add"))?,
296 ),
297 (SqlValue::Decimal(a), BinaryOp::Sub, SqlValue::Int(b)) => SqlValue::Decimal(
298 a.checked_sub(rust_decimal::Decimal::from(b))
299 .ok_or_else(|| overflowed("decimal subtract"))?,
300 ),
301 (SqlValue::Int(a), BinaryOp::Sub, SqlValue::Decimal(b)) => SqlValue::Decimal(
302 rust_decimal::Decimal::from(a)
303 .checked_sub(b)
304 .ok_or_else(|| overflowed("decimal subtract"))?,
305 ),
306 (SqlValue::Decimal(a), BinaryOp::Mul, SqlValue::Int(b)) => SqlValue::Decimal(
307 a.checked_mul(rust_decimal::Decimal::from(b))
308 .ok_or_else(|| overflowed("decimal multiply"))?,
309 ),
310 (SqlValue::Int(a), BinaryOp::Mul, SqlValue::Decimal(b)) => SqlValue::Decimal(
311 rust_decimal::Decimal::from(a)
312 .checked_mul(b)
313 .ok_or_else(|| overflowed("decimal multiply"))?,
314 ),
315 (SqlValue::Decimal(a), BinaryOp::Div, SqlValue::Int(b)) => SqlValue::Decimal(
316 a.checked_div(rust_decimal::Decimal::from(b))
317 .ok_or_else(|| overflowed("decimal divide"))?,
318 ),
319 (SqlValue::Int(a), BinaryOp::Div, SqlValue::Decimal(b)) => SqlValue::Decimal(
320 rust_decimal::Decimal::from(a)
321 .checked_div(b)
322 .ok_or_else(|| overflowed("decimal divide"))?,
323 ),
324 (SqlValue::Decimal(a), BinaryOp::Mod, SqlValue::Int(b)) => SqlValue::Decimal(
325 a.checked_rem(rust_decimal::Decimal::from(b))
326 .ok_or_else(|| overflowed("decimal modulo"))?,
327 ),
328 (SqlValue::Int(a), BinaryOp::Mod, SqlValue::Decimal(b)) => SqlValue::Decimal(
329 rust_decimal::Decimal::from(a)
330 .checked_rem(b)
331 .ok_or_else(|| overflowed("decimal modulo"))?,
332 ),
333 (SqlValue::String(a), BinaryOp::Concat, SqlValue::String(b)) => {
335 SqlValue::String(format!("{a}{b}"))
336 }
337 _ => return Ok(None),
338 }))
339}
340
341pub fn fold_function_call(name: &str, args: &[SqlExpr], registry: &FunctionRegistry) -> FoldResult {
347 fold_function_call_scoped(name, args, registry, FoldScope::Reuse)
348}
349
350pub fn fold_function_call_scoped(
352 name: &str,
353 args: &[SqlExpr],
354 registry: &FunctionRegistry,
355 scope: FoldScope,
356) -> FoldResult {
357 let name_lower = name.to_lowercase();
361 let Some(meta) = registry.lookup(&name_lower) else {
362 return Ok(None);
363 };
364 if matches!(
365 meta.category,
366 FunctionCategory::Aggregate | FunctionCategory::Window
367 ) {
368 return Ok(None);
369 }
370 if meta.volatility.is_volatile() && scope == FoldScope::Reuse {
374 return Ok(None);
375 }
376 if matches!(name_lower.as_str(), "nextval" | "currval" | "setval") {
380 return Ok(None);
381 }
382
383 let mut folded_args = Vec::with_capacity(args.len());
384 for arg in args {
385 match fold_constant_scoped(arg, registry, scope)? {
386 Some(v) => folded_args.push(sql_to_ndb_value(v)),
387 None => return Ok(None),
388 }
389 }
390
391 match nodedb_query::functions::eval_function(&name_lower, &folded_args) {
398 Ok(result) => Ok(Some(ndb_to_sql_value(result))),
399 Err(nodedb_query::EvalError::DivisionByZero) => Err(SqlError::DivisionByZero),
400 Err(
401 e @ (nodedb_query::EvalError::VectorDimensionMismatch { .. }
402 | nodedb_query::EvalError::ArgumentType { .. }
403 | nodedb_query::EvalError::InvalidJsonPath { .. }
404 | nodedb_query::EvalError::NumericOverflow { .. }),
405 ) => Err(SqlError::DataException {
406 detail: e.to_string(),
407 }),
408 Err(nodedb_query::EvalError::UnknownFunction { .. }) => Ok(None),
412 }
413}
414
415fn sql_to_ndb_value(v: SqlValue) -> Value {
416 match v {
417 SqlValue::Null => Value::Null,
418 SqlValue::Bool(b) => Value::Bool(b),
419 SqlValue::Int(i) => Value::Integer(i),
420 SqlValue::Float(f) => Value::Float(f),
421 SqlValue::Decimal(d) => Value::Decimal(d),
422 SqlValue::String(s) => Value::String(s),
423 SqlValue::Bytes(b) => Value::Bytes(b),
424 SqlValue::Array(a) => Value::Array(a.into_iter().map(sql_to_ndb_value).collect()),
425 SqlValue::Timestamp(dt) => Value::NaiveDateTime(dt),
426 SqlValue::Timestamptz(dt) => Value::DateTime(dt),
427 }
428}
429
430fn ndb_to_sql_value(v: Value) -> SqlValue {
431 match v {
432 Value::Null => SqlValue::Null,
433 Value::Bool(b) => SqlValue::Bool(b),
434 Value::Integer(i) => SqlValue::Int(i),
435 Value::Float(f) => SqlValue::Float(f),
436 Value::String(s) => SqlValue::String(s),
437 Value::Bytes(b) => SqlValue::Bytes(b),
438 Value::Array(a) => SqlValue::Array(a.into_iter().map(ndb_to_sql_value).collect()),
439 Value::DateTime(dt) => SqlValue::Timestamptz(dt),
441 Value::NaiveDateTime(dt) => SqlValue::Timestamp(dt),
442 Value::Uuid(s) | Value::Ulid(s) | Value::Regex(s) => SqlValue::String(s),
443 Value::Duration(d) => SqlValue::String(d.to_human()),
444 Value::Decimal(d) => SqlValue::Decimal(d),
445 Value::Geometry(g) => sonic_rs::to_string(&g)
451 .map(SqlValue::String)
452 .unwrap_or(SqlValue::Null),
453 Value::Object(map) => sonic_rs::to_string(&map)
454 .map(SqlValue::String)
455 .unwrap_or(SqlValue::Null),
456 Value::Set(_) | Value::Range { .. } | Value::Record { .. } | Value::ArrayCell(_) => {
459 SqlValue::Null
460 }
461 _ => SqlValue::Null,
464 }
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470
471 #[test]
472 fn fold_now_produces_timestamptz_once_only() {
473 let registry = FunctionRegistry::new();
474 let expr = SqlExpr::Function {
475 name: "now".into(),
476 args: vec![],
477 distinct: false,
478 };
479 assert!(
481 matches!(fold_constant(&expr, ®istry), Ok(None)),
482 "now() must not fold into a reusable plan"
483 );
484 let val = fold_constant_scoped(&expr, ®istry, FoldScope::Once)
485 .expect("fold must not error")
486 .expect("now() must fold for a single execution");
487 match val {
488 SqlValue::Timestamptz(dt) => {
489 assert!(dt.micros > 0, "expected post-epoch timestamp, got micros=0");
491 }
492 other => panic!("expected SqlValue::Timestamptz, got {other:?}"),
493 }
494 }
495
496 #[test]
499 fn negated_integer_literal_past_i64_max_stays_exact() {
500 let registry = FunctionRegistry::new();
501 let neg = |digits: &str| SqlExpr::UnaryOp {
502 op: UnaryOp::Neg,
503 expr: Box::new(SqlExpr::Literal(SqlValue::Decimal(
504 rust_decimal::Decimal::from_str_exact(digits).unwrap(),
505 ))),
506 };
507 assert_eq!(
508 fold_constant(&neg("9223372036854775808"), ®istry).unwrap(),
509 Some(SqlValue::Int(i64::MIN))
510 );
511 assert_eq!(
512 fold_constant(&neg("18446744073709551615"), ®istry).unwrap(),
513 Some(SqlValue::Decimal(-rust_decimal::Decimal::from(u64::MAX)))
514 );
515 }
516
517 #[test]
520 fn fold_of_a_malformed_json_path_is_a_data_exception() {
521 let registry = FunctionRegistry::new();
522 let expr = SqlExpr::Function {
523 name: "doc_get".into(),
524 args: vec![
525 SqlExpr::Literal(SqlValue::String("{\"a\":1}".into())),
526 SqlExpr::Literal(SqlValue::String("$..a".into())),
527 ],
528 distinct: false,
529 };
530 let err = fold_constant(&expr, ®istry).expect_err("malformed path must fail");
531 assert!(
532 matches!(&err, SqlError::DataException { detail } if detail.contains("invalid JSONPath")),
533 "{err:?}"
534 );
535 }
536
537 #[test]
538 fn fold_current_timestamp_produces_timestamptz_once_only() {
539 let registry = FunctionRegistry::new();
540 let expr = SqlExpr::Function {
541 name: "current_timestamp".into(),
542 args: vec![],
543 distinct: false,
544 };
545 assert!(
546 matches!(fold_constant(&expr, ®istry), Ok(None)),
547 "current_timestamp must not fold into a reusable plan"
548 );
549 assert!(matches!(
550 fold_constant_scoped(&expr, ®istry, FoldScope::Once),
551 Ok(Some(SqlValue::Timestamptz(_)))
552 ));
553 }
554
555 #[test]
556 fn fold_unknown_function_returns_none() {
557 let registry = FunctionRegistry::new();
558 let expr = SqlExpr::Function {
559 name: "definitely_not_a_real_function".into(),
560 args: vec![],
561 distinct: false,
562 };
563 assert!(
564 fold_constant(&expr, ®istry)
565 .expect("fold must not error")
566 .is_none()
567 );
568 }
569
570 #[test]
571 fn fold_array_literal_recursively() {
572 let registry = FunctionRegistry::new();
573 let expr = SqlExpr::ArrayLiteral(vec![
574 SqlExpr::Literal(SqlValue::String("public".into())),
575 SqlExpr::Literal(SqlValue::Int(42)),
576 ]);
577 assert_eq!(
578 fold_constant(&expr, ®istry),
579 Ok(Some(SqlValue::Array(vec![
580 SqlValue::String("public".into()),
581 SqlValue::Int(42),
582 ])))
583 );
584 }
585
586 #[test]
587 fn fold_literal_arithmetic_still_works() {
588 let registry = FunctionRegistry::new();
589 let expr = SqlExpr::BinaryOp {
590 left: Box::new(SqlExpr::Literal(SqlValue::Int(2))),
591 op: BinaryOp::Add,
592 right: Box::new(SqlExpr::Literal(SqlValue::Int(3))),
593 };
594 assert_eq!(fold_constant(&expr, ®istry), Ok(Some(SqlValue::Int(5))));
595 }
596
597 #[test]
598 fn fold_column_ref_returns_none() {
599 let registry = FunctionRegistry::new();
600 let expr = SqlExpr::Column {
601 table: None,
602 name: "name".into(),
603 };
604 assert!(
605 fold_constant(&expr, ®istry)
606 .expect("fold must not error")
607 .is_none()
608 );
609 }
610
611 #[test]
612 fn fold_decimal_literal() {
613 let registry = FunctionRegistry::new();
614 let d = rust_decimal::Decimal::new(12345, 2); let expr = SqlExpr::Literal(SqlValue::Decimal(d));
616 assert_eq!(
617 fold_constant(&expr, ®istry),
618 Ok(Some(SqlValue::Decimal(d)))
619 );
620 }
621
622 #[test]
623 fn fold_decimal_addition() {
624 use rust_decimal::Decimal;
625 let registry = FunctionRegistry::new();
626 let a = Decimal::new(12345, 2); let b = Decimal::new(45678, 2); let expr = SqlExpr::BinaryOp {
629 left: Box::new(SqlExpr::Literal(SqlValue::Decimal(a))),
630 op: BinaryOp::Add,
631 right: Box::new(SqlExpr::Literal(SqlValue::Decimal(b))),
632 };
633 let expected = Decimal::new(58023, 2); assert_eq!(
635 fold_constant(&expr, ®istry),
636 Ok(Some(SqlValue::Decimal(expected)))
637 );
638 }
639
640 #[test]
641 fn fold_decimal_negation() {
642 use rust_decimal::Decimal;
643 let registry = FunctionRegistry::new();
644 let d = Decimal::new(100, 0);
645 let expr = SqlExpr::UnaryOp {
646 op: crate::types::UnaryOp::Neg,
647 expr: Box::new(SqlExpr::Literal(SqlValue::Decimal(d))),
648 };
649 assert_eq!(
650 fold_constant(&expr, ®istry),
651 Ok(Some(SqlValue::Decimal(-d)))
652 );
653 }
654
655 #[test]
656 fn fold_st_geohash() {
657 let registry = FunctionRegistry::new();
658 let expr = SqlExpr::Function {
659 name: "st_geohash".into(),
660 args: vec![
661 SqlExpr::UnaryOp {
662 op: UnaryOp::Neg,
663 expr: Box::new(SqlExpr::Literal(SqlValue::Float(122.4))),
664 },
665 SqlExpr::Literal(SqlValue::Float(37.8)),
666 SqlExpr::Literal(SqlValue::Int(6)),
667 ],
668 distinct: false,
669 };
670 let v = fold_constant(&expr, ®istry).expect("fold must not error");
671 match v {
672 Some(SqlValue::String(ref s)) if !s.is_empty() => {}
673 other => panic!("expected non-empty SqlValue::String, got {other:?}"),
674 }
675 }
676
677 #[test]
678 fn fold_st_distance_nested_st_point() {
679 let registry = FunctionRegistry::new();
680 let make_point = |lng: f64, lat: f64| SqlExpr::Function {
681 name: "st_point".into(),
682 args: vec![
683 SqlExpr::Literal(SqlValue::Float(lng)),
684 SqlExpr::Literal(SqlValue::Float(lat)),
685 ],
686 distinct: false,
687 };
688 let expr = SqlExpr::Function {
689 name: "st_distance".into(),
690 args: vec![make_point(-122.4, 37.8), make_point(-87.6, 41.8)],
691 distinct: false,
692 };
693 let v = fold_constant(&expr, ®istry).expect("fold must not error");
694 match v {
695 Some(SqlValue::Float(d)) => {
696 assert!(d > 0.0, "distance should be positive, got {d}");
697 }
698 other => panic!("expected SqlValue::Float, got {other:?}"),
699 }
700 }
701
702 #[test]
703 fn fold_decimal_int_widening() {
704 use rust_decimal::Decimal;
705 let registry = FunctionRegistry::new();
706 let d = Decimal::new(100, 0); let expr = SqlExpr::BinaryOp {
708 left: Box::new(SqlExpr::Literal(SqlValue::Decimal(d))),
709 op: BinaryOp::Add,
710 right: Box::new(SqlExpr::Literal(SqlValue::Int(50))),
711 };
712 let expected = Decimal::new(150, 0);
713 assert_eq!(
714 fold_constant(&expr, ®istry),
715 Ok(Some(SqlValue::Decimal(expected)))
716 );
717 }
718}