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