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