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