Skip to main content

citadel_sql/
eval.rs

1//! Expression evaluator with SQL three-valued logic.
2
3use rustc_hash::FxHashMap;
4
5use crate::error::{Result, SqlError};
6use crate::parser::{BinOp, Expr, UnaryOp};
7use crate::types::{ColumnDef, CompactString, DataType, Value};
8
9#[derive(Debug)]
10pub struct ColumnMap {
11    exact: FxHashMap<String, usize>,
12    short: FxHashMap<String, ShortMatch>,
13    collations: Vec<crate::types::Collation>,
14    has_non_binary_collation: bool,
15}
16
17#[derive(Clone, Debug)]
18enum ShortMatch {
19    Unique(usize),
20    Ambiguous,
21}
22
23impl Clone for ColumnMap {
24    fn clone(&self) -> Self {
25        Self {
26            exact: self.exact.clone(),
27            short: self.short.clone(),
28            collations: self.collations.clone(),
29            has_non_binary_collation: self.has_non_binary_collation,
30        }
31    }
32}
33
34impl ColumnMap {
35    pub fn new(columns: &[ColumnDef]) -> Self {
36        let mut exact = FxHashMap::with_capacity_and_hasher(columns.len() * 2, Default::default());
37        let mut short: FxHashMap<String, ShortMatch> =
38            FxHashMap::with_capacity_and_hasher(columns.len(), Default::default());
39        let mut collations = Vec::with_capacity(columns.len());
40        let mut has_non_binary_collation = false;
41
42        for (i, col) in columns.iter().enumerate() {
43            let lower = col.name.to_ascii_lowercase();
44            exact.insert(lower.clone(), i);
45
46            let unqualified = if let Some(dot) = lower.rfind('.') {
47                &lower[dot + 1..]
48            } else {
49                &lower
50            };
51            short
52                .entry(unqualified.to_string())
53                .and_modify(|e| *e = ShortMatch::Ambiguous)
54                .or_insert(ShortMatch::Unique(i));
55            collations.push(col.collation);
56            if col.collation != crate::types::Collation::Binary {
57                has_non_binary_collation = true;
58            }
59        }
60
61        Self {
62            exact,
63            short,
64            collations,
65            has_non_binary_collation,
66        }
67    }
68
69    pub(crate) fn collation_at(&self, idx: usize) -> crate::types::Collation {
70        self.collations
71            .get(idx)
72            .copied()
73            .unwrap_or(crate::types::Collation::Binary)
74    }
75
76    #[inline]
77    pub(crate) fn has_non_binary_collation(&self) -> bool {
78        self.has_non_binary_collation
79    }
80
81    pub(crate) fn resolve(&self, name: &str) -> Result<usize> {
82        if let Some(&idx) = self.exact.get(name) {
83            return Ok(idx);
84        }
85        match self.short.get(name) {
86            Some(ShortMatch::Unique(idx)) => Ok(*idx),
87            Some(ShortMatch::Ambiguous) => Err(SqlError::AmbiguousColumn(name.to_string())),
88            None => Err(SqlError::ColumnNotFound(name.to_string())),
89        }
90    }
91
92    pub(crate) fn resolve_qualified(&self, table: &str, column: &str) -> Result<usize> {
93        let qualified = format!("{table}.{column}");
94        if let Some(&idx) = self.exact.get(&qualified) {
95            return Ok(idx);
96        }
97        match self.short.get(column) {
98            Some(ShortMatch::Unique(idx)) => Ok(*idx),
99            _ => Err(SqlError::ColumnNotFound(format!("{table}.{column}"))),
100        }
101    }
102}
103
104pub struct EvalCtx<'a> {
105    pub col_map: &'a ColumnMap,
106    pub row: &'a [Value],
107    pub params: &'a [Value],
108    pub excluded: Option<ExcludedRow<'a>>,
109    pub old_new: Option<OldNewRows<'a>>,
110    pub session_tz: Option<jiff::tz::TimeZone>,
111}
112
113pub struct ExcludedRow<'a> {
114    pub col_map: &'a ColumnMap,
115    pub row: &'a [Value],
116}
117
118pub struct OldNewRows<'a> {
119    pub col_map: &'a ColumnMap,
120    pub old_row: Option<&'a [Value]>,
121    pub new_row: Option<&'a [Value]>,
122}
123
124impl<'a> EvalCtx<'a> {
125    pub fn new(col_map: &'a ColumnMap, row: &'a [Value]) -> Self {
126        Self {
127            col_map,
128            row,
129            params: &[],
130            excluded: None,
131            old_new: None,
132            session_tz: None,
133        }
134    }
135
136    pub fn with_session_tz(mut self, tz: Option<jiff::tz::TimeZone>) -> Self {
137        self.session_tz = tz;
138        self
139    }
140
141    pub fn with_params(col_map: &'a ColumnMap, row: &'a [Value], params: &'a [Value]) -> Self {
142        Self {
143            col_map,
144            row,
145            params,
146            excluded: None,
147            old_new: None,
148            session_tz: None,
149        }
150    }
151
152    pub fn with_excluded(
153        col_map: &'a ColumnMap,
154        row: &'a [Value],
155        excluded_col_map: &'a ColumnMap,
156        excluded_row: &'a [Value],
157    ) -> Self {
158        Self {
159            col_map,
160            row,
161            params: &[],
162            excluded: Some(ExcludedRow {
163                col_map: excluded_col_map,
164                row: excluded_row,
165            }),
166            old_new: None,
167            session_tz: None,
168        }
169    }
170
171    pub fn with_old_new(
172        col_map: &'a ColumnMap,
173        row: &'a [Value],
174        old_row: Option<&'a [Value]>,
175        new_row: Option<&'a [Value]>,
176    ) -> Self {
177        Self {
178            col_map,
179            row,
180            params: &[],
181            excluded: None,
182            old_new: Some(OldNewRows {
183                col_map,
184                old_row,
185                new_row,
186            }),
187            session_tz: None,
188        }
189    }
190}
191
192thread_local! {
193    static SCOPED_PARAMS: std::cell::Cell<(*const Value, usize)> =
194        const { std::cell::Cell::new((std::ptr::null(), 0)) };
195}
196
197pub fn with_scoped_params<R>(params: &[Value], f: impl FnOnce() -> R) -> R {
198    struct Guard((*const Value, usize));
199    impl Drop for Guard {
200        fn drop(&mut self) {
201            SCOPED_PARAMS.with(|slot| slot.set(self.0));
202        }
203    }
204    SCOPED_PARAMS.with(|slot| {
205        let prev = slot.get();
206        slot.set((params.as_ptr(), params.len()));
207        let _guard = Guard(prev);
208        f()
209    })
210}
211
212fn resolve_parameter(n: usize, ctx_params: &[Value]) -> Result<Value> {
213    if !ctx_params.is_empty() {
214        if n == 0 || n > ctx_params.len() {
215            return Err(SqlError::ParameterCountMismatch {
216                expected: n,
217                got: ctx_params.len(),
218            });
219        }
220        return Ok(ctx_params[n - 1].clone());
221    }
222    resolve_scoped_param(n)
223}
224
225pub fn resolve_scoped_param(n: usize) -> Result<Value> {
226    SCOPED_PARAMS.with(|slot| {
227        let (ptr, len) = slot.get();
228        if n == 0 || n > len {
229            return Err(SqlError::ParameterCountMismatch {
230                expected: n,
231                got: len,
232            });
233        }
234        // SAFETY: `with_scoped_params` keeps the slice alive for the body's run.
235        unsafe { Ok((*ptr.add(n - 1)).clone()) }
236    })
237}
238
239pub fn eval_expr(expr: &Expr, ctx: &EvalCtx) -> Result<Value> {
240    match expr {
241        Expr::Literal(v) => Ok(v.clone()),
242
243        Expr::Column(name) => {
244            let idx = ctx.col_map.resolve(name)?;
245            Ok(ctx.row[idx].clone())
246        }
247
248        Expr::QualifiedColumn { table, column } => {
249            if let Some(excluded) = ctx.excluded.as_ref() {
250                if table.eq_ignore_ascii_case("excluded") {
251                    let lowered = column.to_ascii_lowercase();
252                    let idx = excluded.col_map.resolve(&lowered)?;
253                    return Ok(excluded.row[idx].clone());
254                }
255            }
256            if let Some(on) = ctx.old_new.as_ref() {
257                if table.eq_ignore_ascii_case("old") {
258                    let lowered = column.to_ascii_lowercase();
259                    let idx = on.col_map.resolve(&lowered)?;
260                    return Ok(on.old_row.map(|r| r[idx].clone()).unwrap_or(Value::Null));
261                }
262                if table.eq_ignore_ascii_case("new") {
263                    let lowered = column.to_ascii_lowercase();
264                    let idx = on.col_map.resolve(&lowered)?;
265                    return Ok(on.new_row.map(|r| r[idx].clone()).unwrap_or(Value::Null));
266                }
267            }
268            // Trigger-body fallback: nested executor calls don't carry `ctx.old_new`.
269            if table.eq_ignore_ascii_case("old") || table.eq_ignore_ascii_case("new") {
270                if let Some(b) = crate::executor::triggers::current_bindings() {
271                    let lowered = column.to_ascii_lowercase();
272                    if table.eq_ignore_ascii_case("old") {
273                        let cm = ColumnMap::new(&b.old_columns);
274                        let idx = cm.resolve(&lowered)?;
275                        return Ok(b.old_row.map(|r| r[idx].clone()).unwrap_or(Value::Null));
276                    } else {
277                        let cm = ColumnMap::new(&b.new_columns);
278                        let idx = cm.resolve(&lowered)?;
279                        return Ok(b.new_row.map(|r| r[idx].clone()).unwrap_or(Value::Null));
280                    }
281                }
282            }
283            let idx = ctx.col_map.resolve_qualified(table, column)?;
284            Ok(ctx.row[idx].clone())
285        }
286
287        Expr::BinaryOp { left, op, right } => {
288            let lval = eval_expr(left, ctx)?;
289            let rval = eval_expr(right, ctx)?;
290            let needs_collation_check = ctx.col_map.has_non_binary_collation()
291                || matches!(left.as_ref(), Expr::Collate { .. })
292                || matches!(right.as_ref(), Expr::Collate { .. });
293            if needs_collation_check {
294                let coll = collation_of(left)
295                    .or_else(|| collation_of(right))
296                    .or_else(|| {
297                        column_collation(left, ctx).or_else(|| column_collation(right, ctx))
298                    });
299                if let Some(c) = coll {
300                    if c != crate::types::Collation::Binary {
301                        if let Some(b) = eval_text_compare(&lval, *op, &rval, c) {
302                            return Ok(Value::Boolean(b));
303                        }
304                    }
305                }
306            }
307            eval_binary_op(&lval, *op, &rval)
308        }
309
310        Expr::UnaryOp { op, expr } => {
311            let val = eval_expr(expr, ctx)?;
312            eval_unary_op(*op, &val)
313        }
314
315        Expr::IsNull(e) => {
316            let val = eval_expr(e, ctx)?;
317            Ok(Value::Boolean(val.is_null()))
318        }
319
320        Expr::IsNotNull(e) => {
321            let val = eval_expr(e, ctx)?;
322            Ok(Value::Boolean(!val.is_null()))
323        }
324
325        Expr::Function { name, args, .. } => eval_scalar_function(name, args, ctx),
326
327        Expr::CountStar => Err(SqlError::Unsupported(
328            "COUNT(*) in non-aggregate context".into(),
329        )),
330
331        Expr::InList {
332            expr: e,
333            list,
334            negated,
335        } => {
336            let lhs = eval_expr(e, ctx)?;
337            eval_in_values(&lhs, list, ctx, *negated)
338        }
339
340        Expr::InSet {
341            expr: e,
342            values,
343            has_null,
344            negated,
345        } => {
346            let lhs = eval_expr(e, ctx)?;
347            eval_in_set(&lhs, values, *has_null, *negated)
348        }
349
350        Expr::Between {
351            expr: e,
352            low,
353            high,
354            negated,
355        } => {
356            let val = eval_expr(e, ctx)?;
357            let lo = eval_expr(low, ctx)?;
358            let hi = eval_expr(high, ctx)?;
359            eval_between(&val, &lo, &hi, *negated)
360        }
361
362        Expr::Like {
363            expr: e,
364            pattern,
365            escape,
366            negated,
367        } => {
368            let val = eval_expr(e, ctx)?;
369            let pat = eval_expr(pattern, ctx)?;
370            let esc = escape.as_ref().map(|e| eval_expr(e, ctx)).transpose()?;
371            eval_like(&val, &pat, esc.as_ref(), *negated)
372        }
373
374        Expr::Case {
375            operand,
376            conditions,
377            else_result,
378        } => eval_case(operand.as_deref(), conditions, else_result.as_deref(), ctx),
379
380        Expr::Coalesce(args) => {
381            for arg in args {
382                let val = eval_expr(arg, ctx)?;
383                if !val.is_null() {
384                    return Ok(val);
385                }
386            }
387            Ok(Value::Null)
388        }
389
390        Expr::Cast { expr: e, data_type } => {
391            let val = eval_expr(e, ctx)?;
392            eval_cast(&val, *data_type)
393        }
394
395        Expr::Collate { expr: e, .. } => eval_expr(e, ctx),
396
397        Expr::InSubquery { .. } | Expr::Exists { .. } | Expr::ScalarSubquery(_) => Err(
398            SqlError::Unsupported("subquery not materialized (internal error)".into()),
399        ),
400
401        Expr::Parameter(n) => resolve_parameter(*n, ctx.params),
402
403        Expr::WindowFunction { .. } => Err(SqlError::Unsupported(
404            "window functions are only allowed in SELECT columns".into(),
405        )),
406
407        Expr::TypedNullRecord(_) => Ok(Value::Null),
408
409        Expr::ArrayLiteral(elems) => {
410            let mut out = Vec::with_capacity(elems.len());
411            for e in elems {
412                out.push(eval_expr(e, ctx)?);
413            }
414            Ok(Value::Array(std::sync::Arc::new(out)))
415        }
416
417        Expr::Quantified {
418            left,
419            op,
420            quantifier,
421            right,
422        } => eval_quantified(left, *op, *quantifier, right, ctx),
423    }
424}
425
426fn eval_quantified(
427    left: &Expr,
428    op: crate::parser::BinOp,
429    quantifier: crate::parser::Quantifier,
430    right: &crate::parser::QuantifiedRhs,
431    ctx: &EvalCtx,
432) -> Result<Value> {
433    use crate::parser::{QuantifiedRhs, Quantifier};
434    let lhs = eval_expr(left, ctx)?;
435    let elems: Vec<Value> = match right {
436        QuantifiedRhs::Array(e) => match eval_expr(e, ctx)? {
437            Value::Array(a) => (*a).clone(),
438            Value::Null => return Ok(Value::Null),
439            other => {
440                return Err(SqlError::TypeMismatch {
441                    expected: "ARRAY".into(),
442                    got: other.data_type().to_string(),
443                });
444            }
445        },
446        QuantifiedRhs::Subquery(_) => {
447            return Err(SqlError::Unsupported(
448                "ANY/ALL subquery not materialized (internal error)".into(),
449            ));
450        }
451    };
452
453    if lhs.is_null() {
454        return if elems.is_empty() {
455            match quantifier {
456                Quantifier::Any => Ok(Value::Boolean(false)),
457                Quantifier::All => Ok(Value::Boolean(true)),
458            }
459        } else {
460            Ok(Value::Null)
461        };
462    }
463
464    let mut any_unknown = false;
465    let mut any_match = false;
466    let mut any_mismatch = false;
467    for elem in &elems {
468        if elem.is_null() {
469            any_unknown = true;
470            continue;
471        }
472        let result = eval_binary_compare(&lhs, op, elem)?;
473        match result {
474            Value::Boolean(true) => any_match = true,
475            Value::Boolean(false) => any_mismatch = true,
476            Value::Null => any_unknown = true,
477            _ => {
478                return Err(SqlError::TypeMismatch {
479                    expected: "BOOLEAN".into(),
480                    got: result.data_type().to_string(),
481                });
482            }
483        }
484    }
485
486    match quantifier {
487        Quantifier::Any => {
488            if any_match {
489                Ok(Value::Boolean(true))
490            } else if any_unknown {
491                Ok(Value::Null)
492            } else {
493                Ok(Value::Boolean(false))
494            }
495        }
496        Quantifier::All => {
497            if any_mismatch {
498                Ok(Value::Boolean(false))
499            } else if any_unknown {
500                Ok(Value::Null)
501            } else {
502                Ok(Value::Boolean(true))
503            }
504        }
505    }
506}
507
508fn eval_binary_compare(left: &Value, op: crate::parser::BinOp, right: &Value) -> Result<Value> {
509    use crate::parser::BinOp;
510    if left.is_null() || right.is_null() {
511        return Ok(Value::Null);
512    }
513    let cmp = match (left, right) {
514        (Value::Text(a), Value::Text(b)) => Some(a.cmp(b)),
515        _ => left.partial_cmp(right),
516    };
517    let Some(cmp) = cmp else {
518        return Ok(Value::Null);
519    };
520    use std::cmp::Ordering;
521    let result = match op {
522        BinOp::Eq => cmp == Ordering::Equal,
523        BinOp::NotEq => cmp != Ordering::Equal,
524        BinOp::Lt => cmp == Ordering::Less,
525        BinOp::Gt => cmp == Ordering::Greater,
526        BinOp::LtEq => cmp != Ordering::Greater,
527        BinOp::GtEq => cmp != Ordering::Less,
528        _ => {
529            return Err(SqlError::Unsupported(format!(
530                "ANY/ALL comparison op {op:?}"
531            )));
532        }
533    };
534    Ok(Value::Boolean(result))
535}
536
537fn collation_of(expr: &Expr) -> Option<crate::types::Collation> {
538    match expr {
539        Expr::Collate { collation, .. } => Some(*collation),
540        _ => None,
541    }
542}
543
544fn column_collation(expr: &Expr, ctx: &EvalCtx<'_>) -> Option<crate::types::Collation> {
545    match expr {
546        Expr::Column(name) => ctx
547            .col_map
548            .resolve(name)
549            .ok()
550            .map(|i| ctx.col_map.collation_at(i)),
551        Expr::QualifiedColumn { table, column } => ctx
552            .col_map
553            .resolve_qualified(table, column)
554            .ok()
555            .map(|i| ctx.col_map.collation_at(i)),
556        _ => None,
557    }
558}
559
560fn eval_text_compare(
561    left: &Value,
562    op: BinOp,
563    right: &Value,
564    coll: crate::types::Collation,
565) -> Option<bool> {
566    let (a, b) = match (left, right) {
567        (Value::Null, _) | (_, Value::Null) => return None,
568        (Value::Text(a), Value::Text(b)) => (a.as_str(), b.as_str()),
569        _ => return None,
570    };
571    let ord = coll.cmp_text(a, b);
572    Some(match op {
573        BinOp::Eq => ord == std::cmp::Ordering::Equal,
574        BinOp::NotEq => ord != std::cmp::Ordering::Equal,
575        BinOp::Lt => ord == std::cmp::Ordering::Less,
576        BinOp::Gt => ord == std::cmp::Ordering::Greater,
577        BinOp::LtEq => ord != std::cmp::Ordering::Greater,
578        BinOp::GtEq => ord != std::cmp::Ordering::Less,
579        _ => return None,
580    })
581}
582
583pub fn eval_binary_op_public(left: &Value, op: BinOp, right: &Value) -> Result<Value> {
584    eval_binary_op(left, op, right)
585}
586
587fn eval_binary_op(left: &Value, op: BinOp, right: &Value) -> Result<Value> {
588    match op {
589        BinOp::And => return eval_and(left, right),
590        BinOp::Or => return eval_or(left, right),
591        _ => {}
592    }
593
594    if left.is_null() || right.is_null() {
595        return Ok(Value::Null);
596    }
597
598    if let Some(res) = eval_temporal_op(left, op, right) {
599        return res;
600    }
601
602    match op {
603        BinOp::Eq => Ok(Value::Boolean(left == right)),
604        BinOp::NotEq => Ok(Value::Boolean(left != right)),
605        BinOp::Lt => Ok(Value::Boolean(left < right)),
606        BinOp::Gt => Ok(Value::Boolean(left > right)),
607        BinOp::LtEq => Ok(Value::Boolean(left <= right)),
608        BinOp::GtEq => Ok(Value::Boolean(left >= right)),
609        BinOp::Add => eval_arithmetic(left, right, i64::checked_add, |a, b| a + b),
610        BinOp::Sub => match left {
611            Value::Json(_) | Value::Jsonb(_) => crate::json::op_delete_one(left, right),
612            _ => eval_arithmetic(left, right, i64::checked_sub, |a, b| a - b),
613        },
614        BinOp::Mul => eval_arithmetic(left, right, i64::checked_mul, |a, b| a * b),
615        BinOp::Div => {
616            match right {
617                Value::Integer(0) => return Err(SqlError::DivisionByZero),
618                Value::Real(r) if *r == 0.0 => return Err(SqlError::DivisionByZero),
619                _ => {}
620            }
621            eval_arithmetic(left, right, i64::checked_div, |a, b| a / b)
622        }
623        BinOp::Mod => {
624            match right {
625                Value::Integer(0) => return Err(SqlError::DivisionByZero),
626                Value::Real(r) if *r == 0.0 => return Err(SqlError::DivisionByZero),
627                _ => {}
628            }
629            eval_arithmetic(left, right, i64::checked_rem, |a, b| a % b)
630        }
631        BinOp::Concat => match (left, right) {
632            (Value::TsVector(a), Value::TsVector(b)) => crate::fts::op_concat(a, b),
633            (Value::Json(_) | Value::Jsonb(_), _) | (_, Value::Json(_) | Value::Jsonb(_)) => {
634                crate::json::op_concat(left, right)
635            }
636            _ => {
637                let ls = value_to_text(left);
638                let rs = value_to_text(right);
639                Ok(Value::Text(format!("{ls}{rs}").into()))
640            }
641        },
642        BinOp::JsonGet
643        | BinOp::JsonGetText
644        | BinOp::JsonPath
645        | BinOp::JsonPathText
646        | BinOp::JsonContains
647        | BinOp::JsonContainedBy
648        | BinOp::JsonHasKey
649        | BinOp::JsonHasAnyKey
650        | BinOp::JsonHasAllKeys
651        | BinOp::JsonDeletePath
652        | BinOp::JsonPathExists
653        | BinOp::JsonPathMatch
654        | BinOp::JsonPathExistsTz
655        | BinOp::JsonPathMatchTz => eval_json_binary_op(left, op, right),
656        BinOp::And | BinOp::Or => unreachable!(),
657    }
658}
659
660#[cold]
661fn eval_json_binary_op(left: &Value, op: BinOp, right: &Value) -> Result<Value> {
662    match op {
663        BinOp::JsonGet => crate::json::op_get(left, right),
664        BinOp::JsonGetText => crate::json::op_get_text(left, right),
665        BinOp::JsonPath => crate::json::op_path(left, right),
666        BinOp::JsonPathText => crate::json::op_path_text(left, right),
667        BinOp::JsonContains => crate::json::op_contains(left, right),
668        BinOp::JsonContainedBy => crate::json::op_contained_by(left, right),
669        BinOp::JsonHasKey => crate::json::op_has_key(left, right),
670        BinOp::JsonHasAnyKey => crate::json::op_has_any_key(left, right),
671        BinOp::JsonHasAllKeys => crate::json::op_has_all_keys(left, right),
672        BinOp::JsonDeletePath => crate::json::op_delete_path(left, right),
673        BinOp::JsonPathExists => crate::json::op_path_exists(left, right),
674        BinOp::JsonPathMatch => eval_at_at(left, right),
675        BinOp::JsonPathExistsTz => {
676            crate::json::fn_jsonb_path_exists_tz(&[left.clone(), right.clone()])
677        }
678        BinOp::JsonPathMatchTz => {
679            crate::json::fn_jsonb_path_match_tz(&[left.clone(), right.clone()])
680        }
681        _ => unreachable!(),
682    }
683}
684
685fn eval_at_at(left: &Value, right: &Value) -> Result<Value> {
686    use crate::types::DataType as D;
687    if left.is_null() || right.is_null() {
688        return Ok(Value::Null);
689    }
690    match (left.data_type(), right.data_type()) {
691        (D::Json | D::Jsonb, D::Text) => crate::json::op_path_match(left, right),
692        (D::TsVector, D::TsQuery) => match (left, right) {
693            (Value::TsVector(v), Value::TsQuery(q)) => crate::fts::op_match(v, q),
694            _ => unreachable!(),
695        },
696        (D::TsQuery, D::TsVector) => match (left, right) {
697            (Value::TsQuery(q), Value::TsVector(v)) => crate::fts::op_match(v, q),
698            _ => unreachable!(),
699        },
700        (D::Text, D::TsQuery) => {
701            let s = match left {
702                Value::Text(s) => s.as_str(),
703                _ => unreachable!(),
704            };
705            let lhs = crate::fts::fn_to_tsvector(s)?;
706            eval_at_at(&lhs, right)
707        }
708        (D::TsVector, D::Text) => {
709            let s = match right {
710                Value::Text(s) => s.as_str(),
711                _ => unreachable!(),
712            };
713            let rhs = crate::fts::fn_plainto_tsquery(s)?;
714            eval_at_at(left, &rhs)
715        }
716        (D::Text, D::Text) => {
717            let ls = match left {
718                Value::Text(s) => s.as_str(),
719                _ => unreachable!(),
720            };
721            let rs = match right {
722                Value::Text(s) => s.as_str(),
723                _ => unreachable!(),
724            };
725            let lhs = crate::fts::fn_to_tsvector(ls)?;
726            let rhs = crate::fts::fn_plainto_tsquery(rs)?;
727            eval_at_at(&lhs, &rhs)
728        }
729        (lt, rt) => Err(SqlError::TypeMismatch {
730            expected: "JSONB @@ text, tsvector @@ tsquery".into(),
731            got: format!("{lt} @@ {rt}"),
732        }),
733    }
734}
735
736/// Returns `Some` when `(left, op, right)` is a temporal operation; `None` to fall through.
737fn eval_temporal_op(left: &Value, op: BinOp, right: &Value) -> Option<Result<Value>> {
738    use crate::datetime as dt;
739    use std::cmp::Ordering;
740
741    let is_temporal = |v: &Value| {
742        matches!(
743            v,
744            Value::Date(_) | Value::Time(_) | Value::Timestamp(_) | Value::Interval { .. }
745        )
746    };
747    if !is_temporal(left) && !is_temporal(right) {
748        return None;
749    }
750    if matches!(op, BinOp::Add | BinOp::Sub)
751        && ((is_temporal(left) && matches!(right, Value::Real(_)))
752            || (matches!(left, Value::Real(_)) && is_temporal(right)))
753    {
754        return Some(Err(SqlError::TypeMismatch {
755            expected: "INTEGER or INTERVAL for date/time arithmetic (use CAST for REAL)".into(),
756            got: format!("{} and {}", left.data_type(), right.data_type()),
757        }));
758    }
759
760    match (left, op, right) {
761        (Value::Date(d), BinOp::Add, Value::Integer(n))
762        | (Value::Integer(n), BinOp::Add, Value::Date(d)) => {
763            Some(dt::add_days_to_date(*d, *n).map(Value::Date))
764        }
765        (Value::Date(d), BinOp::Sub, Value::Integer(n)) => {
766            Some(dt::add_days_to_date(*d, -*n).map(Value::Date))
767        }
768        (Value::Date(a), BinOp::Sub, Value::Date(b)) => {
769            Some(Ok(Value::Integer(*a as i64 - *b as i64)))
770        }
771        // DATE ± INTERVAL → TIMESTAMP (PG rule).
772        (
773            Value::Date(d),
774            BinOp::Add,
775            Value::Interval {
776                months,
777                days,
778                micros,
779            },
780        )
781        | (
782            Value::Interval {
783                months,
784                days,
785                micros,
786            },
787            BinOp::Add,
788            Value::Date(d),
789        ) => Some(dt::add_interval_to_date(*d, *months, *days, *micros).map(Value::Timestamp)),
790        (
791            Value::Date(d),
792            BinOp::Sub,
793            Value::Interval {
794                months,
795                days,
796                micros,
797            },
798        ) => Some(dt::add_interval_to_date(*d, -*months, -*days, -*micros).map(Value::Timestamp)),
799        (
800            Value::Timestamp(t),
801            BinOp::Add,
802            Value::Interval {
803                months,
804                days,
805                micros,
806            },
807        )
808        | (
809            Value::Interval {
810                months,
811                days,
812                micros,
813            },
814            BinOp::Add,
815            Value::Timestamp(t),
816        ) => Some(dt::add_interval_to_timestamp(*t, *months, *days, *micros).map(Value::Timestamp)),
817        (
818            Value::Timestamp(t),
819            BinOp::Sub,
820            Value::Interval {
821                months,
822                days,
823                micros,
824            },
825        ) => Some(
826            dt::add_interval_to_timestamp(*t, -*months, -*days, -*micros).map(Value::Timestamp),
827        ),
828        (Value::Timestamp(a), BinOp::Sub, Value::Timestamp(b)) => {
829            let (days, micros) = dt::subtract_timestamps(*a, *b);
830            Some(Ok(Value::Interval {
831                months: 0,
832                days,
833                micros,
834            }))
835        }
836        (
837            Value::Time(t),
838            BinOp::Add,
839            Value::Interval {
840                months,
841                days,
842                micros,
843            },
844        ) => Some(dt::add_interval_to_time(*t, *months, *days, *micros).map(Value::Time)),
845        (
846            Value::Time(t),
847            BinOp::Sub,
848            Value::Interval {
849                months,
850                days,
851                micros,
852            },
853        ) => Some(dt::add_interval_to_time(*t, -*months, -*days, -*micros).map(Value::Time)),
854        (Value::Time(a), BinOp::Sub, Value::Time(b)) => Some(Ok(Value::Interval {
855            months: 0,
856            days: 0,
857            micros: *a - *b,
858        })),
859        (
860            Value::Interval {
861                months: am,
862                days: ad,
863                micros: au,
864            },
865            BinOp::Add,
866            Value::Interval {
867                months: bm,
868                days: bd,
869                micros: bu,
870            },
871        ) => Some(Ok(Value::Interval {
872            months: am.saturating_add(*bm),
873            days: ad.saturating_add(*bd),
874            micros: au.saturating_add(*bu),
875        })),
876        (
877            Value::Interval {
878                months: am,
879                days: ad,
880                micros: au,
881            },
882            BinOp::Sub,
883            Value::Interval {
884                months: bm,
885                days: bd,
886                micros: bu,
887            },
888        ) => Some(Ok(Value::Interval {
889            months: am.saturating_sub(*bm),
890            days: ad.saturating_sub(*bd),
891            micros: au.saturating_sub(*bu),
892        })),
893        (
894            Value::Interval {
895                months,
896                days,
897                micros,
898            },
899            BinOp::Mul,
900            Value::Integer(n),
901        )
902        | (
903            Value::Integer(n),
904            BinOp::Mul,
905            Value::Interval {
906                months,
907                days,
908                micros,
909            },
910        ) => {
911            let n32 = (*n).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
912            Some(Ok(Value::Interval {
913                months: months.saturating_mul(n32),
914                days: days.saturating_mul(n32),
915                micros: micros.saturating_mul(*n),
916            }))
917        }
918        // INTERVAL * REAL — fractional months → days, fractional days → micros (PG).
919        (
920            Value::Interval {
921                months,
922                days,
923                micros,
924            },
925            BinOp::Mul,
926            Value::Real(r),
927        )
928        | (
929            Value::Real(r),
930            BinOp::Mul,
931            Value::Interval {
932                months,
933                days,
934                micros,
935            },
936        ) => Some(Ok(scale_interval_by_real(*months, *days, *micros, *r))),
937        (
938            Value::Interval {
939                months,
940                days,
941                micros,
942            },
943            BinOp::Div,
944            Value::Integer(n),
945        ) if *n != 0 => Some(Ok(Value::Interval {
946            months: (*months as i64 / *n) as i32,
947            days: (*days as i64 / *n) as i32,
948            micros: *micros / *n,
949        })),
950        (
951            Value::Interval {
952                months,
953                days,
954                micros,
955            },
956            BinOp::Div,
957            Value::Real(r),
958        ) if *r != 0.0 => Some(Ok(scale_interval_by_real(*months, *days, *micros, 1.0 / r))),
959        // PG-normalized INTERVAL compare: 30-day month, 24-hour day.
960        (
961            Value::Interval {
962                months: am,
963                days: ad,
964                micros: au,
965            },
966            op,
967            Value::Interval {
968                months: bm,
969                days: bd,
970                micros: bu,
971            },
972        ) if matches!(
973            op,
974            BinOp::Eq | BinOp::NotEq | BinOp::Lt | BinOp::Gt | BinOp::LtEq | BinOp::GtEq
975        ) =>
976        {
977            let ord = dt::pg_normalized_interval_cmp((*am, *ad, *au), (*bm, *bd, *bu));
978            let b = match op {
979                BinOp::Eq => ord == Ordering::Equal,
980                BinOp::NotEq => ord != Ordering::Equal,
981                BinOp::Lt => ord == Ordering::Less,
982                BinOp::Gt => ord == Ordering::Greater,
983                BinOp::LtEq => ord != Ordering::Greater,
984                BinOp::GtEq => ord != Ordering::Less,
985                _ => unreachable!(),
986            };
987            Some(Ok(Value::Boolean(b)))
988        }
989        // PG rejects TIMESTAMP ± INTEGER; require CAST to INTERVAL.
990        (Value::Timestamp(_), BinOp::Add | BinOp::Sub, Value::Integer(_))
991        | (Value::Integer(_), BinOp::Add, Value::Timestamp(_)) => {
992            Some(Err(SqlError::TypeMismatch {
993                expected: "INTERVAL (use CAST or explicit unit)".into(),
994                got: format!("{} and {}", left.data_type(), right.data_type()),
995            }))
996        }
997        _ => None,
998    }
999}
1000
1001/// PG fractional-propagation: month frac → days (×30), day frac → micros (×86.4G).
1002fn scale_interval_by_real(months: i32, days: i32, micros: i64, factor: f64) -> Value {
1003    let raw_months = months as f64 * factor;
1004    let whole_months = raw_months.trunc() as i64;
1005    let frac_months = raw_months - whole_months as f64;
1006    let months_frac_as_days = frac_months * 30.0;
1007
1008    let raw_days = days as f64 * factor + months_frac_as_days;
1009    let whole_days = raw_days.trunc() as i64;
1010    let frac_days = raw_days - whole_days as f64;
1011    let days_frac_as_micros = (frac_days * crate::datetime::MICROS_PER_DAY as f64).round() as i64;
1012
1013    let raw_micros = (micros as f64 * factor).round() as i64;
1014    let total_micros = raw_micros.saturating_add(days_frac_as_micros);
1015
1016    let clamp_i32 = |n: i64| n.clamp(i32::MIN as i64, i32::MAX as i64) as i32;
1017    Value::Interval {
1018        months: clamp_i32(whole_months),
1019        days: clamp_i32(whole_days),
1020        micros: total_micros,
1021    }
1022}
1023
1024/// SQL three-valued AND: NULL AND false = false, NULL AND true = NULL
1025fn eval_and(left: &Value, right: &Value) -> Result<Value> {
1026    let l = to_bool_or_null(left)?;
1027    let r = to_bool_or_null(right)?;
1028    match (l, r) {
1029        (Some(false), _) | (_, Some(false)) => Ok(Value::Boolean(false)),
1030        (Some(true), Some(true)) => Ok(Value::Boolean(true)),
1031        _ => Ok(Value::Null),
1032    }
1033}
1034
1035/// SQL three-valued OR: NULL OR true = true, NULL OR false = NULL
1036fn eval_or(left: &Value, right: &Value) -> Result<Value> {
1037    let l = to_bool_or_null(left)?;
1038    let r = to_bool_or_null(right)?;
1039    match (l, r) {
1040        (Some(true), _) | (_, Some(true)) => Ok(Value::Boolean(true)),
1041        (Some(false), Some(false)) => Ok(Value::Boolean(false)),
1042        _ => Ok(Value::Null),
1043    }
1044}
1045
1046fn to_bool_or_null(val: &Value) -> Result<Option<bool>> {
1047    match val {
1048        Value::Boolean(b) => Ok(Some(*b)),
1049        Value::Null => Ok(None),
1050        Value::Integer(i) => Ok(Some(*i != 0)),
1051        _ => Err(SqlError::TypeMismatch {
1052            expected: "BOOLEAN".into(),
1053            got: format!("{}", val.data_type()),
1054        }),
1055    }
1056}
1057
1058fn eval_arithmetic(
1059    left: &Value,
1060    right: &Value,
1061    int_op: fn(i64, i64) -> Option<i64>,
1062    real_op: fn(f64, f64) -> f64,
1063) -> Result<Value> {
1064    match (left, right) {
1065        (Value::Integer(a), Value::Integer(b)) => int_op(*a, *b)
1066            .map(Value::Integer)
1067            .ok_or(SqlError::IntegerOverflow),
1068        (Value::Real(a), Value::Real(b)) => Ok(Value::Real(real_op(*a, *b))),
1069        (Value::Integer(a), Value::Real(b)) => Ok(Value::Real(real_op(*a as f64, *b))),
1070        (Value::Real(a), Value::Integer(b)) => Ok(Value::Real(real_op(*a, *b as f64))),
1071        _ => Err(SqlError::TypeMismatch {
1072            expected: "numeric".into(),
1073            got: format!("{} and {}", left.data_type(), right.data_type()),
1074        }),
1075    }
1076}
1077
1078fn eval_in_values(lhs: &Value, list: &[Expr], ctx: &EvalCtx, negated: bool) -> Result<Value> {
1079    if list.is_empty() {
1080        return Ok(Value::Boolean(negated));
1081    }
1082    if lhs.is_null() {
1083        return Ok(Value::Null);
1084    }
1085    let mut has_null = false;
1086    for item in list {
1087        let rhs = eval_expr(item, ctx)?;
1088        if rhs.is_null() {
1089            has_null = true;
1090        } else if lhs == &rhs {
1091            return Ok(Value::Boolean(!negated));
1092        }
1093    }
1094    if has_null {
1095        Ok(Value::Null)
1096    } else {
1097        Ok(Value::Boolean(negated))
1098    }
1099}
1100
1101fn eval_in_set(
1102    lhs: &Value,
1103    values: &rustc_hash::FxHashSet<Value>,
1104    has_null: bool,
1105    negated: bool,
1106) -> Result<Value> {
1107    if values.is_empty() && !has_null {
1108        return Ok(Value::Boolean(negated));
1109    }
1110    if lhs.is_null() {
1111        return Ok(Value::Null);
1112    }
1113    if values.contains(lhs) {
1114        return Ok(Value::Boolean(!negated));
1115    }
1116    if has_null {
1117        Ok(Value::Null)
1118    } else {
1119        Ok(Value::Boolean(negated))
1120    }
1121}
1122
1123fn eval_unary_op(op: UnaryOp, val: &Value) -> Result<Value> {
1124    if val.is_null() {
1125        return Ok(Value::Null);
1126    }
1127    match op {
1128        UnaryOp::Neg => match val {
1129            Value::Integer(i) => i
1130                .checked_neg()
1131                .map(Value::Integer)
1132                .ok_or(SqlError::IntegerOverflow),
1133            Value::Real(r) => Ok(Value::Real(-r)),
1134            Value::Interval {
1135                months,
1136                days,
1137                micros,
1138            } => {
1139                let m = months.checked_neg().ok_or(SqlError::IntegerOverflow)?;
1140                let d = days.checked_neg().ok_or(SqlError::IntegerOverflow)?;
1141                let u = micros.checked_neg().ok_or(SqlError::IntegerOverflow)?;
1142                Ok(Value::Interval {
1143                    months: m,
1144                    days: d,
1145                    micros: u,
1146                })
1147            }
1148            _ => Err(SqlError::TypeMismatch {
1149                expected: "numeric or INTERVAL".into(),
1150                got: format!("{}", val.data_type()),
1151            }),
1152        },
1153        UnaryOp::Not => match val {
1154            Value::Boolean(b) => Ok(Value::Boolean(!b)),
1155            Value::Integer(i) => Ok(Value::Boolean(*i == 0)),
1156            _ => Err(SqlError::TypeMismatch {
1157                expected: "BOOLEAN".into(),
1158                got: format!("{}", val.data_type()),
1159            }),
1160        },
1161    }
1162}
1163
1164fn value_to_text(val: &Value) -> String {
1165    match val {
1166        Value::Text(s) => s.to_string(),
1167        Value::Integer(i) => i.to_string(),
1168        Value::Real(r) => {
1169            if r.fract() == 0.0 && r.is_finite() {
1170                format!("{r:.1}")
1171            } else {
1172                format!("{r}")
1173            }
1174        }
1175        Value::Boolean(b) => if *b { "TRUE" } else { "FALSE" }.into(),
1176        Value::Null => String::new(),
1177        Value::Blob(b) => {
1178            let mut s = String::with_capacity(b.len() * 2);
1179            for byte in b {
1180                s.push_str(&format!("{byte:02X}"));
1181            }
1182            s
1183        }
1184        Value::Date(d) => crate::datetime::format_date(*d),
1185        Value::Time(t) => crate::datetime::format_time(*t),
1186        Value::Timestamp(t) => crate::datetime::format_timestamp(*t),
1187        Value::Interval {
1188            months,
1189            days,
1190            micros,
1191        } => crate::datetime::format_interval(*months, *days, *micros),
1192        Value::Json(s) => s.to_string(),
1193        Value::Jsonb(b) => crate::json::decode_to_text(b).unwrap_or_default(),
1194        Value::TsVector(b) => crate::fts::tsvector_display(b),
1195        Value::TsQuery(b) => crate::fts::tsquery_display(b),
1196        Value::Array(_) => val.to_string(),
1197    }
1198}
1199
1200fn eval_between(val: &Value, low: &Value, high: &Value, negated: bool) -> Result<Value> {
1201    if val.is_null() || low.is_null() || high.is_null() {
1202        let ge = if val.is_null() || low.is_null() {
1203            None
1204        } else {
1205            Some(*val >= *low)
1206        };
1207        let le = if val.is_null() || high.is_null() {
1208            None
1209        } else {
1210            Some(*val <= *high)
1211        };
1212
1213        let result = match (ge, le) {
1214            (Some(false), _) | (_, Some(false)) => Some(false),
1215            (Some(true), Some(true)) => Some(true),
1216            _ => None,
1217        };
1218
1219        return match result {
1220            Some(b) => Ok(Value::Boolean(if negated { !b } else { b })),
1221            None => Ok(Value::Null),
1222        };
1223    }
1224
1225    let in_range = *val >= *low && *val <= *high;
1226    Ok(Value::Boolean(if negated { !in_range } else { in_range }))
1227}
1228
1229const MAX_LIKE_PATTERN_LEN: usize = 10_000;
1230
1231fn eval_like(val: &Value, pattern: &Value, escape: Option<&Value>, negated: bool) -> Result<Value> {
1232    if val.is_null() || pattern.is_null() {
1233        return Ok(Value::Null);
1234    }
1235    let text = match val {
1236        Value::Text(s) => s.as_str(),
1237        _ => {
1238            return Err(SqlError::TypeMismatch {
1239                expected: "TEXT".into(),
1240                got: val.data_type().to_string(),
1241            })
1242        }
1243    };
1244    let pat = match pattern {
1245        Value::Text(s) => s.as_str(),
1246        _ => {
1247            return Err(SqlError::TypeMismatch {
1248                expected: "TEXT".into(),
1249                got: pattern.data_type().to_string(),
1250            })
1251        }
1252    };
1253
1254    if pat.len() > MAX_LIKE_PATTERN_LEN {
1255        return Err(SqlError::InvalidValue(format!(
1256            "LIKE pattern too long ({} chars, max {MAX_LIKE_PATTERN_LEN})",
1257            pat.len()
1258        )));
1259    }
1260
1261    let esc_char = match escape {
1262        Some(Value::Text(s)) => {
1263            let mut chars = s.chars();
1264            let c = chars.next().ok_or_else(|| {
1265                SqlError::InvalidValue("ESCAPE must be a single character".into())
1266            })?;
1267            if chars.next().is_some() {
1268                return Err(SqlError::InvalidValue(
1269                    "ESCAPE must be a single character".into(),
1270                ));
1271            }
1272            Some(c)
1273        }
1274        Some(Value::Null) => return Ok(Value::Null),
1275        Some(_) => {
1276            return Err(SqlError::TypeMismatch {
1277                expected: "TEXT".into(),
1278                got: "non-text".into(),
1279            })
1280        }
1281        None => None,
1282    };
1283
1284    let matched = like_match(text, pat, esc_char);
1285    Ok(Value::Boolean(if negated { !matched } else { matched }))
1286}
1287
1288fn like_match(text: &str, pattern: &str, escape: Option<char>) -> bool {
1289    let t: Vec<char> = text.chars().collect();
1290    let p: Vec<char> = pattern.chars().collect();
1291    like_match_impl(&t, &p, 0, 0, escape)
1292}
1293
1294fn like_match_impl(
1295    t: &[char],
1296    p: &[char],
1297    mut ti: usize,
1298    mut pi: usize,
1299    esc: Option<char>,
1300) -> bool {
1301    let mut star_pi: Option<usize> = None;
1302    let mut star_ti: usize = 0;
1303
1304    while ti < t.len() {
1305        if pi < p.len() {
1306            if let Some(ec) = esc {
1307                if p[pi] == ec && pi + 1 < p.len() {
1308                    pi += 1;
1309                    let pc_lower = p[pi].to_ascii_lowercase();
1310                    let tc_lower = t[ti].to_ascii_lowercase();
1311                    if pc_lower == tc_lower {
1312                        pi += 1;
1313                        ti += 1;
1314                        continue;
1315                    } else if let Some(sp) = star_pi {
1316                        pi = sp + 1;
1317                        star_ti += 1;
1318                        ti = star_ti;
1319                        continue;
1320                    } else {
1321                        return false;
1322                    }
1323                }
1324            }
1325            if p[pi] == '%' {
1326                star_pi = Some(pi);
1327                star_ti = ti;
1328                pi += 1;
1329                continue;
1330            }
1331            if p[pi] == '_' {
1332                pi += 1;
1333                ti += 1;
1334                continue;
1335            }
1336            if p[pi].eq_ignore_ascii_case(&t[ti]) {
1337                pi += 1;
1338                ti += 1;
1339                continue;
1340            }
1341        }
1342        if let Some(sp) = star_pi {
1343            pi = sp + 1;
1344            star_ti += 1;
1345            ti = star_ti;
1346        } else {
1347            return false;
1348        }
1349    }
1350
1351    while pi < p.len() && p[pi] == '%' {
1352        pi += 1;
1353    }
1354    pi == p.len()
1355}
1356
1357fn eval_case(
1358    operand: Option<&Expr>,
1359    conditions: &[(Expr, Expr)],
1360    else_result: Option<&Expr>,
1361    ctx: &EvalCtx,
1362) -> Result<Value> {
1363    if let Some(op_expr) = operand {
1364        let op_val = eval_expr(op_expr, ctx)?;
1365        for (cond, result) in conditions {
1366            let cond_val = eval_expr(cond, ctx)?;
1367            if !op_val.is_null() && !cond_val.is_null() && op_val == cond_val {
1368                return eval_expr(result, ctx);
1369            }
1370        }
1371    } else {
1372        for (cond, result) in conditions {
1373            let cond_val = eval_expr(cond, ctx)?;
1374            if is_truthy(&cond_val) {
1375                return eval_expr(result, ctx);
1376            }
1377        }
1378    }
1379    match else_result {
1380        Some(e) => eval_expr(e, ctx),
1381        None => Ok(Value::Null),
1382    }
1383}
1384
1385pub(crate) fn eval_cast(val: &Value, target: DataType) -> Result<Value> {
1386    if val.is_null() {
1387        return Ok(Value::Null);
1388    }
1389    match target {
1390        DataType::Integer => match val {
1391            Value::Integer(_) => Ok(val.clone()),
1392            Value::Real(r) => Ok(Value::Integer(*r as i64)),
1393            Value::Boolean(b) => Ok(Value::Integer(if *b { 1 } else { 0 })),
1394            Value::Text(s) => s
1395                .trim()
1396                .parse::<i64>()
1397                .map(Value::Integer)
1398                .or_else(|_| s.trim().parse::<f64>().map(|f| Value::Integer(f as i64)))
1399                .map_err(|_| SqlError::InvalidValue(format!("cannot cast '{s}' to INTEGER"))),
1400            _ => Err(SqlError::InvalidValue(format!(
1401                "cannot cast {} to INTEGER",
1402                val.data_type()
1403            ))),
1404        },
1405        DataType::Real => match val {
1406            Value::Real(_) => Ok(val.clone()),
1407            Value::Integer(i) => Ok(Value::Real(*i as f64)),
1408            Value::Boolean(b) => Ok(Value::Real(if *b { 1.0 } else { 0.0 })),
1409            Value::Text(s) => s
1410                .trim()
1411                .parse::<f64>()
1412                .map(Value::Real)
1413                .map_err(|_| SqlError::InvalidValue(format!("cannot cast '{s}' to REAL"))),
1414            _ => Err(SqlError::InvalidValue(format!(
1415                "cannot cast {} to REAL",
1416                val.data_type()
1417            ))),
1418        },
1419        DataType::Text => Ok(Value::Text(value_to_text(val).into())),
1420        DataType::Boolean => match val {
1421            Value::Boolean(_) => Ok(val.clone()),
1422            Value::Integer(i) => Ok(Value::Boolean(*i != 0)),
1423            Value::Text(s) => {
1424                let lower = s.trim().to_ascii_lowercase();
1425                match lower.as_str() {
1426                    "true" | "1" | "yes" | "on" => Ok(Value::Boolean(true)),
1427                    "false" | "0" | "no" | "off" => Ok(Value::Boolean(false)),
1428                    _ => Err(SqlError::InvalidValue(format!(
1429                        "cannot cast '{s}' to BOOLEAN"
1430                    ))),
1431                }
1432            }
1433            _ => Err(SqlError::InvalidValue(format!(
1434                "cannot cast {} to BOOLEAN",
1435                val.data_type()
1436            ))),
1437        },
1438        DataType::Blob => match val {
1439            Value::Blob(_) => Ok(val.clone()),
1440            Value::Text(s) => Ok(Value::Blob(s.as_bytes().to_vec())),
1441            _ => Err(SqlError::InvalidValue(format!(
1442                "cannot cast {} to BLOB",
1443                val.data_type()
1444            ))),
1445        },
1446        DataType::Null => Ok(Value::Null),
1447        DataType::Date => val.clone().coerce_into(DataType::Date).ok_or_else(|| {
1448            SqlError::InvalidValue(format!("cannot cast {} to DATE", val.data_type()))
1449        }),
1450        DataType::Time => val.clone().coerce_into(DataType::Time).ok_or_else(|| {
1451            SqlError::InvalidValue(format!("cannot cast {} to TIME", val.data_type()))
1452        }),
1453        DataType::Timestamp => val.clone().coerce_into(DataType::Timestamp).ok_or_else(|| {
1454            SqlError::InvalidValue(format!("cannot cast {} to TIMESTAMP", val.data_type()))
1455        }),
1456        DataType::Interval => val.clone().coerce_into(DataType::Interval).ok_or_else(|| {
1457            SqlError::InvalidValue(format!("cannot cast {} to INTERVAL", val.data_type()))
1458        }),
1459        DataType::Json => val.clone().coerce_into(DataType::Json).ok_or_else(|| {
1460            SqlError::InvalidValue(format!("cannot cast {} to JSON", val.data_type()))
1461        }),
1462        DataType::Jsonb => val.clone().coerce_into(DataType::Jsonb).ok_or_else(|| {
1463            SqlError::InvalidValue(format!("cannot cast {} to JSONB", val.data_type()))
1464        }),
1465        DataType::TsVector => val.clone().coerce_into(DataType::TsVector).ok_or_else(|| {
1466            SqlError::InvalidValue(format!("cannot cast {} to TSVECTOR", val.data_type()))
1467        }),
1468        DataType::TsQuery => val.clone().coerce_into(DataType::TsQuery).ok_or_else(|| {
1469            SqlError::InvalidValue(format!("cannot cast {} to TSQUERY", val.data_type()))
1470        }),
1471        DataType::Array => val.clone().coerce_into(DataType::Array).ok_or_else(|| {
1472            SqlError::InvalidValue(format!("cannot cast {} to ARRAY", val.data_type()))
1473        }),
1474    }
1475}
1476
1477fn eval_scalar_function(name: &str, args: &[Expr], ctx: &EvalCtx) -> Result<Value> {
1478    let evaluated: Vec<Value> = args
1479        .iter()
1480        .map(|a| eval_expr(a, ctx))
1481        .collect::<Result<Vec<_>>>()?;
1482
1483    match name {
1484        "LENGTH" => {
1485            check_args(name, &evaluated, 1)?;
1486            match &evaluated[0] {
1487                Value::Null => Ok(Value::Null),
1488                Value::Text(s) => Ok(Value::Integer(s.chars().count() as i64)),
1489                Value::Blob(b) => Ok(Value::Integer(b.len() as i64)),
1490                Value::TsVector(b) => crate::fts::fn_length_tsvector(b),
1491                _ => Ok(Value::Integer(
1492                    value_to_text(&evaluated[0]).chars().count() as i64
1493                )),
1494            }
1495        }
1496        "UPPER" => {
1497            check_args(name, &evaluated, 1)?;
1498            match &evaluated[0] {
1499                Value::Null => Ok(Value::Null),
1500                Value::Text(s) => Ok(Value::Text(s.to_ascii_uppercase())),
1501                _ => Ok(Value::Text(
1502                    value_to_text(&evaluated[0]).to_ascii_uppercase().into(),
1503                )),
1504            }
1505        }
1506        "LOWER" => {
1507            check_args(name, &evaluated, 1)?;
1508            match &evaluated[0] {
1509                Value::Null => Ok(Value::Null),
1510                Value::Text(s) => Ok(Value::Text(s.to_ascii_lowercase())),
1511                _ => Ok(Value::Text(
1512                    value_to_text(&evaluated[0]).to_ascii_lowercase().into(),
1513                )),
1514            }
1515        }
1516        "SUBSTR" | "SUBSTRING" => {
1517            if evaluated.len() < 2 || evaluated.len() > 3 {
1518                return Err(SqlError::InvalidValue(format!(
1519                    "{name} requires 2 or 3 arguments"
1520                )));
1521            }
1522            if evaluated.iter().any(|v| v.is_null()) {
1523                return Ok(Value::Null);
1524            }
1525            let s = value_to_text(&evaluated[0]);
1526            let chars: Vec<char> = s.chars().collect();
1527            let start = match &evaluated[1] {
1528                Value::Integer(i) => *i,
1529                _ => {
1530                    return Err(SqlError::TypeMismatch {
1531                        expected: "INTEGER".into(),
1532                        got: evaluated[1].data_type().to_string(),
1533                    })
1534                }
1535            };
1536            let len = chars.len() as i64;
1537
1538            let (begin, count) = if evaluated.len() == 3 {
1539                let cnt = match &evaluated[2] {
1540                    Value::Integer(i) => *i,
1541                    _ => {
1542                        return Err(SqlError::TypeMismatch {
1543                            expected: "INTEGER".into(),
1544                            got: evaluated[2].data_type().to_string(),
1545                        })
1546                    }
1547                };
1548                if start >= 1 {
1549                    let b = (start - 1).min(len) as usize;
1550                    let c = cnt.max(0) as usize;
1551                    (b, c)
1552                } else if start == 0 {
1553                    let c = (cnt - 1).max(0) as usize;
1554                    (0usize, c)
1555                } else {
1556                    let adjusted_cnt = (cnt + start - 1).max(0) as usize;
1557                    (0usize, adjusted_cnt)
1558                }
1559            } else if start >= 1 {
1560                let b = (start - 1).min(len) as usize;
1561                (b, chars.len() - b)
1562            } else if start == 0 {
1563                (0usize, chars.len())
1564            } else {
1565                let b = (len + start).max(0) as usize;
1566                (b, chars.len() - b)
1567            };
1568
1569            let result: String = chars.iter().skip(begin).take(count).collect();
1570            Ok(Value::Text(result.into()))
1571        }
1572        "TRIM" | "LTRIM" | "RTRIM" => {
1573            if evaluated.is_empty() || evaluated.len() > 2 {
1574                return Err(SqlError::InvalidValue(format!(
1575                    "{name} requires 1 or 2 arguments"
1576                )));
1577            }
1578            if evaluated[0].is_null() {
1579                return Ok(Value::Null);
1580            }
1581            let s = value_to_text(&evaluated[0]);
1582            let trim_chars: Vec<char> = if evaluated.len() == 2 {
1583                if evaluated[1].is_null() {
1584                    return Ok(Value::Null);
1585                }
1586                value_to_text(&evaluated[1]).chars().collect()
1587            } else {
1588                vec![' ']
1589            };
1590            let result = match name {
1591                "TRIM" => s
1592                    .trim_matches(|c: char| trim_chars.contains(&c))
1593                    .to_string(),
1594                "LTRIM" => s
1595                    .trim_start_matches(|c: char| trim_chars.contains(&c))
1596                    .to_string(),
1597                "RTRIM" => s
1598                    .trim_end_matches(|c: char| trim_chars.contains(&c))
1599                    .to_string(),
1600                _ => unreachable!(),
1601            };
1602            Ok(Value::Text(result.into()))
1603        }
1604        "REPLACE" => {
1605            check_args(name, &evaluated, 3)?;
1606            if evaluated.iter().any(|v| v.is_null()) {
1607                return Ok(Value::Null);
1608            }
1609            let s = value_to_text(&evaluated[0]);
1610            let from = value_to_text(&evaluated[1]);
1611            let to = value_to_text(&evaluated[2]);
1612            if from.is_empty() {
1613                return Ok(Value::Text(s.into()));
1614            }
1615            Ok(Value::Text(s.replace(&from, &to).into()))
1616        }
1617        "INSTR" => {
1618            check_args(name, &evaluated, 2)?;
1619            if evaluated.iter().any(|v| v.is_null()) {
1620                return Ok(Value::Null);
1621            }
1622            let haystack = value_to_text(&evaluated[0]);
1623            let needle = value_to_text(&evaluated[1]);
1624            let pos = haystack
1625                .find(&needle)
1626                .map(|i| haystack[..i].chars().count() as i64 + 1)
1627                .unwrap_or(0);
1628            Ok(Value::Integer(pos))
1629        }
1630        "CONCAT" => {
1631            if evaluated.is_empty() {
1632                return Ok(Value::Text(CompactString::default()));
1633            }
1634            let mut result = String::new();
1635            for v in &evaluated {
1636                match v {
1637                    Value::Null => {}
1638                    _ => result.push_str(&value_to_text(v)),
1639                }
1640            }
1641            Ok(Value::Text(result.into()))
1642        }
1643        "ABS" => {
1644            check_args(name, &evaluated, 1)?;
1645            match &evaluated[0] {
1646                Value::Null => Ok(Value::Null),
1647                Value::Integer(i) => i
1648                    .checked_abs()
1649                    .map(Value::Integer)
1650                    .ok_or(SqlError::IntegerOverflow),
1651                Value::Real(r) => Ok(Value::Real(r.abs())),
1652                _ => Err(SqlError::TypeMismatch {
1653                    expected: "numeric".into(),
1654                    got: evaluated[0].data_type().to_string(),
1655                }),
1656            }
1657        }
1658        "ROUND" => {
1659            if evaluated.is_empty() || evaluated.len() > 2 {
1660                return Err(SqlError::InvalidValue(
1661                    "ROUND requires 1 or 2 arguments".into(),
1662                ));
1663            }
1664            if evaluated[0].is_null() {
1665                return Ok(Value::Null);
1666            }
1667            let val = match &evaluated[0] {
1668                Value::Integer(i) => *i as f64,
1669                Value::Real(r) => *r,
1670                _ => {
1671                    return Err(SqlError::TypeMismatch {
1672                        expected: "numeric".into(),
1673                        got: evaluated[0].data_type().to_string(),
1674                    })
1675                }
1676            };
1677            let places = if evaluated.len() == 2 {
1678                match &evaluated[1] {
1679                    Value::Null => return Ok(Value::Null),
1680                    Value::Integer(i) => *i,
1681                    _ => {
1682                        return Err(SqlError::TypeMismatch {
1683                            expected: "INTEGER".into(),
1684                            got: evaluated[1].data_type().to_string(),
1685                        })
1686                    }
1687                }
1688            } else {
1689                0
1690            };
1691            let factor = 10f64.powi(places as i32);
1692            let rounded = (val * factor).round() / factor;
1693            Ok(Value::Real(rounded))
1694        }
1695        "CEIL" | "CEILING" => {
1696            check_args(name, &evaluated, 1)?;
1697            match &evaluated[0] {
1698                Value::Null => Ok(Value::Null),
1699                Value::Integer(i) => Ok(Value::Integer(*i)),
1700                Value::Real(r) => Ok(Value::Integer(r.ceil() as i64)),
1701                _ => Err(SqlError::TypeMismatch {
1702                    expected: "numeric".into(),
1703                    got: evaluated[0].data_type().to_string(),
1704                }),
1705            }
1706        }
1707        "FLOOR" => {
1708            check_args(name, &evaluated, 1)?;
1709            match &evaluated[0] {
1710                Value::Null => Ok(Value::Null),
1711                Value::Integer(i) => Ok(Value::Integer(*i)),
1712                Value::Real(r) => Ok(Value::Integer(r.floor() as i64)),
1713                _ => Err(SqlError::TypeMismatch {
1714                    expected: "numeric".into(),
1715                    got: evaluated[0].data_type().to_string(),
1716                }),
1717            }
1718        }
1719        "SIGN" => {
1720            check_args(name, &evaluated, 1)?;
1721            match &evaluated[0] {
1722                Value::Null => Ok(Value::Null),
1723                Value::Integer(i) => Ok(Value::Integer(i.signum())),
1724                Value::Real(r) => {
1725                    if *r > 0.0 {
1726                        Ok(Value::Integer(1))
1727                    } else if *r < 0.0 {
1728                        Ok(Value::Integer(-1))
1729                    } else {
1730                        Ok(Value::Integer(0))
1731                    }
1732                }
1733                _ => Err(SqlError::TypeMismatch {
1734                    expected: "numeric".into(),
1735                    got: evaluated[0].data_type().to_string(),
1736                }),
1737            }
1738        }
1739        "SQRT" => {
1740            check_args(name, &evaluated, 1)?;
1741            match &evaluated[0] {
1742                Value::Null => Ok(Value::Null),
1743                Value::Integer(i) => {
1744                    if *i < 0 {
1745                        Ok(Value::Null)
1746                    } else {
1747                        Ok(Value::Real((*i as f64).sqrt()))
1748                    }
1749                }
1750                Value::Real(r) => {
1751                    if *r < 0.0 {
1752                        Ok(Value::Null)
1753                    } else {
1754                        Ok(Value::Real(r.sqrt()))
1755                    }
1756                }
1757                _ => Err(SqlError::TypeMismatch {
1758                    expected: "numeric".into(),
1759                    got: evaluated[0].data_type().to_string(),
1760                }),
1761            }
1762        }
1763        "RANDOM" => {
1764            check_args(name, &evaluated, 0)?;
1765            use std::collections::hash_map::DefaultHasher;
1766            use std::hash::{Hash, Hasher};
1767            use std::time::SystemTime;
1768            let mut hasher = DefaultHasher::new();
1769            SystemTime::now().hash(&mut hasher);
1770            std::thread::current().id().hash(&mut hasher);
1771            let mut val = hasher.finish() as i64;
1772            if val == i64::MIN {
1773                val = i64::MAX;
1774            }
1775            Ok(Value::Integer(val))
1776        }
1777        "TYPEOF" => {
1778            check_args(name, &evaluated, 1)?;
1779            let type_name = match &evaluated[0] {
1780                Value::Null => "null",
1781                Value::Integer(_) => "integer",
1782                Value::Real(_) => "real",
1783                Value::Text(_) => "text",
1784                Value::Blob(_) => "blob",
1785                Value::Boolean(_) => "boolean",
1786                Value::Date(_) => "date",
1787                Value::Time(_) => "time",
1788                Value::Timestamp(_) => "timestamp",
1789                Value::Interval { .. } => "interval",
1790                Value::Json(_) => "json",
1791                Value::Jsonb(_) => "jsonb",
1792                Value::TsVector(_) => "tsvector",
1793                Value::TsQuery(_) => "tsquery",
1794                Value::Array(_) => "array",
1795            };
1796            Ok(Value::Text(type_name.into()))
1797        }
1798        "MIN" => {
1799            check_args(name, &evaluated, 2)?;
1800            if evaluated[0].is_null() {
1801                return Ok(evaluated[1].clone());
1802            }
1803            if evaluated[1].is_null() {
1804                return Ok(evaluated[0].clone());
1805            }
1806            if evaluated[0] <= evaluated[1] {
1807                Ok(evaluated[0].clone())
1808            } else {
1809                Ok(evaluated[1].clone())
1810            }
1811        }
1812        "MAX" => {
1813            check_args(name, &evaluated, 2)?;
1814            if evaluated[0].is_null() {
1815                return Ok(evaluated[1].clone());
1816            }
1817            if evaluated[1].is_null() {
1818                return Ok(evaluated[0].clone());
1819            }
1820            if evaluated[0] >= evaluated[1] {
1821                Ok(evaluated[0].clone())
1822            } else {
1823                Ok(evaluated[1].clone())
1824            }
1825        }
1826        "HEX" => {
1827            check_args(name, &evaluated, 1)?;
1828            match &evaluated[0] {
1829                Value::Null => Ok(Value::Null),
1830                Value::Blob(b) => {
1831                    let mut s = String::with_capacity(b.len() * 2);
1832                    for byte in b {
1833                        s.push_str(&format!("{byte:02X}"));
1834                    }
1835                    Ok(Value::Text(s.into()))
1836                }
1837                Value::Text(s) => {
1838                    let mut r = String::with_capacity(s.len() * 2);
1839                    for byte in s.as_bytes() {
1840                        r.push_str(&format!("{byte:02X}"));
1841                    }
1842                    Ok(Value::Text(r.into()))
1843                }
1844                _ => Ok(Value::Text(value_to_text(&evaluated[0]).into())),
1845            }
1846        }
1847        "NOW" | "CURRENT_TIMESTAMP" | "LOCALTIMESTAMP" => {
1848            check_args(name, &evaluated, 0)?;
1849            Ok(Value::Timestamp(crate::datetime::txn_or_clock_micros()))
1850        }
1851        "CURRENT_DATE" => {
1852            check_args(name, &evaluated, 0)?;
1853            Ok(Value::Date(crate::datetime::ts_to_date_floor(
1854                crate::datetime::txn_or_clock_micros(),
1855            )))
1856        }
1857        "CURRENT_TIME" | "LOCALTIME" => {
1858            check_args(name, &evaluated, 0)?;
1859            Ok(Value::Time(
1860                crate::datetime::ts_split(crate::datetime::txn_or_clock_micros()).1,
1861            ))
1862        }
1863        "CLOCK_TIMESTAMP" | "STATEMENT_TIMESTAMP" | "TRANSACTION_TIMESTAMP" => {
1864            check_args(name, &evaluated, 0)?;
1865            let ts = match name {
1866                "CLOCK_TIMESTAMP" => crate::datetime::now_micros(),
1867                _ => crate::datetime::txn_or_clock_micros(),
1868            };
1869            Ok(Value::Timestamp(ts))
1870        }
1871        "EXTRACT" | "DATE_PART" | "DATEPART" => {
1872            check_args(name, &evaluated, 2)?;
1873            let field: &str = match &evaluated[0] {
1874                Value::Null => return Ok(Value::Null),
1875                Value::Text(s) => s.as_str(),
1876                _ => {
1877                    return Err(SqlError::TypeMismatch {
1878                        expected: "TEXT field name".into(),
1879                        got: evaluated[0].data_type().to_string(),
1880                    })
1881                }
1882            };
1883            if evaluated[1].is_null() {
1884                return Ok(Value::Null);
1885            }
1886            crate::datetime::extract(field, &evaluated[1])
1887        }
1888        "DATE_TRUNC" => {
1889            if evaluated.len() < 2 || evaluated.len() > 3 {
1890                return Err(SqlError::InvalidValue(
1891                    "DATE_TRUNC requires 2 or 3 arguments".into(),
1892                ));
1893            }
1894            let unit = match &evaluated[0] {
1895                Value::Null => return Ok(Value::Null),
1896                Value::Text(s) => s.to_string(),
1897                _ => {
1898                    return Err(SqlError::TypeMismatch {
1899                        expected: "TEXT unit name".into(),
1900                        got: evaluated[0].data_type().to_string(),
1901                    })
1902                }
1903            };
1904            if evaluated[1].is_null() {
1905                return Ok(Value::Null);
1906            }
1907            // Optional tz arg: truncate in that zone, then convert back to UTC.
1908            if evaluated.len() == 3 {
1909                if let Value::Text(tz) = &evaluated[2] {
1910                    if !tz.eq_ignore_ascii_case("UTC") {
1911                        if let Value::Timestamp(ts) = &evaluated[1] {
1912                            return date_trunc_in_zone(&unit, *ts, tz);
1913                        }
1914                    }
1915                }
1916            }
1917            crate::datetime::date_trunc(&unit, &evaluated[1])
1918        }
1919        "DATE_BIN" => {
1920            check_args(name, &evaluated, 3)?;
1921            if evaluated.iter().any(|v| v.is_null()) {
1922                return Ok(Value::Null);
1923            }
1924            let stride = match &evaluated[0] {
1925                Value::Interval {
1926                    months: _,
1927                    days,
1928                    micros,
1929                } => *days as i64 * crate::datetime::MICROS_PER_DAY + *micros,
1930                _ => {
1931                    return Err(SqlError::TypeMismatch {
1932                        expected: "INTERVAL stride".into(),
1933                        got: evaluated[0].data_type().to_string(),
1934                    })
1935                }
1936            };
1937            if stride <= 0 {
1938                return Err(SqlError::InvalidValue(
1939                    "DATE_BIN stride must be positive".into(),
1940                ));
1941            }
1942            let (src, origin) = match (&evaluated[1], &evaluated[2]) {
1943                (Value::Timestamp(s), Value::Timestamp(o)) => (*s, *o),
1944                _ => {
1945                    return Err(SqlError::TypeMismatch {
1946                        expected: "TIMESTAMP, TIMESTAMP".into(),
1947                        got: format!("{}, {}", evaluated[1].data_type(), evaluated[2].data_type()),
1948                    })
1949                }
1950            };
1951            let diff = src - origin;
1952            let binned = origin + (diff.div_euclid(stride)) * stride;
1953            Ok(Value::Timestamp(binned))
1954        }
1955        "AGE" => {
1956            if evaluated.len() == 1 {
1957                if evaluated[0].is_null() {
1958                    return Ok(Value::Null);
1959                }
1960                let ts = match &evaluated[0] {
1961                    Value::Timestamp(t) => *t,
1962                    Value::Date(d) => crate::datetime::date_to_ts(*d),
1963                    _ => {
1964                        return Err(SqlError::TypeMismatch {
1965                            expected: "TIMESTAMP or DATE".into(),
1966                            got: evaluated[0].data_type().to_string(),
1967                        })
1968                    }
1969                };
1970                // Implicit reference: today at midnight UTC.
1971                let today = crate::datetime::today_days();
1972                let midnight = crate::datetime::date_to_ts(today);
1973                let (m, d, u) = crate::datetime::age(midnight, ts)?;
1974                return Ok(Value::Interval {
1975                    months: m,
1976                    days: d,
1977                    micros: u,
1978                });
1979            }
1980            check_args(name, &evaluated, 2)?;
1981            if evaluated.iter().any(|v| v.is_null()) {
1982                return Ok(Value::Null);
1983            }
1984            let a = ts_of(&evaluated[0])?;
1985            let b = ts_of(&evaluated[1])?;
1986            let (m, d, u) = crate::datetime::age(a, b)?;
1987            Ok(Value::Interval {
1988                months: m,
1989                days: d,
1990                micros: u,
1991            })
1992        }
1993        "MAKE_DATE" => {
1994            check_args(name, &evaluated, 3)?;
1995            if evaluated.iter().any(|v| v.is_null()) {
1996                return Ok(Value::Null);
1997            }
1998            let y = int_arg(&evaluated[0], "MAKE_DATE year")? as i32;
1999            let m = int_arg(&evaluated[1], "MAKE_DATE month")? as u8;
2000            let d = int_arg(&evaluated[2], "MAKE_DATE day")? as u8;
2001            crate::datetime::ymd_to_days(y, m, d)
2002                .map(Value::Date)
2003                .ok_or_else(|| SqlError::InvalidDateLiteral(format!("make_date({y}, {m}, {d})")))
2004        }
2005        "MAKE_TIME" => {
2006            check_args(name, &evaluated, 3)?;
2007            if evaluated.iter().any(|v| v.is_null()) {
2008                return Ok(Value::Null);
2009            }
2010            let h = int_arg(&evaluated[0], "MAKE_TIME hour")? as u8;
2011            let mi = int_arg(&evaluated[1], "MAKE_TIME minute")? as u8;
2012            let (s, us) = real_sec_arg(&evaluated[2])?;
2013            crate::datetime::hmsn_to_micros(h, mi, s, us)
2014                .map(Value::Time)
2015                .ok_or_else(|| SqlError::InvalidTimeLiteral(format!("make_time({h}, {mi}, ...)")))
2016        }
2017        "MAKE_TIMESTAMP" => {
2018            check_args(name, &evaluated, 6)?;
2019            if evaluated.iter().any(|v| v.is_null()) {
2020                return Ok(Value::Null);
2021            }
2022            let y = int_arg(&evaluated[0], "MAKE_TIMESTAMP year")? as i32;
2023            let mo = int_arg(&evaluated[1], "MAKE_TIMESTAMP month")? as u8;
2024            let d = int_arg(&evaluated[2], "MAKE_TIMESTAMP day")? as u8;
2025            let h = int_arg(&evaluated[3], "MAKE_TIMESTAMP hour")? as u8;
2026            let mi = int_arg(&evaluated[4], "MAKE_TIMESTAMP min")? as u8;
2027            let (s, us) = real_sec_arg(&evaluated[5])?;
2028            let days = crate::datetime::ymd_to_days(y, mo, d).ok_or_else(|| {
2029                SqlError::InvalidTimestampLiteral(format!("make_timestamp year={y}"))
2030            })?;
2031            let tmicros = crate::datetime::hmsn_to_micros(h, mi, s, us)
2032                .ok_or_else(|| SqlError::InvalidTimestampLiteral("time out of range".into()))?;
2033            Ok(Value::Timestamp(crate::datetime::ts_combine(days, tmicros)))
2034        }
2035        "MAKE_INTERVAL" => {
2036            // Positional args: years, months, weeks, days, hours, mins, secs.
2037            if evaluated.len() > 7 {
2038                return Err(SqlError::InvalidValue(
2039                    "MAKE_INTERVAL accepts at most 7 arguments".into(),
2040                ));
2041            }
2042            let mut months: i64 = 0;
2043            let mut days: i64 = 0;
2044            let mut micros: i64 = 0;
2045            for (i, v) in evaluated.iter().enumerate() {
2046                if v.is_null() {
2047                    continue;
2048                }
2049                let n = match v {
2050                    Value::Integer(n) => *n,
2051                    Value::Real(r) => *r as i64,
2052                    _ => {
2053                        return Err(SqlError::TypeMismatch {
2054                            expected: "numeric".into(),
2055                            got: v.data_type().to_string(),
2056                        })
2057                    }
2058                };
2059                match i {
2060                    0 => months = months.saturating_add(n.saturating_mul(12)),
2061                    1 => months = months.saturating_add(n),
2062                    2 => days = days.saturating_add(n.saturating_mul(7)),
2063                    3 => days = days.saturating_add(n),
2064                    4 => {
2065                        micros = micros
2066                            .saturating_add(n.saturating_mul(crate::datetime::MICROS_PER_HOUR))
2067                    }
2068                    5 => {
2069                        micros =
2070                            micros.saturating_add(n.saturating_mul(crate::datetime::MICROS_PER_MIN))
2071                    }
2072                    6 => {
2073                        // Seconds may be fractional — also check Real.
2074                        if let Value::Real(r) = v {
2075                            micros = micros.saturating_add(
2076                                (*r * crate::datetime::MICROS_PER_SEC as f64) as i64,
2077                            );
2078                        } else {
2079                            micros = micros
2080                                .saturating_add(n.saturating_mul(crate::datetime::MICROS_PER_SEC));
2081                        }
2082                    }
2083                    _ => unreachable!(),
2084                }
2085            }
2086            Ok(Value::Interval {
2087                months: months.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
2088                days: days.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
2089                micros,
2090            })
2091        }
2092        "JUSTIFY_DAYS" => {
2093            check_args(name, &evaluated, 1)?;
2094            match &evaluated[0] {
2095                Value::Null => Ok(Value::Null),
2096                Value::Interval {
2097                    months,
2098                    days,
2099                    micros,
2100                } => {
2101                    let (m, d, u) = crate::datetime::justify_days(*months, *days, *micros);
2102                    Ok(Value::Interval {
2103                        months: m,
2104                        days: d,
2105                        micros: u,
2106                    })
2107                }
2108                other => Err(SqlError::TypeMismatch {
2109                    expected: "INTERVAL".into(),
2110                    got: other.data_type().to_string(),
2111                }),
2112            }
2113        }
2114        "JUSTIFY_HOURS" => {
2115            check_args(name, &evaluated, 1)?;
2116            match &evaluated[0] {
2117                Value::Null => Ok(Value::Null),
2118                Value::Interval {
2119                    months,
2120                    days,
2121                    micros,
2122                } => {
2123                    let (m, d, u) = crate::datetime::justify_hours(*months, *days, *micros);
2124                    Ok(Value::Interval {
2125                        months: m,
2126                        days: d,
2127                        micros: u,
2128                    })
2129                }
2130                other => Err(SqlError::TypeMismatch {
2131                    expected: "INTERVAL".into(),
2132                    got: other.data_type().to_string(),
2133                }),
2134            }
2135        }
2136        "JUSTIFY_INTERVAL" => {
2137            check_args(name, &evaluated, 1)?;
2138            match &evaluated[0] {
2139                Value::Null => Ok(Value::Null),
2140                Value::Interval {
2141                    months,
2142                    days,
2143                    micros,
2144                } => {
2145                    let (m, d, u) = crate::datetime::justify_interval(*months, *days, *micros);
2146                    Ok(Value::Interval {
2147                        months: m,
2148                        days: d,
2149                        micros: u,
2150                    })
2151                }
2152                other => Err(SqlError::TypeMismatch {
2153                    expected: "INTERVAL".into(),
2154                    got: other.data_type().to_string(),
2155                }),
2156            }
2157        }
2158        "ISFINITE" => {
2159            check_args(name, &evaluated, 1)?;
2160            if evaluated[0].is_null() {
2161                return Ok(Value::Null);
2162            }
2163            Ok(Value::Boolean(evaluated[0].is_finite_temporal()))
2164        }
2165        "DATE" => {
2166            if evaluated.is_empty() {
2167                return Err(SqlError::InvalidValue(
2168                    "DATE requires at least 1 argument".into(),
2169                ));
2170            }
2171            if evaluated[0].is_null() {
2172                return Ok(Value::Null);
2173            }
2174            let d = match &evaluated[0] {
2175                Value::Date(d) => *d,
2176                Value::Timestamp(t) => crate::datetime::ts_to_date_floor(*t),
2177                Value::Text(s) if s.eq_ignore_ascii_case("now") => crate::datetime::today_days(),
2178                Value::Text(s) => crate::datetime::parse_date(s)?,
2179                Value::Integer(n) => {
2180                    crate::datetime::ts_to_date_floor(*n * crate::datetime::MICROS_PER_SEC)
2181                }
2182                other => {
2183                    return Err(SqlError::TypeMismatch {
2184                        expected: "TIMESTAMP, DATE, TEXT, or INTEGER".into(),
2185                        got: other.data_type().to_string(),
2186                    })
2187                }
2188            };
2189            Ok(Value::Date(d))
2190        }
2191        "TIME" => {
2192            if evaluated.is_empty() {
2193                return Err(SqlError::InvalidValue(
2194                    "TIME requires at least 1 argument".into(),
2195                ));
2196            }
2197            if evaluated[0].is_null() {
2198                return Ok(Value::Null);
2199            }
2200            let t = match &evaluated[0] {
2201                Value::Time(t) => *t,
2202                Value::Timestamp(t) => crate::datetime::ts_split(*t).1,
2203                Value::Text(s) if s.eq_ignore_ascii_case("now") => {
2204                    crate::datetime::current_time_micros()
2205                }
2206                Value::Text(s) => crate::datetime::parse_time(s)?,
2207                other => {
2208                    return Err(SqlError::TypeMismatch {
2209                        expected: "TIMESTAMP, TIME, or TEXT".into(),
2210                        got: other.data_type().to_string(),
2211                    })
2212                }
2213            };
2214            Ok(Value::Time(t))
2215        }
2216        "DATETIME" => {
2217            if evaluated.is_empty() {
2218                return Err(SqlError::InvalidValue(
2219                    "DATETIME requires at least 1 argument".into(),
2220                ));
2221            }
2222            if evaluated[0].is_null() {
2223                return Ok(Value::Null);
2224            }
2225            let t = match &evaluated[0] {
2226                Value::Timestamp(t) => *t,
2227                Value::Date(d) => crate::datetime::date_to_ts(*d),
2228                Value::Text(s) if s.eq_ignore_ascii_case("now") => crate::datetime::now_micros(),
2229                Value::Text(s) => crate::datetime::parse_timestamp(s)?,
2230                Value::Integer(n) => n * crate::datetime::MICROS_PER_SEC,
2231                other => {
2232                    return Err(SqlError::TypeMismatch {
2233                        expected: "TIMESTAMP, DATE, TEXT, or INTEGER".into(),
2234                        got: other.data_type().to_string(),
2235                    })
2236                }
2237            };
2238            Ok(Value::Timestamp(t))
2239        }
2240        "STRFTIME" => {
2241            if evaluated.len() < 2 {
2242                return Err(SqlError::InvalidValue(
2243                    "STRFTIME requires format + value".into(),
2244                ));
2245            }
2246            if evaluated.iter().take(2).any(|v| v.is_null()) {
2247                return Ok(Value::Null);
2248            }
2249            let fmt = match &evaluated[0] {
2250                Value::Text(s) => s.to_string(),
2251                _ => {
2252                    return Err(SqlError::TypeMismatch {
2253                        expected: "TEXT format".into(),
2254                        got: evaluated[0].data_type().to_string(),
2255                    })
2256                }
2257            };
2258            let out = crate::datetime::strftime(&fmt, &evaluated[1])?;
2259            Ok(Value::Text(out.into()))
2260        }
2261        "JULIANDAY" => {
2262            if evaluated.is_empty() {
2263                return Err(SqlError::InvalidValue(
2264                    "JULIANDAY requires at least 1 argument".into(),
2265                ));
2266            }
2267            if evaluated[0].is_null() {
2268                return Ok(Value::Null);
2269            }
2270            let micros = ts_of(&evaluated[0])?;
2271            let (days, tmicros) = crate::datetime::ts_split(micros);
2272            // Julian Day 2440587.5 = 1970-01-01 00:00:00 UTC (Julian days start at noon).
2273            let julian =
2274                days as f64 + 2_440_587.5 + tmicros as f64 / crate::datetime::MICROS_PER_DAY as f64;
2275            Ok(Value::Real(julian))
2276        }
2277        "UNIXEPOCH" => {
2278            if evaluated.is_empty() {
2279                return Err(SqlError::InvalidValue(
2280                    "UNIXEPOCH requires at least 1 argument".into(),
2281                ));
2282            }
2283            if evaluated[0].is_null() {
2284                return Ok(Value::Null);
2285            }
2286            let micros = ts_of(&evaluated[0])?;
2287            let subsec = evaluated
2288                .get(1)
2289                .and_then(|v| {
2290                    if let Value::Text(s) = v {
2291                        Some(s.to_string())
2292                    } else {
2293                        None
2294                    }
2295                })
2296                .map(|s| s.eq_ignore_ascii_case("subsec") || s.eq_ignore_ascii_case("subsecond"))
2297                .unwrap_or(false);
2298            if subsec {
2299                Ok(Value::Real(
2300                    micros as f64 / crate::datetime::MICROS_PER_SEC as f64,
2301                ))
2302            } else {
2303                Ok(Value::Integer(micros / crate::datetime::MICROS_PER_SEC))
2304            }
2305        }
2306        "TIMEDIFF" => {
2307            check_args(name, &evaluated, 2)?;
2308            if evaluated.iter().any(|v| v.is_null()) {
2309                return Ok(Value::Null);
2310            }
2311            let a = ts_of(&evaluated[0])?;
2312            let b = ts_of(&evaluated[1])?;
2313            let (days, micros) = crate::datetime::subtract_timestamps(a, b);
2314            let sign = if days < 0 || (days == 0 && micros < 0) {
2315                "-"
2316            } else {
2317                "+"
2318            };
2319            let abs_days = days.unsigned_abs() as i64;
2320            let abs_us = micros.unsigned_abs() as i64;
2321            // PG-compat format string: "(+|-)YYYY-MM-DD HH:MM:SS.SSS", days-only.
2322            let (h, m, s, us) = crate::datetime::micros_to_hmsn(abs_us);
2323            Ok(Value::Text(
2324                format!("{sign}{abs_days:04}-00-00 {h:02}:{m:02}:{s:02}.{us:06}").into(),
2325            ))
2326        }
2327        "AT_TIMEZONE" => {
2328            check_args(name, &evaluated, 2)?;
2329            if evaluated.iter().any(|v| v.is_null()) {
2330                return Ok(Value::Null);
2331            }
2332            let ts = match &evaluated[0] {
2333                Value::Timestamp(t) => *t,
2334                Value::Date(d) => crate::datetime::date_to_ts(*d),
2335                other => {
2336                    return Err(SqlError::TypeMismatch {
2337                        expected: "TIMESTAMP or DATE".into(),
2338                        got: other.data_type().to_string(),
2339                    })
2340                }
2341            };
2342            let zone = match &evaluated[1] {
2343                Value::Text(s) => s.to_string(),
2344                _ => {
2345                    return Err(SqlError::TypeMismatch {
2346                        expected: "TEXT time zone".into(),
2347                        got: evaluated[1].data_type().to_string(),
2348                    })
2349                }
2350            };
2351            // Reject POSIX-style 'UTC+5' (ambiguous sign convention).
2352            let upper = zone.to_ascii_uppercase();
2353            if (upper.starts_with("UTC+") || upper.starts_with("UTC-")) && zone.len() > 3 {
2354                return Err(SqlError::InvalidTimezone(format!(
2355                    "'{zone}' is ambiguous — use ISO-8601 offset like '+05:00' or named zone like 'Etc/GMT-5'"
2356                )));
2357            }
2358            let formatted = crate::datetime::format_timestamp_in_zone(ts, &zone)?;
2359            Ok(Value::Text(formatted.into()))
2360        }
2361        "JSONB_TYPEOF" | "JSON_TYPEOF" => {
2362            check_args(name, &evaluated, 1)?;
2363            if evaluated[0].is_null() {
2364                return Ok(Value::Null);
2365            }
2366            crate::json::fn_typeof(&evaluated[0])
2367        }
2368        "JSONB_ARRAY_LENGTH" | "JSON_ARRAY_LENGTH" => {
2369            check_args(name, &evaluated, 1)?;
2370            if evaluated[0].is_null() {
2371                return Ok(Value::Null);
2372            }
2373            crate::json::fn_array_length(&evaluated[0])
2374        }
2375        "JSONB_OBJECT_LENGTH" | "JSON_OBJECT_LENGTH" => {
2376            check_args(name, &evaluated, 1)?;
2377            if evaluated[0].is_null() {
2378                return Ok(Value::Null);
2379            }
2380            crate::json::fn_object_length(&evaluated[0])
2381        }
2382        "JSONB_EXTRACT_PATH" | "JSON_EXTRACT_PATH" => {
2383            if evaluated.is_empty() {
2384                return Err(SqlError::InvalidValue(format!(
2385                    "{name} requires at least 1 argument"
2386                )));
2387            }
2388            if evaluated[0].is_null() {
2389                return Ok(Value::Null);
2390            }
2391            let target = if name.eq_ignore_ascii_case("JSONB_EXTRACT_PATH") {
2392                crate::types::DataType::Jsonb
2393            } else {
2394                crate::types::DataType::Json
2395            };
2396            crate::json::fn_extract_path(&evaluated, target, false)
2397        }
2398        "JSONB_EXTRACT_PATH_TEXT" | "JSON_EXTRACT_PATH_TEXT" => {
2399            if evaluated.is_empty() {
2400                return Err(SqlError::InvalidValue(format!(
2401                    "{name} requires at least 1 argument"
2402                )));
2403            }
2404            if evaluated[0].is_null() {
2405                return Ok(Value::Null);
2406            }
2407            crate::json::fn_extract_path(&evaluated, crate::types::DataType::Text, true)
2408        }
2409        "JSON_EXTRACT" => {
2410            check_args(name, &evaluated, 2)?;
2411            if evaluated[0].is_null() || evaluated[1].is_null() {
2412                return Ok(Value::Null);
2413            }
2414            crate::json::fn_sqlite_extract(&evaluated[0], &evaluated[1])
2415        }
2416        "JSON_VALID" => {
2417            check_args(name, &evaluated, 1)?;
2418            if evaluated[0].is_null() {
2419                return Ok(Value::Null);
2420            }
2421            crate::json::fn_valid(&evaluated[0])
2422        }
2423        "JSONB_STRIP_NULLS" | "JSON_STRIP_NULLS" => {
2424            check_args(name, &evaluated, 1)?;
2425            if evaluated[0].is_null() {
2426                return Ok(Value::Null);
2427            }
2428            let target = if name.eq_ignore_ascii_case("JSONB_STRIP_NULLS") {
2429                crate::types::DataType::Jsonb
2430            } else {
2431                crate::types::DataType::Json
2432            };
2433            crate::json::fn_strip_nulls(&evaluated[0], target)
2434        }
2435        "JSONB_PRETTY" | "JSON_PRETTY" => {
2436            check_args(name, &evaluated, 1)?;
2437            if evaluated[0].is_null() {
2438                return Ok(Value::Null);
2439            }
2440            crate::json::fn_pretty(&evaluated[0])
2441        }
2442        "JSONB_BUILD_OBJECT" | "JSON_BUILD_OBJECT" => {
2443            let target = if name.eq_ignore_ascii_case("JSONB_BUILD_OBJECT") {
2444                crate::types::DataType::Jsonb
2445            } else {
2446                crate::types::DataType::Json
2447            };
2448            crate::json::fn_build_object(&evaluated, target)
2449        }
2450        "JSONB_BUILD_ARRAY" | "JSON_BUILD_ARRAY" => {
2451            let target = if name.eq_ignore_ascii_case("JSONB_BUILD_ARRAY") {
2452                crate::types::DataType::Jsonb
2453            } else {
2454                crate::types::DataType::Json
2455            };
2456            crate::json::fn_build_array(&evaluated, target)
2457        }
2458        "JSONB_SET" | "JSON_SET" => {
2459            if !(3..=4).contains(&evaluated.len()) {
2460                return Err(SqlError::InvalidValue(format!(
2461                    "{name} requires 3 or 4 arguments"
2462                )));
2463            }
2464            if evaluated[0].is_null() {
2465                return Ok(Value::Null);
2466            }
2467            let target = if name.eq_ignore_ascii_case("JSONB_SET") {
2468                crate::types::DataType::Jsonb
2469            } else {
2470                crate::types::DataType::Json
2471            };
2472            let create_missing = evaluated
2473                .get(3)
2474                .map(|v| matches!(v, Value::Boolean(true)))
2475                .unwrap_or(true);
2476            crate::json::fn_set(
2477                &evaluated[0],
2478                &evaluated[1],
2479                &evaluated[2],
2480                create_missing,
2481                target,
2482            )
2483        }
2484        "JSONB_INSERT" | "JSON_INSERT" => {
2485            if !(3..=4).contains(&evaluated.len()) {
2486                return Err(SqlError::InvalidValue(format!(
2487                    "{name} requires 3 or 4 arguments"
2488                )));
2489            }
2490            if evaluated[0].is_null() {
2491                return Ok(Value::Null);
2492            }
2493            let target = if name.eq_ignore_ascii_case("JSONB_INSERT") {
2494                crate::types::DataType::Jsonb
2495            } else {
2496                crate::types::DataType::Json
2497            };
2498            let insert_after = evaluated
2499                .get(3)
2500                .map(|v| matches!(v, Value::Boolean(true)))
2501                .unwrap_or(false);
2502            crate::json::fn_insert(
2503                &evaluated[0],
2504                &evaluated[1],
2505                &evaluated[2],
2506                insert_after,
2507                target,
2508            )
2509        }
2510        "TO_JSONB" | "TO_JSON" => {
2511            check_args(name, &evaluated, 1)?;
2512            let target = if name.eq_ignore_ascii_case("TO_JSONB") {
2513                crate::types::DataType::Jsonb
2514            } else {
2515                crate::types::DataType::Json
2516            };
2517            crate::json::fn_to_json(&evaluated[0], target)
2518        }
2519        "ROW_TO_JSON" | "ROW_TO_JSONB" => {
2520            check_args(name, &evaluated, 1)?;
2521            let target = if name.eq_ignore_ascii_case("ROW_TO_JSONB") {
2522                crate::types::DataType::Jsonb
2523            } else {
2524                crate::types::DataType::Json
2525            };
2526            crate::json::fn_to_json(&evaluated[0], target)
2527        }
2528        "JSON_OBJECT" => crate::json::fn_json_object(&evaluated),
2529        "JSON_EXISTS" => {
2530            check_args(name, &evaluated, 2)?;
2531            if evaluated[0].is_null() || evaluated[1].is_null() {
2532                return Ok(Value::Null);
2533            }
2534            crate::json::fn_json_exists(&evaluated[0], &evaluated[1])
2535        }
2536        "JSON_VALUE" => {
2537            check_args(name, &evaluated, 2)?;
2538            if evaluated[0].is_null() || evaluated[1].is_null() {
2539                return Ok(Value::Null);
2540            }
2541            crate::json::fn_json_value(&evaluated[0], &evaluated[1])
2542        }
2543        "JSON_QUERY" => {
2544            check_args(name, &evaluated, 2)?;
2545            if evaluated[0].is_null() || evaluated[1].is_null() {
2546                return Ok(Value::Null);
2547            }
2548            crate::json::fn_json_query(&evaluated[0], &evaluated[1], crate::types::DataType::Jsonb)
2549        }
2550        "JSONB_PATH_EXISTS" => {
2551            if evaluated[0].is_null() || evaluated[1].is_null() {
2552                return Ok(Value::Null);
2553            }
2554            crate::json::fn_jsonb_path_exists(&evaluated)
2555        }
2556        "JSONB_PATH_MATCH" => {
2557            if evaluated[0].is_null() || evaluated[1].is_null() {
2558                return Ok(Value::Null);
2559            }
2560            crate::json::fn_jsonb_path_match(&evaluated)
2561        }
2562        "JSONB_PATH_QUERY_FIRST" => {
2563            if evaluated[0].is_null() || evaluated[1].is_null() {
2564                return Ok(Value::Null);
2565            }
2566            crate::json::fn_jsonb_path_query_first(&evaluated)
2567        }
2568        "JSONB_PATH_QUERY_ARRAY" => {
2569            if evaluated[0].is_null() || evaluated[1].is_null() {
2570                return Ok(Value::Null);
2571            }
2572            crate::json::fn_jsonb_path_query_array(&evaluated)
2573        }
2574        "JSONB_PATH_EXISTS_TZ" => {
2575            if evaluated[0].is_null() || evaluated[1].is_null() {
2576                return Ok(Value::Null);
2577            }
2578            crate::json::fn_jsonb_path_exists_tz(&evaluated)
2579        }
2580        "JSONB_PATH_MATCH_TZ" => {
2581            if evaluated[0].is_null() || evaluated[1].is_null() {
2582                return Ok(Value::Null);
2583            }
2584            crate::json::fn_jsonb_path_match_tz(&evaluated)
2585        }
2586        "JSONB_PATH_QUERY_TZ" => {
2587            if evaluated[0].is_null() || evaluated[1].is_null() {
2588                return Ok(Value::Null);
2589            }
2590            crate::json::fn_jsonb_path_query_tz(&evaluated)
2591        }
2592        "JSONB_PATH_QUERY_FIRST_TZ" => {
2593            if evaluated[0].is_null() || evaluated[1].is_null() {
2594                return Ok(Value::Null);
2595            }
2596            crate::json::fn_jsonb_path_query_first_tz(&evaluated)
2597        }
2598        "JSONB_PATH_QUERY_ARRAY_TZ" => {
2599            if evaluated[0].is_null() || evaluated[1].is_null() {
2600                return Ok(Value::Null);
2601            }
2602            crate::json::fn_jsonb_path_query_array_tz(&evaluated)
2603        }
2604        "JSONB_HAS_KEY" | "JSON_HAS_KEY" => {
2605            check_args(name, &evaluated, 2)?;
2606            if evaluated[0].is_null() || evaluated[1].is_null() {
2607                return Ok(Value::Null);
2608            }
2609            crate::json::op_has_key(&evaluated[0], &evaluated[1])
2610        }
2611        "JSONB_HAS_ANY_KEY" | "JSON_HAS_ANY_KEY" => {
2612            check_args(name, &evaluated, 2)?;
2613            if evaluated[0].is_null() || evaluated[1].is_null() {
2614                return Ok(Value::Null);
2615            }
2616            crate::json::op_has_any_key(&evaluated[0], &evaluated[1])
2617        }
2618        "JSONB_HAS_ALL_KEYS" | "JSON_HAS_ALL_KEYS" => {
2619            check_args(name, &evaluated, 2)?;
2620            if evaluated[0].is_null() || evaluated[1].is_null() {
2621                return Ok(Value::Null);
2622            }
2623            crate::json::op_has_all_keys(&evaluated[0], &evaluated[1])
2624        }
2625        "TO_TSVECTOR" => fts_to_tsvector(&evaluated),
2626        "TO_TSQUERY" => fts_to_tsquery(&evaluated),
2627        "PLAINTO_TSQUERY" => fts_plainto_tsquery(&evaluated),
2628        "PHRASETO_TSQUERY" => fts_phraseto_tsquery(&evaluated),
2629        "WEBSEARCH_TO_TSQUERY" => fts_websearch_to_tsquery(&evaluated),
2630        "TS_RANK" => fts_ts_rank(&evaluated, false),
2631        "TS_RANK_CD" => fts_ts_rank(&evaluated, true),
2632        "TS_HEADLINE" => fts_ts_headline(&evaluated),
2633        "TS_LEXIZE" => fts_ts_lexize(&evaluated),
2634        "NUMNODE" => fts_numnode(&evaluated),
2635        "SETWEIGHT" => fts_setweight(&evaluated),
2636        "STRIP" => fts_strip(&evaluated),
2637        _ => Err(SqlError::Unsupported(format!("scalar function: {name}"))),
2638    }
2639}
2640
2641fn fts_resolve_config_and_text(
2642    args: &[Value],
2643    fname: &str,
2644) -> Result<(crate::fts::TokenizerKind, String)> {
2645    if args.is_empty() || args.len() > 2 {
2646        return Err(SqlError::InvalidValue(format!(
2647            "{fname} requires 1 or 2 arguments"
2648        )));
2649    }
2650    let (config_name, text) = if args.len() == 2 {
2651        let cfg = match &args[0] {
2652            Value::Text(s) => Some(s.as_str().to_string()),
2653            v => {
2654                return Err(SqlError::TypeMismatch {
2655                    expected: "TEXT (config)".into(),
2656                    got: v.data_type().to_string(),
2657                })
2658            }
2659        };
2660        let txt = match &args[1] {
2661            Value::Text(s) => s.as_str().to_string(),
2662            v => {
2663                return Err(SqlError::TypeMismatch {
2664                    expected: "TEXT".into(),
2665                    got: v.data_type().to_string(),
2666                })
2667            }
2668        };
2669        (cfg, txt)
2670    } else {
2671        let txt = match &args[0] {
2672            Value::Text(s) => s.as_str().to_string(),
2673            v => {
2674                return Err(SqlError::TypeMismatch {
2675                    expected: "TEXT".into(),
2676                    got: v.data_type().to_string(),
2677                })
2678            }
2679        };
2680        (None, txt)
2681    };
2682    let kind = match config_name {
2683        Some(name) => crate::fts::TokenizerKind::from_name(&name)?,
2684        None => crate::fts::TokenizerKind::English,
2685    };
2686    Ok((kind, text))
2687}
2688
2689fn fts_to_tsvector(args: &[Value]) -> Result<Value> {
2690    if args.iter().any(|v| v.is_null()) {
2691        return Ok(Value::Null);
2692    }
2693    let (kind, text) = fts_resolve_config_and_text(args, "to_tsvector")?;
2694    crate::fts::fn_to_tsvector_with(kind, &text)
2695}
2696
2697fn fts_to_tsquery(args: &[Value]) -> Result<Value> {
2698    if args.iter().any(|v| v.is_null()) {
2699        return Ok(Value::Null);
2700    }
2701    let (kind, text) = fts_resolve_config_and_text(args, "to_tsquery")?;
2702    crate::fts::fn_to_tsquery_with(kind, &text)
2703}
2704
2705fn fts_plainto_tsquery(args: &[Value]) -> Result<Value> {
2706    if args.iter().any(|v| v.is_null()) {
2707        return Ok(Value::Null);
2708    }
2709    let (kind, text) = fts_resolve_config_and_text(args, "plainto_tsquery")?;
2710    crate::fts::fn_plainto_tsquery_with(kind, &text)
2711}
2712
2713fn fts_phraseto_tsquery(args: &[Value]) -> Result<Value> {
2714    if args.iter().any(|v| v.is_null()) {
2715        return Ok(Value::Null);
2716    }
2717    let (kind, text) = fts_resolve_config_and_text(args, "phraseto_tsquery")?;
2718    crate::fts::fn_phraseto_tsquery_with(kind, &text)
2719}
2720
2721fn fts_websearch_to_tsquery(args: &[Value]) -> Result<Value> {
2722    if args.iter().any(|v| v.is_null()) {
2723        return Ok(Value::Null);
2724    }
2725    let (kind, text) = fts_resolve_config_and_text(args, "websearch_to_tsquery")?;
2726    crate::fts::fn_websearch_to_tsquery_with(kind, &text)
2727}
2728
2729fn fts_ts_rank(args: &[Value], cover_density: bool) -> Result<Value> {
2730    let fname = if cover_density {
2731        "ts_rank_cd"
2732    } else {
2733        "ts_rank"
2734    };
2735    if args.len() != 2 && args.len() != 3 {
2736        return Err(SqlError::InvalidValue(format!(
2737            "{fname} requires 2 or 3 arguments"
2738        )));
2739    }
2740    if args[0].is_null() || args[1].is_null() {
2741        return Ok(Value::Null);
2742    }
2743    let tsv = match &args[0] {
2744        Value::TsVector(b) => b,
2745        v => {
2746            return Err(SqlError::TypeMismatch {
2747                expected: "TSVECTOR".into(),
2748                got: v.data_type().to_string(),
2749            })
2750        }
2751    };
2752    let tsq = match &args[1] {
2753        Value::TsQuery(b) => b,
2754        v => {
2755            return Err(SqlError::TypeMismatch {
2756                expected: "TSQUERY".into(),
2757                got: v.data_type().to_string(),
2758            })
2759        }
2760    };
2761    let norm = if args.len() == 3 {
2762        match &args[2] {
2763            Value::Integer(n) => *n,
2764            Value::Null => return Ok(Value::Null),
2765            v => {
2766                return Err(SqlError::TypeMismatch {
2767                    expected: "INTEGER (norm)".into(),
2768                    got: v.data_type().to_string(),
2769                })
2770            }
2771        }
2772    } else {
2773        0
2774    };
2775    if cover_density {
2776        crate::fts::fn_ts_rank_cd(tsv, tsq, norm)
2777    } else {
2778        crate::fts::fn_ts_rank(tsv, tsq, norm)
2779    }
2780}
2781
2782fn fts_ts_headline(args: &[Value]) -> Result<Value> {
2783    if args.len() < 2 || args.len() > 4 {
2784        return Err(SqlError::InvalidValue(
2785            "ts_headline requires 2 to 4 arguments".into(),
2786        ));
2787    }
2788    if args.iter().any(|v| v.is_null()) {
2789        return Ok(Value::Null);
2790    }
2791    let kind = if args.len() >= 3 {
2792        match &args[0] {
2793            Value::Text(s) => crate::fts::TokenizerKind::from_name(s.as_str())?,
2794            v => {
2795                return Err(SqlError::TypeMismatch {
2796                    expected: "TEXT (config)".into(),
2797                    got: v.data_type().to_string(),
2798                })
2799            }
2800        }
2801    } else {
2802        crate::fts::TokenizerKind::English
2803    };
2804    let text_idx = if args.len() >= 3 { 1 } else { 0 };
2805    let tsq_idx = if args.len() >= 3 { 2 } else { 1 };
2806    let text = match args.get(text_idx) {
2807        Some(Value::Text(s)) => s.as_str(),
2808        _ => {
2809            return Err(SqlError::TypeMismatch {
2810                expected: "TEXT".into(),
2811                got: "non-text".into(),
2812            })
2813        }
2814    };
2815    let tsq = match args.get(tsq_idx) {
2816        Some(Value::TsQuery(b)) => b.as_ref(),
2817        _ => {
2818            return Err(SqlError::TypeMismatch {
2819                expected: "TSQUERY".into(),
2820                got: "non-tsquery".into(),
2821            })
2822        }
2823    };
2824    crate::fts::fn_ts_headline_with(kind, text, tsq)
2825}
2826
2827fn fts_ts_lexize(args: &[Value]) -> Result<Value> {
2828    if args.len() != 2 {
2829        return Err(SqlError::InvalidValue(
2830            "ts_lexize requires 2 arguments (config, word)".into(),
2831        ));
2832    }
2833    if args.iter().any(|v| v.is_null()) {
2834        return Ok(Value::Null);
2835    }
2836    let kind = match &args[0] {
2837        Value::Text(s) => crate::fts::TokenizerKind::from_name(s.as_str())?,
2838        v => {
2839            return Err(SqlError::TypeMismatch {
2840                expected: "TEXT (config)".into(),
2841                got: v.data_type().to_string(),
2842            })
2843        }
2844    };
2845    let word = match &args[1] {
2846        Value::Text(s) => s.as_str(),
2847        v => {
2848            return Err(SqlError::TypeMismatch {
2849                expected: "TEXT (word)".into(),
2850                got: v.data_type().to_string(),
2851            })
2852        }
2853    };
2854    crate::fts::fn_ts_lexize_with(kind, word)
2855}
2856
2857fn fts_numnode(args: &[Value]) -> Result<Value> {
2858    check_args("numnode", args, 1)?;
2859    if args[0].is_null() {
2860        return Ok(Value::Null);
2861    }
2862    let tsq = match &args[0] {
2863        Value::TsQuery(b) => b,
2864        v => {
2865            return Err(SqlError::TypeMismatch {
2866                expected: "TSQUERY".into(),
2867                got: v.data_type().to_string(),
2868            })
2869        }
2870    };
2871    crate::fts::fn_numnode(tsq)
2872}
2873
2874fn fts_setweight(args: &[Value]) -> Result<Value> {
2875    if args.len() == 3 {
2876        return fts_setweight_selective(args);
2877    }
2878    check_args("setweight", args, 2)?;
2879    if args[0].is_null() || args[1].is_null() {
2880        return Ok(Value::Null);
2881    }
2882    let tsv = match &args[0] {
2883        Value::TsVector(b) => b,
2884        v => {
2885            return Err(SqlError::TypeMismatch {
2886                expected: "TSVECTOR".into(),
2887                got: v.data_type().to_string(),
2888            })
2889        }
2890    };
2891    let weight_text = match &args[1] {
2892        Value::Text(s) => s.as_str(),
2893        v => {
2894            return Err(SqlError::TypeMismatch {
2895                expected: "TEXT".into(),
2896                got: v.data_type().to_string(),
2897            })
2898        }
2899    };
2900    let weight = crate::fts::parse_weight_char(weight_text)?;
2901    crate::fts::fn_setweight(tsv, weight)
2902}
2903
2904fn fts_setweight_selective(args: &[Value]) -> Result<Value> {
2905    check_args("setweight", args, 3)?;
2906    if args[0].is_null() || args[1].is_null() || args[2].is_null() {
2907        return Ok(Value::Null);
2908    }
2909    let tsv = match &args[0] {
2910        Value::TsVector(b) => b,
2911        v => {
2912            return Err(SqlError::TypeMismatch {
2913                expected: "TSVECTOR".into(),
2914                got: v.data_type().to_string(),
2915            })
2916        }
2917    };
2918    let weight_text = match &args[1] {
2919        Value::Text(s) => s.as_str(),
2920        v => {
2921            return Err(SqlError::TypeMismatch {
2922                expected: "TEXT".into(),
2923                got: v.data_type().to_string(),
2924            })
2925        }
2926    };
2927    let weight = crate::fts::parse_weight_char(weight_text)?;
2928    let filter = match &args[2] {
2929        Value::Array(a) => a.as_ref().as_slice(),
2930        v => {
2931            return Err(SqlError::TypeMismatch {
2932                expected: "TEXT[]".into(),
2933                got: v.data_type().to_string(),
2934            })
2935        }
2936    };
2937    crate::fts::fn_setweight_selective(tsv, weight, filter)
2938}
2939
2940fn fts_strip(args: &[Value]) -> Result<Value> {
2941    check_args("strip", args, 1)?;
2942    if args[0].is_null() {
2943        return Ok(Value::Null);
2944    }
2945    let tsv = match &args[0] {
2946        Value::TsVector(b) => b,
2947        v => {
2948            return Err(SqlError::TypeMismatch {
2949                expected: "TSVECTOR".into(),
2950                got: v.data_type().to_string(),
2951            })
2952        }
2953    };
2954    crate::fts::fn_strip(tsv)
2955}
2956
2957/// Extract a timestamp (µs UTC) from a Value, coercing DATE → midnight.
2958fn ts_of(v: &Value) -> Result<i64> {
2959    match v {
2960        Value::Timestamp(t) => Ok(*t),
2961        Value::Date(d) => Ok(crate::datetime::date_to_ts(*d)),
2962        _ => Err(SqlError::TypeMismatch {
2963            expected: "TIMESTAMP or DATE".into(),
2964            got: v.data_type().to_string(),
2965        }),
2966    }
2967}
2968
2969fn int_arg(v: &Value, label: &str) -> Result<i64> {
2970    match v {
2971        Value::Integer(n) => Ok(*n),
2972        _ => Err(SqlError::TypeMismatch {
2973            expected: format!("INTEGER ({label})"),
2974            got: v.data_type().to_string(),
2975        }),
2976    }
2977}
2978
2979/// Extract (whole_seconds: u8, frac_micros: u32) from a numeric argument for MAKE_TIME-style calls.
2980fn real_sec_arg(v: &Value) -> Result<(u8, u32)> {
2981    match v {
2982        Value::Integer(n) => {
2983            if !(0..=60).contains(n) {
2984                return Err(SqlError::InvalidValue(format!("second out of range: {n}")));
2985            }
2986            Ok((*n as u8, 0))
2987        }
2988        Value::Real(r) => {
2989            let whole = r.trunc() as i64;
2990            if !(0..=60).contains(&whole) {
2991                return Err(SqlError::InvalidValue(format!("second out of range: {r}")));
2992            }
2993            let frac = ((r - whole as f64) * 1_000_000.0).round() as i64;
2994            Ok((whole as u8, frac.max(0) as u32))
2995        }
2996        _ => Err(SqlError::TypeMismatch {
2997            expected: "numeric seconds".into(),
2998            got: v.data_type().to_string(),
2999        }),
3000    }
3001}
3002
3003/// DATE_TRUNC with a non-UTC IANA zone: convert → truncate in that zone → convert back to UTC.
3004fn date_trunc_in_zone(unit: &str, ts_utc: i64, tz: &str) -> Result<Value> {
3005    use jiff::{tz::TimeZone, Timestamp as JTimestamp};
3006    let zone = TimeZone::get(tz).map_err(|e| SqlError::InvalidTimezone(format!("{tz}: {e}")))?;
3007    let ts = JTimestamp::from_microsecond(ts_utc)
3008        .map_err(|e| SqlError::InvalidValue(format!("ts: {e}")))?;
3009    let zoned = ts.to_zoned(zone.clone());
3010    let unit_lower = unit.to_ascii_lowercase();
3011    let rounded = match unit_lower.as_str() {
3012        "microseconds" => return Ok(Value::Timestamp(ts_utc)),
3013        "second" => zoned
3014            .start_of_day()
3015            .map_err(|e| SqlError::InvalidValue(format!("{e}")))?,
3016        _ => {
3017            let naive_ts = zoned.timestamp().as_microsecond();
3018            return crate::datetime::date_trunc(unit, &Value::Timestamp(naive_ts));
3019        }
3020    };
3021    Ok(Value::Timestamp(rounded.timestamp().as_microsecond()))
3022}
3023
3024fn check_args(name: &str, args: &[Value], expected: usize) -> Result<()> {
3025    if args.len() != expected {
3026        Err(SqlError::InvalidValue(format!(
3027            "{name} requires {expected} argument(s), got {}",
3028            args.len()
3029        )))
3030    } else {
3031        Ok(())
3032    }
3033}
3034
3035pub fn referenced_columns(expr: &Expr, columns: &[ColumnDef]) -> Vec<usize> {
3036    let mut indices = Vec::new();
3037    collect_column_refs(expr, columns, &mut indices);
3038    indices.sort_unstable();
3039    indices.dedup();
3040    indices
3041}
3042
3043fn collect_column_refs(expr: &Expr, columns: &[ColumnDef], out: &mut Vec<usize>) {
3044    match expr {
3045        Expr::Column(name) => {
3046            for (i, c) in columns.iter().enumerate() {
3047                if c.name == *name
3048                    || (c.name.len() > name.len()
3049                        && c.name.as_bytes()[c.name.len() - name.len() - 1] == b'.'
3050                        && c.name.ends_with(name.as_str()))
3051                {
3052                    out.push(i);
3053                    break;
3054                }
3055            }
3056        }
3057        Expr::QualifiedColumn { table, column } => {
3058            let mut found: Option<usize> = None;
3059            let mut bare_match: Option<usize> = None;
3060            let mut bare_count = 0usize;
3061            for (i, c) in columns.iter().enumerate() {
3062                if c.name.len() == table.len() + 1 + column.len()
3063                    && c.name.as_bytes()[table.len()] == b'.'
3064                    && c.name.starts_with(table.as_str())
3065                    && c.name.ends_with(column.as_str())
3066                {
3067                    found = Some(i);
3068                    break;
3069                }
3070                if c.name == *column {
3071                    bare_match = Some(i);
3072                    bare_count += 1;
3073                }
3074            }
3075            if let Some(idx) = found {
3076                out.push(idx);
3077            } else if bare_count == 1 {
3078                out.push(bare_match.unwrap());
3079            }
3080        }
3081        Expr::BinaryOp { left, right, .. } => {
3082            collect_column_refs(left, columns, out);
3083            collect_column_refs(right, columns, out);
3084        }
3085        Expr::UnaryOp { expr, .. } => {
3086            collect_column_refs(expr, columns, out);
3087        }
3088        Expr::IsNull(e) | Expr::IsNotNull(e) => {
3089            collect_column_refs(e, columns, out);
3090        }
3091        Expr::Function { args, .. } => {
3092            for arg in args {
3093                collect_column_refs(arg, columns, out);
3094            }
3095        }
3096        Expr::InSubquery { expr, .. } => {
3097            collect_column_refs(expr, columns, out);
3098        }
3099        Expr::InList { expr, list, .. } => {
3100            collect_column_refs(expr, columns, out);
3101            for item in list {
3102                collect_column_refs(item, columns, out);
3103            }
3104        }
3105        Expr::InSet { expr, .. } => {
3106            collect_column_refs(expr, columns, out);
3107        }
3108        Expr::Between {
3109            expr, low, high, ..
3110        } => {
3111            collect_column_refs(expr, columns, out);
3112            collect_column_refs(low, columns, out);
3113            collect_column_refs(high, columns, out);
3114        }
3115        Expr::Like {
3116            expr,
3117            pattern,
3118            escape,
3119            ..
3120        } => {
3121            collect_column_refs(expr, columns, out);
3122            collect_column_refs(pattern, columns, out);
3123            if let Some(esc) = escape {
3124                collect_column_refs(esc, columns, out);
3125            }
3126        }
3127        Expr::Case {
3128            operand,
3129            conditions,
3130            else_result,
3131        } => {
3132            if let Some(op) = operand {
3133                collect_column_refs(op, columns, out);
3134            }
3135            for (when, then) in conditions {
3136                collect_column_refs(when, columns, out);
3137                collect_column_refs(then, columns, out);
3138            }
3139            if let Some(e) = else_result {
3140                collect_column_refs(e, columns, out);
3141            }
3142        }
3143        Expr::Coalesce(args) => {
3144            for arg in args {
3145                collect_column_refs(arg, columns, out);
3146            }
3147        }
3148        Expr::Cast { expr, .. } => {
3149            collect_column_refs(expr, columns, out);
3150        }
3151        Expr::Collate { expr, .. } => {
3152            collect_column_refs(expr, columns, out);
3153        }
3154        Expr::WindowFunction { args, spec, .. } => {
3155            for arg in args {
3156                collect_column_refs(arg, columns, out);
3157            }
3158            for pb in &spec.partition_by {
3159                collect_column_refs(pb, columns, out);
3160            }
3161            for ob in &spec.order_by {
3162                collect_column_refs(&ob.expr, columns, out);
3163            }
3164        }
3165        Expr::ArrayLiteral(elems) => {
3166            for e in elems {
3167                collect_column_refs(e, columns, out);
3168            }
3169        }
3170        Expr::Quantified { left, right, .. } => {
3171            collect_column_refs(left, columns, out);
3172            if let crate::parser::QuantifiedRhs::Array(e) = right {
3173                collect_column_refs(e, columns, out);
3174            }
3175        }
3176        Expr::Literal(_)
3177        | Expr::Parameter(_)
3178        | Expr::CountStar
3179        | Expr::Exists { .. }
3180        | Expr::ScalarSubquery(_)
3181        | Expr::TypedNullRecord(_) => {}
3182    }
3183}
3184
3185pub fn is_truthy(val: &Value) -> bool {
3186    match val {
3187        Value::Boolean(b) => *b,
3188        Value::Integer(i) => *i != 0,
3189        Value::Null => false,
3190        _ => true,
3191    }
3192}
3193
3194#[cfg(test)]
3195#[path = "eval_tests.rs"]
3196mod tests;