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 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}