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