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