Skip to main content

nodedb_sql/planner/
const_fold.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Plan-time constant folding for `SqlExpr`.
4//!
5//! Evaluates literal expressions and registered zero-or-few-arg scalar
6//! functions (e.g. `now()`, `current_timestamp`, `date_add(now(), '1h')`)
7//! at plan time via the shared `nodedb_query::functions::eval_function`
8//! evaluator.
9//!
10//! This keeps the bare-`SELECT` projection path, the `INSERT`/`UPSERT`
11//! `VALUES` path, and any future default-expression paths from drifting
12//! apart — they all reach the same evaluator that the Data Plane uses
13//! for column-reference evaluation.
14//!
15//! Semantics: Postgres / SQL-standard compatible. `now()` and
16//! `current_timestamp` snapshot once per statement — `CURRENT_TIMESTAMP`
17//! is defined to return the same value for every row of a single
18//! statement, and Postgres goes further (same value for the whole
19//! transaction). Folding at plan time satisfies both contracts and is
20//! cheaper than per-row runtime dispatch.
21
22use 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
31/// Outcome of folding a constant expression.
32///
33/// Three states, kept apart on purpose: `Ok(Some)` folded, `Ok(None)` is not
34/// a constant and belongs to the row-scope evaluator, `Err` was a constant
35/// whose evaluation failed. Collapsing the last two loses the difference
36/// between "ask again at runtime" and "this can never succeed" — and a
37/// from-less SELECT has no runtime to ask.
38pub type FoldResult = Result<Option<SqlValue>, SqlError>;
39
40/// Process-wide default registry. Used by call sites that don't already
41/// thread a `FunctionRegistry` through (e.g. the DML `VALUES` path).
42static DEFAULT_REGISTRY: LazyLock<FunctionRegistry> = LazyLock::new(FunctionRegistry::new);
43
44/// Access the shared default registry.
45pub fn default_registry() -> &'static FunctionRegistry {
46    &DEFAULT_REGISTRY
47}
48
49/// Convenience wrapper around [`fold_constant`] using the default registry.
50pub fn fold_constant_default(expr: &SqlExpr) -> FoldResult {
51    fold_constant(expr, default_registry())
52}
53
54/// Whether a fold is allowed to evaluate `Volatile` functions.
55///
56/// `Reuse` is plan-time folding whose result outlives this execution, so a
57/// volatile call is never evaluated. `Once` is folding for a plan that is
58/// itself marked volatile and never cached, so the call is evaluated here and
59/// re-evaluated on the next execution's re-plan.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum FoldScope {
62    /// The folded value can be reused by a later execution.
63    Reuse,
64    /// The folded value serves this execution only.
65    Once,
66}
67
68/// Fold a `SqlExpr` to a literal `SqlValue` at plan time. See [`FoldResult`]
69/// for what each outcome means. Volatile calls are never folded.
70pub fn fold_constant(expr: &SqlExpr, registry: &FunctionRegistry) -> FoldResult {
71    fold_constant_scoped(expr, registry, FoldScope::Reuse)
72}
73
74/// Fold a `SqlExpr` under an explicit [`FoldScope`].
75pub 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            // `checked_neg` so negating `i64::MIN` declines to fold rather
97            // than wrapping (release) or panicking (debug).
98            Some(SqlValue::Int(i)) => i.checked_neg().map(SqlValue::Int),
99            Some(SqlValue::Float(f)) => Some(SqlValue::Float(-f)),
100            // `9223372036854775808` is past `i64::MAX`, so its literal is a
101            // `Decimal`. Negated it is `i64::MIN`, an integer like every
102            // other in range.
103            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
129/// Fold a CAST at plan time. Only applies when the inner expression is already
130/// a constant. The `to_type` string comes from sqlparser's `format!("{data_type}")`
131/// output, so parameterised types like `NUMERIC(5,1)` must be matched by prefix.
132fn fold_cast(inner: SqlValue, to_type: &str) -> Option<SqlValue> {
133    let upper = to_type.to_uppercase();
134    // Strip any precision/scale suffix: "NUMERIC(5,1)" → "NUMERIC".
135    let base = upper
136        .split('(')
137        .next()
138        .map(str::trim)
139        .unwrap_or(upper.as_str());
140
141    match base {
142        // REGCLASS needs the session catalog. Keep the cast intact for the
143        // contextual catalog-fold pass rather than erasing it to a string.
144        "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` — JSON values live internally as their text form
199        // in `SqlValue::String`; the write path parses JSON-looking strings
200        // into document structure. The cast elides to the inner value's JSON
201        // text (mirrors the `::tsvector` / `::tsquery` elision in the resolver).
202        "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
215/// Whether a value is a zero divisor. Applied before any `checked_div` /
216/// `checked_rem`, which return `None` for a zero divisor and for genuine
217/// overflow alike — folding those together is what let a division by zero
218/// pass as "not foldable".
219fn 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        // Int × Int arithmetic.
240        (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        // Int division and modulo. The zero divisor is already refused above,
252        // so `None` here is `i64::MIN / -1`, which overflows.
253        (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        // Float × Float arithmetic.
262        (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        // Decimal × Decimal arithmetic.
268        (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        // Decimal × Int widening (Int promotes to Decimal).
288        (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        // String concat.
334        (SqlValue::String(a), BinaryOp::Concat, SqlValue::String(b)) => {
335            SqlValue::String(format!("{a}{b}"))
336        }
337        _ => return Ok(None),
338    }))
339}
340
341/// Fold a function call by recursively folding its arguments, dispatching
342/// through the shared scalar evaluator, and converting the result back to
343/// `SqlValue`. Only folds functions that are present in `registry`, so
344/// callers can distinguish "unknown function" from "known function, all
345/// args folded". A `Volatile` function is never folded.
346pub fn fold_function_call(name: &str, args: &[SqlExpr], registry: &FunctionRegistry) -> FoldResult {
347    fold_function_call_scoped(name, args, registry, FoldScope::Reuse)
348}
349
350/// Fold a function call under an explicit [`FoldScope`].
351pub fn fold_function_call_scoped(
352    name: &str,
353    args: &[SqlExpr],
354    registry: &FunctionRegistry,
355    scope: FoldScope,
356) -> FoldResult {
357    // Gate on registry so unknown-function paths keep their existing
358    // fallbacks instead of collapsing to SqlValue::Null. Aggregates and
359    // window functions aren't foldable — they need a row stream.
360    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    // A volatile call must run per execution, so folding it into a reusable
371    // literal would freeze the first result into every later execution of the
372    // same plan. Same "not folded" outcome the category guard above produces.
373    if meta.volatility.is_volatile() && scope == FoldScope::Reuse {
374        return Ok(None);
375    }
376    // Sequence accessors read catalog state this folder has no handle on.
377    // `planner::catalog_expr_fold::eval_catalog_constant` resolves them
378    // through `SqlCatalog` before any fold runs.
379    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    // A registered function whose arguments all folded is fully determined
392    // here, so a failure is the answer — not a reason to defer. Deferring it
393    // reached the row-scope evaluator only when the statement had a FROM
394    // clause; `SELECT mod(5, 0)` has no row scope and became NULL.
395    // Exhaustive on purpose: a new `EvalError` variant must be classified
396    // here rather than defaulting to "defer to a runtime that may not exist".
397    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        // A registered name some other evaluator owns (a search score):
409        // not foldable here. Its search plan serves it, or the plan-time
410        // search-scope pass refuses it.
411        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        // TZ-aware DateTime → Timestamptz; naive → Timestamp.
440        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        // Geometry and Object values are serialized to JSON strings so that
446        // nested function calls like ST_Distance(ST_Point(...), ST_Point(...))
447        // survive the SqlValue round-trip. The geo evaluator's geom_arg helper
448        // recovers Geometry from a GeoJSON string; Object results (e.g. from
449        // ST_GeoHashDecode) reach the client as a JSON-encoded string column.
450        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        // Structured and opaque types collapse to Null — callers that
457        // need these go through the runtime expression path, not folding.
458        Value::Set(_) | Value::Range { .. } | Value::Record { .. } | Value::ArrayCell(_) => {
459            SqlValue::Null
460        }
461        // Value is #[non_exhaustive]; future variants collapse to Null in the
462        // constant-folding path — runtime expression evaluation handles them.
463        _ => 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        // `now` is volatile, so a reusable plan must never carry its value.
480        assert!(
481            matches!(fold_constant(&expr, &registry), Ok(None)),
482            "now() must not fold into a reusable plan"
483        );
484        let val = fold_constant_scoped(&expr, &registry, 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                // Sanity: must not be epoch (year 1970).
490                assert!(dt.micros > 0, "expected post-epoch timestamp, got micros=0");
491            }
492            other => panic!("expected SqlValue::Timestamptz, got {other:?}"),
493        }
494    }
495
496    /// `-9223372036854775808` folds to the integer `i64::MIN`, and a negated
497    /// literal past it stays an exact `Decimal`.
498    #[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"), &registry).unwrap(),
509            Some(SqlValue::Int(i64::MIN))
510        );
511        assert_eq!(
512            fold_constant(&neg("18446744073709551615"), &registry).unwrap(),
513            Some(SqlValue::Decimal(-rust_decimal::Decimal::from(u64::MAX)))
514        );
515    }
516
517    /// A constant call whose argument the function cannot compute on fails
518    /// the statement at plan time instead of folding to NULL.
519    #[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, &registry).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, &registry), Ok(None)),
547            "current_timestamp must not fold into a reusable plan"
548        );
549        assert!(matches!(
550            fold_constant_scoped(&expr, &registry, 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, &registry)
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, &registry),
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, &registry), 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, &registry)
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); // 123.45
615        let expr = SqlExpr::Literal(SqlValue::Decimal(d));
616        assert_eq!(
617            fold_constant(&expr, &registry),
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); // 123.45
627        let b = Decimal::new(45678, 2); // 456.78
628        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); // 580.23
634        assert_eq!(
635            fold_constant(&expr, &registry),
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, &registry),
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, &registry).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, &registry).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); // 100
707        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, &registry),
715            Ok(Some(SqlValue::Decimal(expected)))
716        );
717    }
718}