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};
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}
211
212#[derive(Debug, Clone)]
213pub struct TypedCaseWhen {
214 pub when: TypedExpr,
215 pub then: TypedExpr,
216}
217
218/// Quantifier for quantified subquery comparisons.
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220pub enum Quantifier {
221 Any,
222 All,
223}
224
225/// A sort expression for ORDER BY clauses.
226///
227/// Contains a typed expression and sort direction information.
228///
229/// # Examples
230///
231/// ```
232/// use alopex_sql::planner::typed_expr::{SortExpr, TypedExpr, TypedExprKind};
233/// use alopex_sql::planner::types::ResolvedType;
234/// use alopex_sql::Span;
235///
236/// let sort_expr = SortExpr {
237/// expr: TypedExpr {
238/// kind: TypedExprKind::ColumnRef {
239/// table: "users".to_string(),
240/// column: "name".to_string(),
241/// column_index: 1,
242/// },
243/// resolved_type: ResolvedType::Text,
244/// span: Span::default(),
245/// },
246/// asc: true,
247/// nulls_first: false,
248/// };
249/// ```
250#[derive(Debug, Clone)]
251pub struct SortExpr {
252 /// The expression to sort by.
253 pub expr: TypedExpr,
254 /// Sort in ascending order (true) or descending (false).
255 pub asc: bool,
256 /// Place NULLs first (true) or last (false).
257 pub nulls_first: bool,
258}
259
260/// A typed assignment for UPDATE SET clauses.
261///
262/// Contains the column name, index, and the typed value expression.
263///
264/// # Examples
265///
266/// ```
267/// use alopex_sql::planner::typed_expr::{TypedAssignment, TypedExpr, TypedExprKind};
268/// use alopex_sql::planner::types::ResolvedType;
269/// use alopex_sql::ast::expr::Literal;
270/// use alopex_sql::Span;
271///
272/// let assignment = TypedAssignment {
273/// column: "name".to_string(),
274/// column_index: 1,
275/// value: TypedExpr {
276/// kind: TypedExprKind::Literal(Literal::String("Bob".to_string())),
277/// resolved_type: ResolvedType::Text,
278/// span: Span::default(),
279/// },
280/// };
281/// ```
282#[derive(Debug, Clone)]
283pub struct TypedAssignment {
284 /// The column name being assigned.
285 pub column: String,
286 /// The column index in the table's column list (0-based).
287 pub column_index: usize,
288 /// The value expression (type-checked against the column type).
289 pub value: TypedExpr,
290}
291
292/// A projected column for SELECT clauses.
293///
294/// Contains a typed expression and an optional alias.
295///
296/// # Examples
297///
298/// ```
299/// use alopex_sql::planner::typed_expr::{ProjectedColumn, TypedExpr, TypedExprKind};
300/// use alopex_sql::planner::types::ResolvedType;
301/// use alopex_sql::Span;
302///
303/// // SELECT name AS user_name
304/// let projected = ProjectedColumn {
305/// expr: TypedExpr {
306/// kind: TypedExprKind::ColumnRef {
307/// table: "users".to_string(),
308/// column: "name".to_string(),
309/// column_index: 1,
310/// },
311/// resolved_type: ResolvedType::Text,
312/// span: Span::default(),
313/// },
314/// alias: Some("user_name".to_string()),
315/// };
316/// ```
317#[derive(Debug, Clone)]
318pub struct ProjectedColumn {
319 /// The projected expression.
320 pub expr: TypedExpr,
321 /// Optional alias (AS name).
322 pub alias: Option<String>,
323}
324
325/// Projection specification for SELECT clauses.
326///
327/// Represents either all columns (after wildcard expansion) or specific columns.
328#[derive(Debug, Clone)]
329pub enum Projection {
330 /// All columns (expanded from `*`).
331 /// Contains the list of column names in definition order.
332 All(Vec<String>),
333
334 /// Specific columns/expressions.
335 Columns(Vec<ProjectedColumn>),
336}
337
338impl TypedExpr {
339 /// Creates a new typed expression.
340 pub fn new(kind: TypedExprKind, resolved_type: ResolvedType, span: Span) -> Self {
341 Self {
342 kind,
343 resolved_type,
344 span,
345 }
346 }
347
348 /// Creates a typed literal expression.
349 pub fn literal(lit: Literal, resolved_type: ResolvedType, span: Span) -> Self {
350 Self::new(TypedExprKind::Literal(lit), resolved_type, span)
351 }
352
353 /// Creates a typed column reference.
354 pub fn column_ref(
355 table: String,
356 column: String,
357 column_index: usize,
358 resolved_type: ResolvedType,
359 span: Span,
360 ) -> Self {
361 Self::new(
362 TypedExprKind::ColumnRef {
363 table,
364 column,
365 column_index,
366 },
367 resolved_type,
368 span,
369 )
370 }
371
372 /// Creates a typed binary operation.
373 pub fn binary_op(
374 left: TypedExpr,
375 op: BinaryOp,
376 right: TypedExpr,
377 resolved_type: ResolvedType,
378 span: Span,
379 ) -> Self {
380 Self::new(
381 TypedExprKind::BinaryOp {
382 left: Box::new(left),
383 op,
384 right: Box::new(right),
385 },
386 resolved_type,
387 span,
388 )
389 }
390
391 /// Creates a typed unary operation.
392 pub fn unary_op(
393 op: UnaryOp,
394 operand: TypedExpr,
395 resolved_type: ResolvedType,
396 span: Span,
397 ) -> Self {
398 Self::new(
399 TypedExprKind::UnaryOp {
400 op,
401 operand: Box::new(operand),
402 },
403 resolved_type,
404 span,
405 )
406 }
407
408 /// Creates a typed function call.
409 pub fn function_call(
410 name: String,
411 args: Vec<TypedExpr>,
412 distinct: bool,
413 star: bool,
414 resolved_type: ResolvedType,
415 span: Span,
416 ) -> Self {
417 Self::new(
418 TypedExprKind::FunctionCall {
419 name,
420 args,
421 distinct,
422 star,
423 over: None,
424 },
425 resolved_type,
426 span,
427 )
428 }
429
430 /// Creates a typed cast expression.
431 pub fn cast(expr: TypedExpr, target_type: ResolvedType, span: Span) -> Self {
432 Self::new(
433 TypedExprKind::Cast {
434 expr: Box::new(expr),
435 target_type: target_type.clone(),
436 },
437 target_type,
438 span,
439 )
440 }
441
442 /// Creates a typed vector literal.
443 pub fn vector_literal(values: Vec<f64>, dimension: u32, span: Span) -> Self {
444 use crate::ast::ddl::VectorMetric;
445 Self::new(
446 TypedExprKind::VectorLiteral(values),
447 ResolvedType::Vector {
448 dimension,
449 metric: VectorMetric::Cosine,
450 },
451 span,
452 )
453 }
454}
455
456impl SortExpr {
457 /// Creates a new sort expression with ascending order.
458 pub fn asc(expr: TypedExpr) -> Self {
459 Self {
460 expr,
461 asc: true,
462 nulls_first: false,
463 }
464 }
465
466 /// Creates a new sort expression with descending order.
467 ///
468 /// Note: `nulls_first` defaults to `false` (NULLS LAST) for consistency.
469 /// Use [`SortExpr::new`] for explicit NULLS ordering.
470 pub fn desc(expr: TypedExpr) -> Self {
471 Self {
472 expr,
473 asc: false,
474 nulls_first: false,
475 }
476 }
477
478 /// Creates a new sort expression with custom settings.
479 pub fn new(expr: TypedExpr, asc: bool, nulls_first: bool) -> Self {
480 Self {
481 expr,
482 asc,
483 nulls_first,
484 }
485 }
486}
487
488impl TypedAssignment {
489 /// Creates a new typed assignment.
490 pub fn new(column: String, column_index: usize, value: TypedExpr) -> Self {
491 Self {
492 column,
493 column_index,
494 value,
495 }
496 }
497}
498
499impl ProjectedColumn {
500 /// Creates a new projected column without an alias.
501 pub fn new(expr: TypedExpr) -> Self {
502 Self { expr, alias: None }
503 }
504
505 /// Creates a new projected column with an alias.
506 pub fn with_alias(expr: TypedExpr, alias: String) -> Self {
507 Self {
508 expr,
509 alias: Some(alias),
510 }
511 }
512
513 /// Returns the output name (alias if present, otherwise derived from expression).
514 ///
515 /// Returns:
516 /// - The alias if one was specified (e.g., `SELECT name AS user_name`)
517 /// - The column name for simple column references (e.g., `SELECT name`)
518 /// - `None` for complex expressions without an alias (e.g., `SELECT 1 + 2`)
519 ///
520 /// Complex expressions (function calls, literals, binary operations) return `None`
521 /// because they don't have a natural name. Use [`with_alias`](Self::with_alias)
522 /// to give them an output name.
523 pub fn output_name(&self) -> Option<&str> {
524 if let Some(ref alias) = self.alias {
525 return Some(alias);
526 }
527 // For column references, return the column name
528 if let TypedExprKind::ColumnRef { ref column, .. } = self.expr.kind {
529 return Some(column);
530 }
531 None
532 }
533}
534
535impl Projection {
536 /// Returns the number of columns in the projection.
537 pub fn len(&self) -> usize {
538 match self {
539 Projection::All(cols) => cols.len(),
540 Projection::Columns(cols) => cols.len(),
541 }
542 }
543
544 /// Returns true if the projection has no columns.
545 pub fn is_empty(&self) -> bool {
546 self.len() == 0
547 }
548
549 /// Returns the column names in the projection.
550 ///
551 /// For [`Projection::All`], all names are present (from the wildcard expansion).
552 /// For [`Projection::Columns`], names may be `None` for complex expressions
553 /// without aliases. See [`ProjectedColumn::output_name`] for details.
554 pub fn column_names(&self) -> Vec<Option<&str>> {
555 match self {
556 Projection::All(cols) => cols.iter().map(|s| Some(s.as_str())).collect(),
557 Projection::Columns(cols) => cols.iter().map(|c| c.output_name()).collect(),
558 }
559 }
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565 use crate::ast::ddl::VectorMetric;
566
567 #[test]
568 fn test_typed_expr_literal() {
569 let expr = TypedExpr::literal(
570 Literal::Number("42".to_string()),
571 ResolvedType::Integer,
572 Span::default(),
573 );
574
575 assert!(matches!(
576 expr.kind,
577 TypedExprKind::Literal(Literal::Number(_))
578 ));
579 assert_eq!(expr.resolved_type, ResolvedType::Integer);
580 }
581
582 #[test]
583 fn test_typed_expr_column_ref() {
584 let expr = TypedExpr::column_ref(
585 "users".to_string(),
586 "id".to_string(),
587 0,
588 ResolvedType::Integer,
589 Span::default(),
590 );
591
592 if let TypedExprKind::ColumnRef {
593 table,
594 column,
595 column_index,
596 } = &expr.kind
597 {
598 assert_eq!(table, "users");
599 assert_eq!(column, "id");
600 assert_eq!(*column_index, 0);
601 } else {
602 panic!("Expected ColumnRef");
603 }
604 }
605
606 #[test]
607 fn test_typed_expr_binary_op() {
608 let left = TypedExpr::literal(
609 Literal::Number("1".to_string()),
610 ResolvedType::Integer,
611 Span::default(),
612 );
613 let right = TypedExpr::literal(
614 Literal::Number("2".to_string()),
615 ResolvedType::Integer,
616 Span::default(),
617 );
618
619 let expr = TypedExpr::binary_op(
620 left,
621 BinaryOp::Add,
622 right,
623 ResolvedType::Integer,
624 Span::default(),
625 );
626
627 assert!(matches!(expr.kind, TypedExprKind::BinaryOp { .. }));
628 assert_eq!(expr.resolved_type, ResolvedType::Integer);
629 }
630
631 #[test]
632 fn test_typed_expr_vector_literal() {
633 let values = vec![1.0, 2.0, 3.0];
634 let expr = TypedExpr::vector_literal(values.clone(), 3, Span::default());
635
636 if let TypedExprKind::VectorLiteral(v) = &expr.kind {
637 assert_eq!(v, &values);
638 } else {
639 panic!("Expected VectorLiteral");
640 }
641
642 if let ResolvedType::Vector { dimension, metric } = &expr.resolved_type {
643 assert_eq!(*dimension, 3);
644 assert_eq!(*metric, VectorMetric::Cosine);
645 } else {
646 panic!("Expected Vector type");
647 }
648 }
649
650 #[test]
651 fn test_sort_expr_asc() {
652 let col = TypedExpr::column_ref(
653 "users".to_string(),
654 "name".to_string(),
655 1,
656 ResolvedType::Text,
657 Span::default(),
658 );
659 let sort = SortExpr::asc(col);
660
661 assert!(sort.asc);
662 assert!(!sort.nulls_first);
663 }
664
665 #[test]
666 fn test_sort_expr_desc() {
667 let col = TypedExpr::column_ref(
668 "users".to_string(),
669 "name".to_string(),
670 1,
671 ResolvedType::Text,
672 Span::default(),
673 );
674 let sort = SortExpr::desc(col);
675
676 assert!(!sort.asc);
677 // NULLS LAST is the consistent default for both ASC and DESC
678 assert!(!sort.nulls_first);
679 }
680
681 #[test]
682 fn test_typed_assignment() {
683 let value = TypedExpr::literal(
684 Literal::String("Alice".to_string()),
685 ResolvedType::Text,
686 Span::default(),
687 );
688 let assignment = TypedAssignment::new("name".to_string(), 1, value);
689
690 assert_eq!(assignment.column, "name");
691 assert_eq!(assignment.column_index, 1);
692 }
693
694 #[test]
695 fn test_projected_column_output_name() {
696 let col = TypedExpr::column_ref(
697 "users".to_string(),
698 "name".to_string(),
699 1,
700 ResolvedType::Text,
701 Span::default(),
702 );
703
704 // Without alias, output name is the column name
705 let proj1 = ProjectedColumn::new(col.clone());
706 assert_eq!(proj1.output_name(), Some("name"));
707
708 // With alias, output name is the alias
709 let proj2 = ProjectedColumn::with_alias(col, "user_name".to_string());
710 assert_eq!(proj2.output_name(), Some("user_name"));
711 }
712
713 #[test]
714 fn test_projection_all() {
715 let columns = vec!["id".to_string(), "name".to_string(), "email".to_string()];
716 let proj = Projection::All(columns);
717
718 assert_eq!(proj.len(), 3);
719 assert!(!proj.is_empty());
720
721 let names: Vec<_> = proj.column_names();
722 assert_eq!(names, vec![Some("id"), Some("name"), Some("email")]);
723 }
724
725 #[test]
726 fn test_projection_columns() {
727 let col1 = ProjectedColumn::new(TypedExpr::column_ref(
728 "users".to_string(),
729 "id".to_string(),
730 0,
731 ResolvedType::Integer,
732 Span::default(),
733 ));
734 let col2 = ProjectedColumn::with_alias(
735 TypedExpr::column_ref(
736 "users".to_string(),
737 "name".to_string(),
738 1,
739 ResolvedType::Text,
740 Span::default(),
741 ),
742 "user_name".to_string(),
743 );
744
745 let proj = Projection::Columns(vec![col1, col2]);
746
747 assert_eq!(proj.len(), 2);
748 let names: Vec<_> = proj.column_names();
749 assert_eq!(names, vec![Some("id"), Some("user_name")]);
750 }
751
752 #[test]
753 fn test_typed_expr_cast() {
754 let inner = TypedExpr::literal(
755 Literal::Number("42".to_string()),
756 ResolvedType::Integer,
757 Span::default(),
758 );
759 let expr = TypedExpr::cast(inner, ResolvedType::Double, Span::default());
760
761 assert!(matches!(expr.kind, TypedExprKind::Cast { .. }));
762 assert_eq!(expr.resolved_type, ResolvedType::Double);
763 }
764
765 #[test]
766 fn test_typed_expr_kind_between() {
767 let expr_kind = TypedExprKind::Between {
768 expr: Box::new(TypedExpr::column_ref(
769 "t".to_string(),
770 "x".to_string(),
771 0,
772 ResolvedType::Integer,
773 Span::default(),
774 )),
775 low: Box::new(TypedExpr::literal(
776 Literal::Number("1".to_string()),
777 ResolvedType::Integer,
778 Span::default(),
779 )),
780 high: Box::new(TypedExpr::literal(
781 Literal::Number("10".to_string()),
782 ResolvedType::Integer,
783 Span::default(),
784 )),
785 negated: false,
786 };
787
788 assert!(matches!(
789 expr_kind,
790 TypedExprKind::Between { negated: false, .. }
791 ));
792 }
793
794 #[test]
795 fn test_typed_expr_kind_is_null() {
796 let expr_kind = TypedExprKind::IsNull {
797 expr: Box::new(TypedExpr::column_ref(
798 "t".to_string(),
799 "x".to_string(),
800 0,
801 ResolvedType::Integer,
802 Span::default(),
803 )),
804 negated: true,
805 };
806
807 assert!(matches!(
808 expr_kind,
809 TypedExprKind::IsNull { negated: true, .. }
810 ));
811 }
812}