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