alopex_sql/planner/typed_expr.rs
1//! Type-checked expression types for the planner.
2//!
3//! This module defines [`TypedExpr`] and related types that represent
4//! expressions after type checking. These types carry resolved type
5//! information and are used in [`crate::planner::LogicalPlan`] construction.
6//!
7//! # Overview
8//!
9//! - [`TypedExpr`]: A type-checked expression with resolved type and span
10//! - [`TypedExprKind`]: The kind of typed expression (literals, column refs, operators, etc.)
11//! - [`SortExpr`]: A sort expression for ORDER BY clauses
12//! - [`TypedAssignment`]: A typed assignment for UPDATE SET clauses
13//! - [`ProjectedColumn`]: A projected column for SELECT clauses
14//! - [`Projection`]: The projection specification for SELECT
15
16use crate::ast::expr::{BinaryOp, Literal, PatternMatchKind, UnaryOp, WindowFrame};
17use crate::ast::span::Span;
18use crate::planner::logical_plan::LogicalPlan;
19use crate::planner::types::ResolvedType;
20
21/// A type-checked expression with resolved type information.
22///
23/// This struct represents an expression that has been validated by the type checker.
24/// It contains the expression kind, the resolved type, and the source span for
25/// error reporting.
26///
27/// # Examples
28///
29/// ```
30/// use alopex_sql::planner::typed_expr::{TypedExpr, TypedExprKind};
31/// use alopex_sql::planner::types::ResolvedType;
32/// use alopex_sql::ast::expr::Literal;
33/// use alopex_sql::Span;
34///
35/// let expr = TypedExpr {
36/// kind: TypedExprKind::Literal(Literal::Number("42".to_string())),
37/// resolved_type: ResolvedType::Integer,
38/// span: Span::default(),
39/// };
40/// ```
41#[derive(Debug, Clone)]
42pub struct TypedExpr {
43 /// The kind of expression.
44 pub kind: TypedExprKind,
45 /// The resolved type of this expression.
46 pub resolved_type: ResolvedType,
47 /// Source span for error reporting.
48 pub span: Span,
49}
50
51/// The kind of a typed expression.
52///
53/// Each variant corresponds to a different expression type that has been
54/// type-checked. Unlike [`ExprKind`](crate::ast::expr::ExprKind), column
55/// references include the resolved column index for efficient access.
56#[derive(Debug, Clone)]
57pub enum TypedExprKind {
58 /// A literal value.
59 Literal(Literal),
60
61 /// A column reference with resolved table and column index.
62 ColumnRef {
63 /// The table name (resolved, never None after name resolution).
64 table: String,
65 /// The column name.
66 column: String,
67 /// The column index in the table's column list (0-based).
68 /// This allows efficient column access during execution.
69 column_index: usize,
70 },
71
72 /// A binary operation.
73 BinaryOp {
74 /// Left operand.
75 left: Box<TypedExpr>,
76 /// The operator.
77 op: BinaryOp,
78 /// Right operand.
79 right: Box<TypedExpr>,
80 },
81
82 /// A unary operation.
83 UnaryOp {
84 /// The operator.
85 op: UnaryOp,
86 /// The operand.
87 operand: Box<TypedExpr>,
88 },
89
90 /// A searched or simple CASE expression.
91 Case {
92 /// The simple CASE operand, or None for searched CASE.
93 operand: Option<Box<TypedExpr>>,
94 /// WHEN/THEN branches in source order.
95 branches: Vec<TypedCaseWhen>,
96 /// The ELSE expression, or None for the implicit NULL result.
97 else_expr: Option<Box<TypedExpr>>,
98 },
99
100 /// A function call.
101 FunctionCall {
102 /// Function name.
103 name: String,
104 /// Function arguments.
105 args: Vec<TypedExpr>,
106 /// DISTINCT modifier for aggregate functions.
107 distinct: bool,
108 /// STAR modifier for COUNT(*).
109 star: bool,
110 /// Optional typed `OVER (...)` specification.
111 over: Option<TypedWindowSpec>,
112 },
113
114 /// An explicit type cast.
115 Cast {
116 /// Expression to cast.
117 expr: Box<TypedExpr>,
118 /// Target type.
119 target_type: ResolvedType,
120 },
121
122 /// A BETWEEN expression.
123 Between {
124 /// Expression to test.
125 expr: Box<TypedExpr>,
126 /// Lower bound.
127 low: Box<TypedExpr>,
128 /// Upper bound.
129 high: Box<TypedExpr>,
130 /// Whether the expression is negated (NOT BETWEEN).
131 negated: bool,
132 },
133
134 /// A LIKE pattern match expression.
135 Like {
136 /// Expression to match.
137 expr: Box<TypedExpr>,
138 /// Pattern to match against.
139 pattern: Box<TypedExpr>,
140 /// Optional escape character.
141 escape: Option<Box<TypedExpr>>,
142 /// Whether the expression is negated (NOT LIKE).
143 negated: bool,
144 /// Pattern operator variant.
145 kind: PatternMatchKind,
146 },
147
148 /// An IN list expression.
149 InList {
150 /// Expression to test.
151 expr: Box<TypedExpr>,
152 /// List of values to check against.
153 list: Vec<TypedExpr>,
154 /// Whether the expression is negated (NOT IN).
155 negated: bool,
156 },
157
158 /// An IS NULL expression.
159 IsNull {
160 /// Expression to test.
161 expr: Box<TypedExpr>,
162 /// Whether the expression is negated (IS NOT NULL).
163 negated: bool,
164 },
165
166 /// A vector literal.
167 VectorLiteral(Vec<f64>),
168
169 /// A scalar subquery.
170 ScalarSubquery(Box<LogicalPlan>),
171
172 /// An IN subquery.
173 InSubquery {
174 /// Expression to test.
175 expr: Box<TypedExpr>,
176 /// Planned subquery.
177 subquery: Box<LogicalPlan>,
178 /// Whether this is NOT IN.
179 negated: bool,
180 },
181
182 /// An EXISTS subquery.
183 Exists {
184 /// Planned subquery.
185 subquery: Box<LogicalPlan>,
186 /// Whether this is NOT EXISTS.
187 negated: bool,
188 },
189
190 /// A quantified comparison subquery.
191 Quantified {
192 /// Expression to compare.
193 expr: Box<TypedExpr>,
194 /// Comparison operator.
195 op: BinaryOp,
196 /// ANY/ALL quantifier.
197 quantifier: Quantifier,
198 /// Planned subquery.
199 subquery: Box<LogicalPlan>,
200 },
201}
202
203/// A type-checked window specification.
204#[derive(Debug, Clone)]
205pub struct TypedWindowSpec {
206 /// Expressions identifying independent partitions.
207 pub partition_by: Vec<TypedExpr>,
208 /// Window-local ordering.
209 pub order_by: Vec<SortExpr>,
210 /// Semantically validated explicit frame, if supplied.
211 pub frame: Option<WindowFrame>,
212}
213
214#[derive(Debug, Clone)]
215pub struct TypedCaseWhen {
216 pub when: TypedExpr,
217 pub then: TypedExpr,
218}
219
220/// Quantifier for quantified subquery comparisons.
221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222pub enum Quantifier {
223 Any,
224 All,
225}
226
227/// A sort expression for ORDER BY clauses.
228///
229/// Contains a typed expression and sort direction information.
230///
231/// # Examples
232///
233/// ```
234/// use alopex_sql::planner::typed_expr::{SortExpr, TypedExpr, TypedExprKind};
235/// use alopex_sql::planner::types::ResolvedType;
236/// use alopex_sql::Span;
237///
238/// let sort_expr = SortExpr {
239/// expr: TypedExpr {
240/// kind: TypedExprKind::ColumnRef {
241/// table: "users".to_string(),
242/// column: "name".to_string(),
243/// column_index: 1,
244/// },
245/// resolved_type: ResolvedType::Text,
246/// span: Span::default(),
247/// },
248/// asc: true,
249/// nulls_first: false,
250/// };
251/// ```
252#[derive(Debug, Clone)]
253pub struct SortExpr {
254 /// The expression to sort by.
255 pub expr: TypedExpr,
256 /// Sort in ascending order (true) or descending (false).
257 pub asc: bool,
258 /// Place NULLs first (true) or last (false).
259 pub nulls_first: bool,
260}
261
262/// A typed assignment for UPDATE SET clauses.
263///
264/// Contains the column name, index, and the typed value expression.
265///
266/// # Examples
267///
268/// ```
269/// use alopex_sql::planner::typed_expr::{TypedAssignment, TypedExpr, TypedExprKind};
270/// use alopex_sql::planner::types::ResolvedType;
271/// use alopex_sql::ast::expr::Literal;
272/// use alopex_sql::Span;
273///
274/// let assignment = TypedAssignment {
275/// column: "name".to_string(),
276/// column_index: 1,
277/// value: TypedExpr {
278/// kind: TypedExprKind::Literal(Literal::String("Bob".to_string())),
279/// resolved_type: ResolvedType::Text,
280/// span: Span::default(),
281/// },
282/// };
283/// ```
284#[derive(Debug, Clone)]
285pub struct TypedAssignment {
286 /// The column name being assigned.
287 pub column: String,
288 /// The column index in the table's column list (0-based).
289 pub column_index: usize,
290 /// The value expression (type-checked against the column type).
291 pub value: TypedExpr,
292}
293
294/// A projected column for SELECT clauses.
295///
296/// Contains a typed expression and an optional alias.
297///
298/// # Examples
299///
300/// ```
301/// use alopex_sql::planner::typed_expr::{ProjectedColumn, TypedExpr, TypedExprKind};
302/// use alopex_sql::planner::types::ResolvedType;
303/// use alopex_sql::Span;
304///
305/// // SELECT name AS user_name
306/// let projected = ProjectedColumn {
307/// expr: TypedExpr {
308/// kind: TypedExprKind::ColumnRef {
309/// table: "users".to_string(),
310/// column: "name".to_string(),
311/// column_index: 1,
312/// },
313/// resolved_type: ResolvedType::Text,
314/// span: Span::default(),
315/// },
316/// alias: Some("user_name".to_string()),
317/// };
318/// ```
319#[derive(Debug, Clone)]
320pub struct ProjectedColumn {
321 /// The projected expression.
322 pub expr: TypedExpr,
323 /// Optional alias (AS name).
324 pub alias: Option<String>,
325}
326
327/// Projection specification for SELECT clauses.
328///
329/// Represents either all columns (after wildcard expansion) or specific columns.
330#[derive(Debug, Clone)]
331pub enum Projection {
332 /// All columns (expanded from `*`).
333 /// Contains the list of column names in definition order.
334 All(Vec<String>),
335
336 /// Specific columns/expressions.
337 Columns(Vec<ProjectedColumn>),
338}
339
340impl TypedExpr {
341 /// Creates a new typed expression.
342 pub fn new(kind: TypedExprKind, resolved_type: ResolvedType, span: Span) -> Self {
343 Self {
344 kind,
345 resolved_type,
346 span,
347 }
348 }
349
350 /// Creates a typed literal expression.
351 pub fn literal(lit: Literal, resolved_type: ResolvedType, span: Span) -> Self {
352 Self::new(TypedExprKind::Literal(lit), resolved_type, span)
353 }
354
355 /// Creates a typed column reference.
356 pub fn column_ref(
357 table: String,
358 column: String,
359 column_index: usize,
360 resolved_type: ResolvedType,
361 span: Span,
362 ) -> Self {
363 Self::new(
364 TypedExprKind::ColumnRef {
365 table,
366 column,
367 column_index,
368 },
369 resolved_type,
370 span,
371 )
372 }
373
374 /// Creates a typed binary operation.
375 pub fn binary_op(
376 left: TypedExpr,
377 op: BinaryOp,
378 right: TypedExpr,
379 resolved_type: ResolvedType,
380 span: Span,
381 ) -> Self {
382 Self::new(
383 TypedExprKind::BinaryOp {
384 left: Box::new(left),
385 op,
386 right: Box::new(right),
387 },
388 resolved_type,
389 span,
390 )
391 }
392
393 /// Creates a typed unary operation.
394 pub fn unary_op(
395 op: UnaryOp,
396 operand: TypedExpr,
397 resolved_type: ResolvedType,
398 span: Span,
399 ) -> Self {
400 Self::new(
401 TypedExprKind::UnaryOp {
402 op,
403 operand: Box::new(operand),
404 },
405 resolved_type,
406 span,
407 )
408 }
409
410 /// Creates a typed function call.
411 pub fn function_call(
412 name: String,
413 args: Vec<TypedExpr>,
414 distinct: bool,
415 star: bool,
416 resolved_type: ResolvedType,
417 span: Span,
418 ) -> Self {
419 Self::new(
420 TypedExprKind::FunctionCall {
421 name,
422 args,
423 distinct,
424 star,
425 over: None,
426 },
427 resolved_type,
428 span,
429 )
430 }
431
432 /// Creates a typed cast expression.
433 pub fn cast(expr: TypedExpr, target_type: ResolvedType, span: Span) -> Self {
434 Self::new(
435 TypedExprKind::Cast {
436 expr: Box::new(expr),
437 target_type: target_type.clone(),
438 },
439 target_type,
440 span,
441 )
442 }
443
444 /// Creates a typed vector literal.
445 pub fn vector_literal(values: Vec<f64>, dimension: u32, span: Span) -> Self {
446 use crate::ast::ddl::VectorMetric;
447 Self::new(
448 TypedExprKind::VectorLiteral(values),
449 ResolvedType::Vector {
450 dimension,
451 metric: VectorMetric::Cosine,
452 },
453 span,
454 )
455 }
456}
457
458impl SortExpr {
459 /// Creates a new sort expression with ascending order.
460 pub fn asc(expr: TypedExpr) -> Self {
461 Self {
462 expr,
463 asc: true,
464 nulls_first: false,
465 }
466 }
467
468 /// Creates a new sort expression with descending order.
469 ///
470 /// Note: `nulls_first` defaults to `false` (NULLS LAST) for consistency.
471 /// Use [`SortExpr::new`] for explicit NULLS ordering.
472 pub fn desc(expr: TypedExpr) -> Self {
473 Self {
474 expr,
475 asc: false,
476 nulls_first: false,
477 }
478 }
479
480 /// Creates a new sort expression with custom settings.
481 pub fn new(expr: TypedExpr, asc: bool, nulls_first: bool) -> Self {
482 Self {
483 expr,
484 asc,
485 nulls_first,
486 }
487 }
488}
489
490impl TypedAssignment {
491 /// Creates a new typed assignment.
492 pub fn new(column: String, column_index: usize, value: TypedExpr) -> Self {
493 Self {
494 column,
495 column_index,
496 value,
497 }
498 }
499}
500
501impl ProjectedColumn {
502 /// Creates a new projected column without an alias.
503 pub fn new(expr: TypedExpr) -> Self {
504 Self { expr, alias: None }
505 }
506
507 /// Creates a new projected column with an alias.
508 pub fn with_alias(expr: TypedExpr, alias: String) -> Self {
509 Self {
510 expr,
511 alias: Some(alias),
512 }
513 }
514
515 /// Returns the output name (alias if present, otherwise derived from expression).
516 ///
517 /// Returns:
518 /// - The alias if one was specified (e.g., `SELECT name AS user_name`)
519 /// - The column name for simple column references (e.g., `SELECT name`)
520 /// - `None` for complex expressions without an alias (e.g., `SELECT 1 + 2`)
521 ///
522 /// Complex expressions (function calls, literals, binary operations) return `None`
523 /// because they don't have a natural name. Use [`with_alias`](Self::with_alias)
524 /// to give them an output name.
525 pub fn output_name(&self) -> Option<&str> {
526 if let Some(ref alias) = self.alias {
527 return Some(alias);
528 }
529 // For column references, return the column name
530 if let TypedExprKind::ColumnRef { ref column, .. } = self.expr.kind {
531 return Some(column);
532 }
533 None
534 }
535}
536
537impl Projection {
538 /// Returns the number of columns in the projection.
539 pub fn len(&self) -> usize {
540 match self {
541 Projection::All(cols) => cols.len(),
542 Projection::Columns(cols) => cols.len(),
543 }
544 }
545
546 /// Returns true if the projection has no columns.
547 pub fn is_empty(&self) -> bool {
548 self.len() == 0
549 }
550
551 /// Returns the column names in the projection.
552 ///
553 /// For [`Projection::All`], all names are present (from the wildcard expansion).
554 /// For [`Projection::Columns`], names may be `None` for complex expressions
555 /// without aliases. See [`ProjectedColumn::output_name`] for details.
556 pub fn column_names(&self) -> Vec<Option<&str>> {
557 match self {
558 Projection::All(cols) => cols.iter().map(|s| Some(s.as_str())).collect(),
559 Projection::Columns(cols) => cols.iter().map(|c| c.output_name()).collect(),
560 }
561 }
562}
563
564#[cfg(test)]
565mod tests {
566 use super::*;
567 use crate::ast::ddl::VectorMetric;
568
569 #[test]
570 fn test_typed_expr_literal() {
571 let expr = TypedExpr::literal(
572 Literal::Number("42".to_string()),
573 ResolvedType::Integer,
574 Span::default(),
575 );
576
577 assert!(matches!(
578 expr.kind,
579 TypedExprKind::Literal(Literal::Number(_))
580 ));
581 assert_eq!(expr.resolved_type, ResolvedType::Integer);
582 }
583
584 #[test]
585 fn test_typed_expr_column_ref() {
586 let expr = TypedExpr::column_ref(
587 "users".to_string(),
588 "id".to_string(),
589 0,
590 ResolvedType::Integer,
591 Span::default(),
592 );
593
594 if let TypedExprKind::ColumnRef {
595 table,
596 column,
597 column_index,
598 } = &expr.kind
599 {
600 assert_eq!(table, "users");
601 assert_eq!(column, "id");
602 assert_eq!(*column_index, 0);
603 } else {
604 panic!("Expected ColumnRef");
605 }
606 }
607
608 #[test]
609 fn test_typed_expr_binary_op() {
610 let left = TypedExpr::literal(
611 Literal::Number("1".to_string()),
612 ResolvedType::Integer,
613 Span::default(),
614 );
615 let right = TypedExpr::literal(
616 Literal::Number("2".to_string()),
617 ResolvedType::Integer,
618 Span::default(),
619 );
620
621 let expr = TypedExpr::binary_op(
622 left,
623 BinaryOp::Add,
624 right,
625 ResolvedType::Integer,
626 Span::default(),
627 );
628
629 assert!(matches!(expr.kind, TypedExprKind::BinaryOp { .. }));
630 assert_eq!(expr.resolved_type, ResolvedType::Integer);
631 }
632
633 #[test]
634 fn test_typed_expr_vector_literal() {
635 let values = vec![1.0, 2.0, 3.0];
636 let expr = TypedExpr::vector_literal(values.clone(), 3, Span::default());
637
638 if let TypedExprKind::VectorLiteral(v) = &expr.kind {
639 assert_eq!(v, &values);
640 } else {
641 panic!("Expected VectorLiteral");
642 }
643
644 if let ResolvedType::Vector { dimension, metric } = &expr.resolved_type {
645 assert_eq!(*dimension, 3);
646 assert_eq!(*metric, VectorMetric::Cosine);
647 } else {
648 panic!("Expected Vector type");
649 }
650 }
651
652 #[test]
653 fn test_sort_expr_asc() {
654 let col = TypedExpr::column_ref(
655 "users".to_string(),
656 "name".to_string(),
657 1,
658 ResolvedType::Text,
659 Span::default(),
660 );
661 let sort = SortExpr::asc(col);
662
663 assert!(sort.asc);
664 assert!(!sort.nulls_first);
665 }
666
667 #[test]
668 fn test_sort_expr_desc() {
669 let col = TypedExpr::column_ref(
670 "users".to_string(),
671 "name".to_string(),
672 1,
673 ResolvedType::Text,
674 Span::default(),
675 );
676 let sort = SortExpr::desc(col);
677
678 assert!(!sort.asc);
679 // NULLS LAST is the consistent default for both ASC and DESC
680 assert!(!sort.nulls_first);
681 }
682
683 #[test]
684 fn test_typed_assignment() {
685 let value = TypedExpr::literal(
686 Literal::String("Alice".to_string()),
687 ResolvedType::Text,
688 Span::default(),
689 );
690 let assignment = TypedAssignment::new("name".to_string(), 1, value);
691
692 assert_eq!(assignment.column, "name");
693 assert_eq!(assignment.column_index, 1);
694 }
695
696 #[test]
697 fn test_projected_column_output_name() {
698 let col = TypedExpr::column_ref(
699 "users".to_string(),
700 "name".to_string(),
701 1,
702 ResolvedType::Text,
703 Span::default(),
704 );
705
706 // Without alias, output name is the column name
707 let proj1 = ProjectedColumn::new(col.clone());
708 assert_eq!(proj1.output_name(), Some("name"));
709
710 // With alias, output name is the alias
711 let proj2 = ProjectedColumn::with_alias(col, "user_name".to_string());
712 assert_eq!(proj2.output_name(), Some("user_name"));
713 }
714
715 #[test]
716 fn test_projection_all() {
717 let columns = vec!["id".to_string(), "name".to_string(), "email".to_string()];
718 let proj = Projection::All(columns);
719
720 assert_eq!(proj.len(), 3);
721 assert!(!proj.is_empty());
722
723 let names: Vec<_> = proj.column_names();
724 assert_eq!(names, vec![Some("id"), Some("name"), Some("email")]);
725 }
726
727 #[test]
728 fn test_projection_columns() {
729 let col1 = ProjectedColumn::new(TypedExpr::column_ref(
730 "users".to_string(),
731 "id".to_string(),
732 0,
733 ResolvedType::Integer,
734 Span::default(),
735 ));
736 let col2 = ProjectedColumn::with_alias(
737 TypedExpr::column_ref(
738 "users".to_string(),
739 "name".to_string(),
740 1,
741 ResolvedType::Text,
742 Span::default(),
743 ),
744 "user_name".to_string(),
745 );
746
747 let proj = Projection::Columns(vec![col1, col2]);
748
749 assert_eq!(proj.len(), 2);
750 let names: Vec<_> = proj.column_names();
751 assert_eq!(names, vec![Some("id"), Some("user_name")]);
752 }
753
754 #[test]
755 fn test_typed_expr_cast() {
756 let inner = TypedExpr::literal(
757 Literal::Number("42".to_string()),
758 ResolvedType::Integer,
759 Span::default(),
760 );
761 let expr = TypedExpr::cast(inner, ResolvedType::Double, Span::default());
762
763 assert!(matches!(expr.kind, TypedExprKind::Cast { .. }));
764 assert_eq!(expr.resolved_type, ResolvedType::Double);
765 }
766
767 #[test]
768 fn test_typed_expr_kind_between() {
769 let expr_kind = TypedExprKind::Between {
770 expr: Box::new(TypedExpr::column_ref(
771 "t".to_string(),
772 "x".to_string(),
773 0,
774 ResolvedType::Integer,
775 Span::default(),
776 )),
777 low: Box::new(TypedExpr::literal(
778 Literal::Number("1".to_string()),
779 ResolvedType::Integer,
780 Span::default(),
781 )),
782 high: Box::new(TypedExpr::literal(
783 Literal::Number("10".to_string()),
784 ResolvedType::Integer,
785 Span::default(),
786 )),
787 negated: false,
788 };
789
790 assert!(matches!(
791 expr_kind,
792 TypedExprKind::Between { negated: false, .. }
793 ));
794 }
795
796 #[test]
797 fn test_typed_expr_kind_is_null() {
798 let expr_kind = TypedExprKind::IsNull {
799 expr: Box::new(TypedExpr::column_ref(
800 "t".to_string(),
801 "x".to_string(),
802 0,
803 ResolvedType::Integer,
804 Span::default(),
805 )),
806 negated: true,
807 };
808
809 assert!(matches!(
810 expr_kind,
811 TypedExprKind::IsNull { negated: true, .. }
812 ));
813 }
814}