Skip to main content

reddb_rql/
expr_typing.rs

1//! Fase 3 expression typer.
2//!
3//! Walks an `ast::Expr` tree and assigns a concrete `DataType` to
4//! every node, using:
5//!
6//! - Column scope (table → column → DataType) supplied by the
7//!   caller as a closure so we don't depend on the schema registry
8//!   directly — keeps this module trivially testable.
9//! - The cast catalog (`schema::cast_catalog`) for implicit
10//!   coercion paths.
11//! - The type-category preferred-member rule for tie breaks (PG
12//!   `func_select_candidate` heuristic, simplified).
13//!
14//! Output is a `TypedExpr` mirroring the shape of `Expr` but with a
15//! `ty: DataType` slot on every node. The runtime evaluator can use
16//! that to skip the Value→Number tagging dance the current
17//! `expr_eval` does at every step.
18//!
19//! Scope today (Fase 3 starter): handles literals, columns, unary,
20//! binary arith / comparison / logical, cast, IsNull, Between,
21//! InList, Case, and built-in FunctionCall nodes resolved through the
22//! static function catalog. The remaining gap is advanced polymorphic
23//! signatures beyond lightweight cases such as `COALESCE`.
24//! Subqueries / parameters / advanced polymorphism are out of scope.
25//!
26//! This module is **not yet wired** into the parser → planner flow.
27//! It exists so Fase 3 Week 4+ can plug it in once the parser v2
28//! emits Expr trees as the canonical projection / filter
29//! representation.
30
31use crate::ast::{BinOp, Expr, FieldRef, UnaryOp};
32use reddb_types::cast_catalog::{can_implicit_cast, CastContext};
33use reddb_types::types::{DataType, TypeCategory, Value};
34
35/// Errors reported by the expression typer. All variants are
36/// recoverable diagnostics — the analyzer can collect several and
37/// emit them together rather than fail-fast like the parser.
38#[derive(Debug, Clone)]
39pub enum TypeError {
40    /// Column reference doesn't resolve in the active scope.
41    UnknownColumn { table: String, column: String },
42    /// Operator doesn't accept the given operand types after
43    /// implicit coercion.
44    OperatorMismatch {
45        op: BinOp,
46        lhs: DataType,
47        rhs: DataType,
48    },
49    /// Unary operator doesn't accept the operand type.
50    UnaryMismatch { op: UnaryOp, operand: DataType },
51    /// Explicit CAST target not reachable from source even via
52    /// the Explicit context.
53    InvalidCast { src: DataType, target: DataType },
54    /// CASE branches yield different unifiable types.
55    CaseBranchMismatch { first: DataType, other: DataType },
56    /// IN list elements don't unify with the target.
57    InListMismatch { target: DataType, element: DataType },
58}
59
60impl std::fmt::Display for TypeError {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        match self {
63            Self::UnknownColumn { table, column } => {
64                if table.is_empty() {
65                    write!(f, "unknown column `{column}`")
66                } else {
67                    write!(f, "unknown column `{table}.{column}`")
68                }
69            }
70            Self::OperatorMismatch { op, lhs, rhs } => {
71                write!(
72                    f,
73                    "operator `{op:?}` cannot apply to `{lhs:?}` and `{rhs:?}`"
74                )
75            }
76            Self::UnaryMismatch { op, operand } => {
77                write!(f, "unary `{op:?}` cannot apply to `{operand:?}`")
78            }
79            Self::InvalidCast { src, target } => {
80                write!(f, "no cast from `{src:?}` to `{target:?}`")
81            }
82            Self::CaseBranchMismatch { first, other } => {
83                write!(
84                    f,
85                    "CASE branches disagree on type: `{first:?}` vs `{other:?}`"
86                )
87            }
88            Self::InListMismatch { target, element } => {
89                write!(
90                    f,
91                    "IN list element `{element:?}` is incompatible with target `{target:?}`"
92                )
93            }
94        }
95    }
96}
97
98impl std::error::Error for TypeError {}
99
100/// Resolved type for an expression node. Mirrors `Expr` shape with
101/// an added `ty` slot. Span is preserved so analyzer diagnostics can
102/// still point at the original token range.
103///
104/// Stored as `Box<…>` for the recursive variants because the typer
105/// runs once per query and the trees are bounded — no need for the
106/// arena tricks the runtime evaluator uses.
107#[derive(Debug, Clone)]
108pub struct TypedExpr {
109    pub kind: TypedExprKind,
110    pub ty: DataType,
111}
112
113#[derive(Debug, Clone)]
114pub enum TypedExprKind {
115    Literal(Value),
116    Column(FieldRef),
117    UnaryOp {
118        op: UnaryOp,
119        operand: Box<TypedExpr>,
120    },
121    BinaryOp {
122        op: BinOp,
123        lhs: Box<TypedExpr>,
124        rhs: Box<TypedExpr>,
125    },
126    Cast {
127        inner: Box<TypedExpr>,
128    },
129    FunctionCall {
130        name: String,
131        args: Vec<TypedExpr>,
132    },
133    Case {
134        branches: Vec<(TypedExpr, TypedExpr)>,
135        else_: Option<Box<TypedExpr>>,
136    },
137    IsNull {
138        operand: Box<TypedExpr>,
139        negated: bool,
140    },
141    InList {
142        target: Box<TypedExpr>,
143        values: Vec<TypedExpr>,
144        negated: bool,
145    },
146    Between {
147        target: Box<TypedExpr>,
148        low: Box<TypedExpr>,
149        high: Box<TypedExpr>,
150        negated: bool,
151    },
152}
153
154/// Closure-based column scope. The analyzer passes a closure that
155/// resolves `(table, column)` to a `DataType`, returning `None` if
156/// the column doesn't exist in the active scope. Callers wire this
157/// to the schema registry (production) or a static map (tests).
158pub trait Scope {
159    fn lookup(&self, table: &str, column: &str) -> Option<DataType>;
160}
161
162impl<F> Scope for F
163where
164    F: Fn(&str, &str) -> Option<DataType>,
165{
166    fn lookup(&self, table: &str, column: &str) -> Option<DataType> {
167        self(table, column)
168    }
169}
170
171/// Type a single expression against the given scope.
172pub fn type_expr(expr: &Expr, scope: &dyn Scope) -> Result<TypedExpr, TypeError> {
173    match expr {
174        Expr::Literal { value, .. } => Ok(TypedExpr {
175            ty: literal_type(value),
176            kind: TypedExprKind::Literal(value.clone()),
177        }),
178        Expr::Column { field, .. } => {
179            let (table, column) = match field {
180                FieldRef::TableColumn { table, column } => (table.as_str(), column.as_str()),
181                FieldRef::NodeProperty { alias, property } => (alias.as_str(), property.as_str()),
182                FieldRef::EdgeProperty { alias, property } => (alias.as_str(), property.as_str()),
183                FieldRef::NodeId { .. } => ("", ""),
184            };
185            let ty = scope
186                .lookup(table, column)
187                .ok_or(TypeError::UnknownColumn {
188                    table: table.to_string(),
189                    column: column.to_string(),
190                })?;
191            Ok(TypedExpr {
192                ty,
193                kind: TypedExprKind::Column(field.clone()),
194            })
195        }
196        Expr::Parameter { .. } => {
197            // Parameters get the catch-all Nullable type until the
198            // bind phase substitutes a concrete value. The Fase 4
199            // plan-cache parameter work tracks this properly.
200            Ok(TypedExpr {
201                ty: DataType::Nullable,
202                kind: TypedExprKind::Literal(Value::Null),
203            })
204        }
205        Expr::UnaryOp { op, operand, .. } => {
206            let inner = type_expr(operand, scope)?;
207            let ty = unary_result_type(*op, inner.ty)?;
208            Ok(TypedExpr {
209                ty,
210                kind: TypedExprKind::UnaryOp {
211                    op: *op,
212                    operand: Box::new(inner),
213                },
214            })
215        }
216        Expr::BinaryOp { op, lhs, rhs, .. } => {
217            let l = type_expr(lhs, scope)?;
218            let r = type_expr(rhs, scope)?;
219            let ty = binop_result_type(*op, l.ty, r.ty)?;
220            Ok(TypedExpr {
221                ty,
222                kind: TypedExprKind::BinaryOp {
223                    op: *op,
224                    lhs: Box::new(l),
225                    rhs: Box::new(r),
226                },
227            })
228        }
229        Expr::Cast { inner, target, .. } => {
230            let inner_typed = type_expr(inner, scope)?;
231            // Validate the cast against the catalog using Explicit
232            // context — user wrote it, so the widest rule applies.
233            if !reddb_types::cast_catalog::can_explicit_cast(inner_typed.ty, *target) {
234                return Err(TypeError::InvalidCast {
235                    src: inner_typed.ty,
236                    target: *target,
237                });
238            }
239            Ok(TypedExpr {
240                ty: *target,
241                kind: TypedExprKind::Cast {
242                    inner: Box::new(inner_typed),
243                },
244            })
245        }
246        Expr::FunctionCall { name, args, .. } => {
247            let typed_args = args
248                .iter()
249                .map(|a| type_expr(a, scope))
250                .collect::<Result<Vec<_>, _>>()?;
251            // Look up the function in the static catalog. Resolution
252            // picks the best-matching overload by exact-type score
253            // with a preferred-return-type tie break (see
254            // schema::function_catalog::resolve). Unknown functions
255            // fall through to `Nullable` so the rest of the query
256            // still types — matches PG's permissive
257            // `function does not exist` warning rather than hard fail.
258            let arg_dt: Vec<DataType> = typed_args.iter().map(|t| t.ty).collect();
259            let return_ty = resolve_function_return_type(name, &arg_dt);
260            Ok(TypedExpr {
261                ty: return_ty,
262                kind: TypedExprKind::FunctionCall {
263                    name: name.clone(),
264                    args: typed_args,
265                },
266            })
267        }
268        Expr::Case {
269            branches, else_, ..
270        } => {
271            let mut typed_branches = Vec::with_capacity(branches.len());
272            let mut result_ty: Option<DataType> = None;
273            for (cond, val) in branches {
274                let cond_typed = type_expr(cond, scope)?;
275                let val_typed = type_expr(val, scope)?;
276                let prev_ty = result_ty;
277                result_ty = merge_compatible_type(result_ty, val_typed.ty).map_err(|_| {
278                    TypeError::CaseBranchMismatch {
279                        first: prev_ty.unwrap_or(val_typed.ty),
280                        other: val_typed.ty,
281                    }
282                })?;
283                typed_branches.push((cond_typed, val_typed));
284            }
285            let typed_else = if let Some(else_expr) = else_ {
286                let e = type_expr(else_expr, scope)?;
287                let prev_ty = result_ty;
288                result_ty = merge_compatible_type(result_ty, e.ty).map_err(|_| {
289                    TypeError::CaseBranchMismatch {
290                        first: prev_ty.unwrap_or(e.ty),
291                        other: e.ty,
292                    }
293                })?;
294                Some(Box::new(e))
295            } else {
296                None
297            };
298            let ty = result_ty.unwrap_or(DataType::Nullable);
299            Ok(TypedExpr {
300                ty,
301                kind: TypedExprKind::Case {
302                    branches: typed_branches,
303                    else_: typed_else,
304                },
305            })
306        }
307        Expr::IsNull {
308            operand, negated, ..
309        } => {
310            let inner = type_expr(operand, scope)?;
311            Ok(TypedExpr {
312                ty: DataType::Boolean,
313                kind: TypedExprKind::IsNull {
314                    operand: Box::new(inner),
315                    negated: *negated,
316                },
317            })
318        }
319        Expr::InList {
320            target,
321            values,
322            negated,
323            ..
324        } => {
325            let target_typed = type_expr(target, scope)?;
326            let mut typed_values = Vec::with_capacity(values.len());
327            for v in values {
328                let vt = type_expr(v, scope)?;
329                if vt.ty != target_typed.ty && !can_implicit_cast(vt.ty, target_typed.ty) {
330                    return Err(TypeError::InListMismatch {
331                        target: target_typed.ty,
332                        element: vt.ty,
333                    });
334                }
335                typed_values.push(vt);
336            }
337            Ok(TypedExpr {
338                ty: DataType::Boolean,
339                kind: TypedExprKind::InList {
340                    target: Box::new(target_typed),
341                    values: typed_values,
342                    negated: *negated,
343                },
344            })
345        }
346        Expr::Between {
347            target,
348            low,
349            high,
350            negated,
351            ..
352        } => {
353            let target_typed = type_expr(target, scope)?;
354            let low_typed = type_expr(low, scope)?;
355            let high_typed = type_expr(high, scope)?;
356            // Both bounds must coerce to the target's type.
357            for bound in &[&low_typed, &high_typed] {
358                if bound.ty != target_typed.ty && !can_implicit_cast(bound.ty, target_typed.ty) {
359                    return Err(TypeError::OperatorMismatch {
360                        op: BinOp::Ge,
361                        lhs: target_typed.ty,
362                        rhs: bound.ty,
363                    });
364                }
365            }
366            Ok(TypedExpr {
367                ty: DataType::Boolean,
368                kind: TypedExprKind::Between {
369                    target: Box::new(target_typed),
370                    low: Box::new(low_typed),
371                    high: Box::new(high_typed),
372                    negated: *negated,
373                },
374            })
375        }
376        Expr::Subquery { .. } => Ok(TypedExpr {
377            ty: DataType::Nullable,
378            kind: TypedExprKind::Literal(Value::Null),
379        }),
380        // Slice 7a (#589): no concrete type yet — the analytics
381        // executor and its type inference land in a follow-up. Fall
382        // back to Nullable so downstream callers don't trip.
383        Expr::WindowFunctionCall { .. } => Ok(TypedExpr {
384            ty: DataType::Nullable,
385            kind: TypedExprKind::Literal(Value::Null),
386        }),
387    }
388}
389
390/// Map a `Value` literal to its concrete `DataType`. Mirrors the
391/// existing `Value::data_type()` impl in `schema::types` but is
392/// inlined here so the typer doesn't depend on a method that may
393/// not yet exist on every Value variant.
394fn literal_type(v: &Value) -> DataType {
395    match v {
396        Value::Null => DataType::Nullable,
397        Value::Boolean(_) => DataType::Boolean,
398        Value::Integer(_) => DataType::Integer,
399        Value::UnsignedInteger(_) => DataType::UnsignedInteger,
400        Value::Float(_) => DataType::Float,
401        Value::BigInt(_) => DataType::BigInt,
402        Value::Decimal(_) => DataType::Decimal,
403        Value::DecimalText(_) => DataType::DecimalText,
404        Value::Text(_) => DataType::Text,
405        Value::Blob(_) => DataType::Blob,
406        Value::Timestamp(_) => DataType::Timestamp,
407        Value::TimestampMs(_) => DataType::TimestampMs,
408        Value::Duration(_) => DataType::Duration,
409        Value::Date(_) => DataType::Date,
410        Value::Time(_) => DataType::Time,
411        Value::IpAddr(_) => DataType::IpAddr,
412        Value::Ipv4(_) => DataType::Ipv4,
413        Value::Ipv6(_) => DataType::Ipv6,
414        Value::Subnet(_, _) => DataType::Subnet,
415        Value::Cidr(_, _) => DataType::Cidr,
416        Value::MacAddr(_) => DataType::MacAddr,
417        Value::Port(_) => DataType::Port,
418        Value::Latitude(_) => DataType::Latitude,
419        Value::Longitude(_) => DataType::Longitude,
420        Value::GeoPoint(_, _) => DataType::GeoPoint,
421        Value::Country2(_) => DataType::Country2,
422        Value::Country3(_) => DataType::Country3,
423        Value::Lang2(_) => DataType::Lang2,
424        Value::Lang5(_) => DataType::Lang5,
425        Value::Currency(_) => DataType::Currency,
426        Value::AssetCode(_) => DataType::AssetCode,
427        Value::Money { .. } => DataType::Money,
428        Value::Color(_) => DataType::Color,
429        Value::ColorAlpha(_) => DataType::ColorAlpha,
430        Value::Email(_) => DataType::Email,
431        Value::Url(_) => DataType::Url,
432        Value::Phone(_) => DataType::Phone,
433        Value::Semver(_) => DataType::Semver,
434        Value::Uuid(_) => DataType::Uuid,
435        Value::Vector(_) => DataType::Vector,
436        Value::Array(_) => DataType::Array,
437        Value::Json(_) => DataType::Json,
438        Value::EnumValue(_) => DataType::Enum,
439        Value::NodeRef(_) => DataType::NodeRef,
440        Value::EdgeRef(_) => DataType::EdgeRef,
441        Value::VectorRef(_, _) => DataType::VectorRef,
442        Value::RowRef(_, _) => DataType::RowRef,
443        Value::KeyRef(_, _) => DataType::KeyRef,
444        Value::DocRef(_, _) => DataType::DocRef,
445        Value::TableRef(_) => DataType::TableRef,
446        Value::PageRef(_) => DataType::PageRef,
447        Value::Secret(_) => DataType::Secret,
448        Value::Password(_) => DataType::Password,
449    }
450}
451
452fn resolve_function_return_type(name: &str, arg_types: &[DataType]) -> DataType {
453    let upper = name.to_ascii_uppercase();
454    match upper.as_str() {
455        // CONCAT stringifies every non-null argument at runtime, so the
456        // return type is always text even when the catalog match is
457        // intentionally loose.
458        "CONCAT" | "CONCAT_WS" | "QUOTE_LITERAL" => DataType::Text,
459        "MONEY" => DataType::Money,
460        "MONEY_ASSET" => DataType::AssetCode,
461        "MONEY_MINOR" => DataType::BigInt,
462        "MONEY_SCALE" => DataType::Integer,
463        // COALESCE is effectively `anycompatible`: ignore NULL/unknown
464        // args, then widen left-to-right using the implicit-cast graph.
465        "COALESCE" => resolve_coalesce_return_type(arg_types),
466        _ => reddb_types::function_catalog::resolve(name, arg_types)
467            .map(|entry| entry.return_type)
468            .unwrap_or(DataType::Nullable),
469    }
470}
471
472fn resolve_coalesce_return_type(arg_types: &[DataType]) -> DataType {
473    let mut resolved: Option<DataType> = None;
474
475    for &arg_ty in arg_types {
476        match merge_compatible_type(resolved, arg_ty) {
477            Ok(next) => resolved = next,
478            Err(_) => return DataType::Nullable,
479        }
480    }
481
482    resolved.unwrap_or(DataType::Nullable)
483}
484
485fn merge_compatible_type(
486    current: Option<DataType>,
487    next: DataType,
488) -> Result<Option<DataType>, ()> {
489    if next == DataType::Nullable {
490        return Ok(current);
491    }
492
493    match current {
494        None => Ok(Some(next)),
495        Some(prev) if prev == next => Ok(Some(prev)),
496        Some(prev) if can_implicit_cast(next, prev) => Ok(Some(prev)),
497        Some(prev) if can_implicit_cast(prev, next) => Ok(Some(next)),
498        Some(_) => Err(()),
499    }
500}
501
502/// Resolve the result type of a unary operator. Negation requires a
503/// numeric operand; logical NOT requires a boolean.
504fn unary_result_type(op: UnaryOp, operand: DataType) -> Result<DataType, TypeError> {
505    match op {
506        UnaryOp::Neg if operand.category() == TypeCategory::Numeric => Ok(operand),
507        UnaryOp::Not if operand == DataType::Boolean => Ok(DataType::Boolean),
508        _ => Err(TypeError::UnaryMismatch { op, operand }),
509    }
510}
511
512/// Resolve the result type of a binary operator. Implements a
513/// simplified PG `func_select_candidate` heuristic:
514///
515/// 1. Short-circuit on identical types.
516/// 2. Logical (AND/OR) require booleans on both sides.
517/// 3. Comparison operators always return Boolean. Operands must
518///    share a category; cross-category comparison is an error.
519/// 4. Arithmetic operators promote to the preferred type of the
520///    common Numeric category (Float wins over Integer / BigInt).
521/// 5. `||` (Concat) requires String on both sides — anything that
522///    isn't already Text needs an explicit CAST first.
523fn binop_result_type(op: BinOp, lhs: DataType, rhs: DataType) -> Result<DataType, TypeError> {
524    use BinOp::*;
525    match op {
526        And | Or => {
527            if lhs == DataType::Boolean && rhs == DataType::Boolean {
528                Ok(DataType::Boolean)
529            } else {
530                Err(TypeError::OperatorMismatch { op, lhs, rhs })
531            }
532        }
533        Eq | Ne | Lt | Le | Gt | Ge => {
534            // Same type → trivial. Different types → must share a
535            // category and have an implicit-cast bridge in either
536            // direction.
537            if lhs == rhs {
538                return Ok(DataType::Boolean);
539            }
540            if lhs.category() == rhs.category()
541                && (can_implicit_cast(lhs, rhs) || can_implicit_cast(rhs, lhs))
542            {
543                return Ok(DataType::Boolean);
544            }
545            Err(TypeError::OperatorMismatch { op, lhs, rhs })
546        }
547        Add | Sub | Mul | Div | Mod => {
548            if lhs.category() != TypeCategory::Numeric || rhs.category() != TypeCategory::Numeric {
549                return Err(TypeError::OperatorMismatch { op, lhs, rhs });
550            }
551            // Promote to the preferred member of the category if
552            // either side is preferred. Float beats Integer beats
553            // BigInt under our preference rules.
554            if lhs == DataType::Float || rhs == DataType::Float {
555                Ok(DataType::Float)
556            } else if lhs == DataType::Decimal || rhs == DataType::Decimal {
557                Ok(DataType::Decimal)
558            } else if lhs == DataType::BigInt || rhs == DataType::BigInt {
559                Ok(DataType::BigInt)
560            } else {
561                Ok(DataType::Integer)
562            }
563        }
564        Concat => {
565            if lhs == DataType::Text && rhs == DataType::Text {
566                Ok(DataType::Text)
567            } else {
568                Err(TypeError::OperatorMismatch { op, lhs, rhs })
569            }
570        }
571    }
572}
573
574// `Cast::context_for` lookup helper is unused for now — the typer
575// always uses Explicit context for user-written CAST nodes. Kept as
576// a hook for the future Implicit-on-arith path.
577#[allow(dead_code)]
578fn _ctx_explicit() -> CastContext {
579    CastContext::Explicit
580}
581
582#[cfg(test)]
583mod tests {
584    use super::*;
585    use crate::ast::Span;
586    use crate::lexer::Position;
587    use std::net::{IpAddr, Ipv4Addr};
588    use std::sync::Arc;
589
590    fn span() -> Span {
591        Span {
592            start: Position::default(),
593            end: Position::default(),
594        }
595    }
596
597    fn lit(value: Value) -> Expr {
598        Expr::Literal {
599            value,
600            span: span(),
601        }
602    }
603
604    fn col(table: &str, column: &str) -> Expr {
605        Expr::Column {
606            field: FieldRef::column(table, column),
607            span: span(),
608        }
609    }
610
611    fn bin(op: BinOp, lhs: Expr, rhs: Expr) -> Expr {
612        Expr::BinaryOp {
613            op,
614            lhs: Box::new(lhs),
615            rhs: Box::new(rhs),
616            span: span(),
617        }
618    }
619
620    fn unary(op: UnaryOp, operand: Expr) -> Expr {
621        Expr::UnaryOp {
622            op,
623            operand: Box::new(operand),
624            span: span(),
625        }
626    }
627
628    fn scope(table: &str, column: &str) -> Option<DataType> {
629        match (table, column) {
630            ("", "age") => Some(DataType::Integer),
631            ("", "active") => Some(DataType::Boolean),
632            ("users", "name") => Some(DataType::Text),
633            ("n", "score") => Some(DataType::Float),
634            _ => None,
635        }
636    }
637
638    fn no_scope(_: &str, _: &str) -> Option<DataType> {
639        None
640    }
641
642    #[test]
643    fn literal_values_map_to_declared_types() {
644        let values = vec![
645            (Value::Null, DataType::Nullable),
646            (Value::Boolean(true), DataType::Boolean),
647            (Value::Integer(1), DataType::Integer),
648            (Value::UnsignedInteger(1), DataType::UnsignedInteger),
649            (Value::Float(1.0), DataType::Float),
650            (Value::BigInt(1), DataType::BigInt),
651            (Value::Decimal(100), DataType::Decimal),
652            (Value::Text(Arc::from("x")), DataType::Text),
653            (Value::Blob(vec![1, 2]), DataType::Blob),
654            (Value::Timestamp(1), DataType::Timestamp),
655            (Value::TimestampMs(1), DataType::TimestampMs),
656            (Value::Duration(1), DataType::Duration),
657            (Value::Date(1), DataType::Date),
658            (Value::Time(1), DataType::Time),
659            (
660                Value::IpAddr(IpAddr::V4(Ipv4Addr::LOCALHOST)),
661                DataType::IpAddr,
662            ),
663            (Value::Ipv4(0x7f00_0001), DataType::Ipv4),
664            (Value::Ipv6([0; 16]), DataType::Ipv6),
665            (Value::Subnet(0, 24), DataType::Subnet),
666            (Value::Cidr(0, 24), DataType::Cidr),
667            (Value::MacAddr([1, 2, 3, 4, 5, 6]), DataType::MacAddr),
668            (Value::Port(5432), DataType::Port),
669            (Value::Latitude(1), DataType::Latitude),
670            (Value::Longitude(1), DataType::Longitude),
671            (Value::GeoPoint(1, 2), DataType::GeoPoint),
672            (Value::Country2(*b"BR"), DataType::Country2),
673            (Value::Country3(*b"BRA"), DataType::Country3),
674            (Value::Lang2(*b"pt"), DataType::Lang2),
675            (Value::Lang5(*b"pt-BR"), DataType::Lang5),
676            (Value::Currency(*b"BRL"), DataType::Currency),
677            (Value::AssetCode("BTC".to_string()), DataType::AssetCode),
678            (
679                Value::Money {
680                    asset_code: "BRL".to_string(),
681                    minor_units: 123,
682                    scale: 2,
683                },
684                DataType::Money,
685            ),
686            (Value::Color([1, 2, 3]), DataType::Color),
687            (Value::ColorAlpha([1, 2, 3, 4]), DataType::ColorAlpha),
688            (Value::Email("a@example.com".to_string()), DataType::Email),
689            (Value::Url("https://example.com".to_string()), DataType::Url),
690            (Value::Phone(5511999999999), DataType::Phone),
691            (Value::Semver(1_002_003), DataType::Semver),
692            (Value::Uuid([1; 16]), DataType::Uuid),
693            (Value::Vector(vec![1.0, 2.0]), DataType::Vector),
694            (Value::Array(vec![Value::Integer(1)]), DataType::Array),
695            (Value::Json(br#"{"x":1}"#.to_vec()), DataType::Json),
696            (Value::EnumValue(1), DataType::Enum),
697            (Value::NodeRef("n1".to_string()), DataType::NodeRef),
698            (Value::EdgeRef("e1".to_string()), DataType::EdgeRef),
699            (Value::VectorRef("vecs".to_string(), 1), DataType::VectorRef),
700            (Value::RowRef("rows".to_string(), 1), DataType::RowRef),
701            (
702                Value::KeyRef("kv".to_string(), "k".to_string()),
703                DataType::KeyRef,
704            ),
705            (Value::DocRef("docs".to_string(), 1), DataType::DocRef),
706            (Value::TableRef("users".to_string()), DataType::TableRef),
707            (Value::PageRef(7), DataType::PageRef),
708            (Value::Secret(vec![1, 2, 3]), DataType::Secret),
709            (Value::Password("argon2".to_string()), DataType::Password),
710        ];
711
712        for (value, expected) in values {
713            let typed = type_expr(&lit(value), &no_scope).unwrap();
714            assert_eq!(typed.ty, expected);
715            assert!(matches!(typed.kind, TypedExprKind::Literal(_)));
716        }
717    }
718
719    #[test]
720    fn column_lookup_preserves_field_ref_and_reports_unknowns() {
721        let typed = type_expr(&col("users", "name"), &scope).unwrap();
722        assert_eq!(typed.ty, DataType::Text);
723        assert!(matches!(
724            typed.kind,
725            TypedExprKind::Column(FieldRef::TableColumn { table, column })
726                if table == "users" && column == "name"
727        ));
728
729        let err = type_expr(&col("", "missing"), &scope).unwrap_err();
730        assert!(matches!(
731            err,
732            TypeError::UnknownColumn { ref table, ref column }
733                if table.is_empty() && column == "missing"
734        ));
735        assert_eq!(err.to_string(), "unknown column `missing`");
736    }
737
738    #[test]
739    fn arithmetic_logical_and_unary_ops_return_expected_types() {
740        let add = bin(BinOp::Add, lit(Value::Integer(1)), lit(Value::Float(2.0)));
741        assert_eq!(type_expr(&add, &scope).unwrap().ty, DataType::Float);
742
743        let and = bin(BinOp::And, col("", "active"), lit(Value::Boolean(false)));
744        assert_eq!(type_expr(&and, &scope).unwrap().ty, DataType::Boolean);
745
746        let neg = unary(UnaryOp::Neg, col("", "age"));
747        assert_eq!(type_expr(&neg, &scope).unwrap().ty, DataType::Integer);
748
749        let not = unary(UnaryOp::Not, col("", "active"));
750        assert_eq!(type_expr(&not, &scope).unwrap().ty, DataType::Boolean);
751    }
752
753    #[test]
754    fn operator_mismatches_are_reported() {
755        let bad_and = bin(
756            BinOp::And,
757            lit(Value::Boolean(true)),
758            lit(Value::Integer(1)),
759        );
760        assert!(matches!(
761            type_expr(&bad_and, &scope).unwrap_err(),
762            TypeError::OperatorMismatch {
763                op: BinOp::And,
764                lhs: DataType::Boolean,
765                rhs: DataType::Integer,
766            }
767        ));
768
769        let bad_neg = unary(UnaryOp::Neg, lit(Value::Text(Arc::from("x"))));
770        assert!(matches!(
771            type_expr(&bad_neg, &scope).unwrap_err(),
772            TypeError::UnaryMismatch {
773                op: UnaryOp::Neg,
774                operand: DataType::Text,
775            }
776        ));
777    }
778
779    #[test]
780    fn casts_functions_and_parameters_have_stable_types() {
781        let cast = Expr::Cast {
782            inner: Box::new(lit(Value::Integer(1))),
783            target: DataType::Text,
784            span: span(),
785        };
786        assert_eq!(type_expr(&cast, &scope).unwrap().ty, DataType::Text);
787
788        let concat = Expr::FunctionCall {
789            name: "concat".to_string(),
790            args: vec![lit(Value::Text(Arc::from("a"))), lit(Value::Integer(1))],
791            span: span(),
792        };
793        assert_eq!(type_expr(&concat, &scope).unwrap().ty, DataType::Text);
794
795        let money_minor = Expr::FunctionCall {
796            name: "money_minor".to_string(),
797            args: vec![lit(Value::Money {
798                asset_code: "BRL".to_string(),
799                minor_units: 10,
800                scale: 2,
801            })],
802            span: span(),
803        };
804        assert_eq!(
805            type_expr(&money_minor, &scope).unwrap().ty,
806            DataType::BigInt
807        );
808
809        let coalesce = Expr::FunctionCall {
810            name: "coalesce".to_string(),
811            args: vec![
812                lit(Value::Null),
813                lit(Value::Integer(1)),
814                lit(Value::Float(2.0)),
815            ],
816            span: span(),
817        };
818        assert_eq!(type_expr(&coalesce, &scope).unwrap().ty, DataType::Integer);
819
820        let unknown = Expr::FunctionCall {
821            name: "not_a_function".to_string(),
822            args: Vec::new(),
823            span: span(),
824        };
825        assert_eq!(type_expr(&unknown, &scope).unwrap().ty, DataType::Nullable);
826
827        let parameter = Expr::Parameter {
828            index: 1,
829            span: span(),
830        };
831        assert_eq!(
832            type_expr(&parameter, &scope).unwrap().ty,
833            DataType::Nullable
834        );
835    }
836
837    #[test]
838    fn invalid_casts_case_branches_and_lists_are_errors() {
839        let bad_cast = Expr::Cast {
840            inner: Box::new(lit(Value::Blob(vec![1]))),
841            target: DataType::Money,
842            span: span(),
843        };
844        assert!(matches!(
845            type_expr(&bad_cast, &scope).unwrap_err(),
846            TypeError::InvalidCast {
847                src: DataType::Blob,
848                target: DataType::Money,
849            }
850        ));
851
852        let case = Expr::Case {
853            branches: vec![(
854                lit(Value::Boolean(true)),
855                lit(Value::Text(Arc::from("text"))),
856            )],
857            else_: Some(Box::new(lit(Value::Integer(1)))),
858            span: span(),
859        };
860        assert!(matches!(
861            type_expr(&case, &scope).unwrap_err(),
862            TypeError::CaseBranchMismatch {
863                first: DataType::Text,
864                other: DataType::Integer,
865            }
866        ));
867
868        let in_list = Expr::InList {
869            target: Box::new(lit(Value::Integer(1))),
870            values: vec![lit(Value::Text(Arc::from("x")))],
871            negated: false,
872            span: span(),
873        };
874        assert!(matches!(
875            type_expr(&in_list, &scope).unwrap_err(),
876            TypeError::InListMismatch {
877                target: DataType::Integer,
878                element: DataType::Text,
879            }
880        ));
881    }
882
883    #[test]
884    fn predicates_return_boolean_when_bounds_and_values_are_compatible() {
885        let is_null = Expr::IsNull {
886            operand: Box::new(col("", "age")),
887            negated: true,
888            span: span(),
889        };
890        assert_eq!(type_expr(&is_null, &scope).unwrap().ty, DataType::Boolean);
891
892        let in_list = Expr::InList {
893            target: Box::new(col("", "age")),
894            values: vec![lit(Value::Integer(1)), lit(Value::Integer(2))],
895            negated: false,
896            span: span(),
897        };
898        assert_eq!(type_expr(&in_list, &scope).unwrap().ty, DataType::Boolean);
899
900        let between = Expr::Between {
901            target: Box::new(col("", "age")),
902            low: Box::new(lit(Value::Integer(1))),
903            high: Box::new(lit(Value::Integer(9))),
904            negated: false,
905            span: span(),
906        };
907        assert_eq!(type_expr(&between, &scope).unwrap().ty, DataType::Boolean);
908
909        let bad_between = Expr::Between {
910            target: Box::new(col("", "age")),
911            low: Box::new(lit(Value::Text(Arc::from("low")))),
912            high: Box::new(lit(Value::Integer(9))),
913            negated: false,
914            span: span(),
915        };
916        assert!(matches!(
917            type_expr(&bad_between, &scope).unwrap_err(),
918            TypeError::OperatorMismatch {
919                op: BinOp::Ge,
920                lhs: DataType::Integer,
921                rhs: DataType::Text,
922            }
923        ));
924    }
925}