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