alopex_sql/planner/
typed_expr.rs

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