Skip to main content

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}