1use crate::ast::{BinOp, Expr, FieldRef, UnaryOp};
32use reddb_types::cast_catalog::{can_implicit_cast, CastContext};
33use reddb_types::types::{DataType, TypeCategory, Value};
34
35#[derive(Debug, Clone)]
39pub enum TypeError {
40 UnknownColumn { table: String, column: String },
42 OperatorMismatch {
45 op: BinOp,
46 lhs: DataType,
47 rhs: DataType,
48 },
49 UnaryMismatch { op: UnaryOp, operand: DataType },
51 InvalidCast { src: DataType, target: DataType },
54 CaseBranchMismatch { first: DataType, other: DataType },
56 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#[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
154pub 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
171pub 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 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 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 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 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 Expr::WindowFunctionCall { .. } => Ok(TypedExpr {
384 ty: DataType::Nullable,
385 kind: TypedExprKind::Literal(Value::Null),
386 }),
387 }
388}
389
390fn 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" | "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" => 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
502fn 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
512fn 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 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 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#[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(¬, &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(¶meter, &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}