Skip to main content

alopex_sql/planner/
logical_plan.rs

1//! Logical plan representation for query execution.
2//!
3//! This module defines [`LogicalPlan`], which represents the logical structure
4//! of a query after parsing and semantic analysis. The logical plan is used
5//! by the executor to produce query results.
6//!
7//! # Plan Structure
8//!
9//! Logical plans form a tree structure where:
10//! - Leaf nodes are typically scans or DDL operations
11//! - Internal nodes represent transformations (filter, sort, limit)
12//! - DML operations (insert, update, delete) are also represented
13//!
14//! # Examples
15//!
16//! ```
17//! use alopex_sql::planner::logical_plan::LogicalPlan;
18//! use alopex_sql::planner::{Projection, TypedExpr, TypedExprKind, SortExpr};
19//! use alopex_sql::planner::types::ResolvedType;
20//! use alopex_sql::Span;
21//!
22//! // SELECT * FROM users ORDER BY name LIMIT 10
23//! let scan = LogicalPlan::Scan {
24//!     table: "users".to_string(),
25//!     projection: Projection::All(vec!["id".to_string(), "name".to_string()]),
26//! };
27//!
28//! let sort = LogicalPlan::Sort {
29//!     input: Box::new(scan),
30//!     order_by: vec![SortExpr::asc(TypedExpr::column_ref(
31//!         "users".to_string(),
32//!         "name".to_string(),
33//!         1,
34//!         ResolvedType::Text,
35//!         Span::default(),
36//!     ))],
37//! };
38//!
39//! let limit = LogicalPlan::Limit {
40//!     input: Box::new(sort),
41//!     limit: Some(10),
42//!     offset: None,
43//! };
44//! ```
45
46use crate::catalog::{IndexMetadata, TableMetadata};
47use crate::planner::aggregate_expr::AggregateExpr;
48use crate::planner::typed_expr::{Projection, SortExpr, TypedAssignment, TypedExpr};
49
50/// JOIN type for logical and physical execution.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum JoinType {
53    Inner,
54    Left,
55    Right,
56    Full,
57    Cross,
58}
59
60/// Logical query plan representation.
61///
62/// This enum represents all possible logical operations that can be performed.
63/// Plans are organized into three categories:
64///
65/// 1. **Query Plans**: Read operations (Scan, Filter, Sort, Limit)
66/// 2. **DML Plans**: Data modification (Insert, Update, Delete)
67/// 3. **DDL Plans**: Schema modification (CreateTable, DropTable, CreateIndex, DropIndex)
68#[derive(Debug, Clone)]
69pub enum LogicalPlan {
70    // === Query Plans ===
71    /// Table scan operation.
72    ///
73    /// Scans all rows from a table with the specified projection.
74    /// This is typically the leaf node of query plans.
75    Scan {
76        /// Table name to scan.
77        table: String,
78        /// Columns to project (after wildcard expansion).
79        projection: Projection,
80    },
81
82    /// Filter operation (WHERE clause).
83    ///
84    /// Filters rows from the input plan based on a predicate.
85    Filter {
86        /// Input plan to filter.
87        input: Box<LogicalPlan>,
88        /// Filter predicate (must evaluate to Boolean).
89        predicate: TypedExpr,
90    },
91
92    /// Projection boundary.
93    ///
94    /// Scan keeps the legacy single-table projection path; this node is used
95    /// when a relation-producing input such as JOIN or a derived table must be
96    /// materialized before being consumed by a parent query.
97    Project {
98        /// Input plan to project.
99        input: Box<LogicalPlan>,
100        /// Projection to apply.
101        projection: Projection,
102    },
103
104    /// JOIN operation.
105    Join {
106        /// Left input.
107        left: Box<LogicalPlan>,
108        /// Right input.
109        right: Box<LogicalPlan>,
110        /// Join type.
111        join_type: JoinType,
112        /// Optional ON condition.
113        condition: Option<TypedExpr>,
114        /// Optional USING columns.
115        using: Option<Vec<String>>,
116    },
117
118    /// Aggregate operation (GROUP BY / aggregation).
119    ///
120    /// Aggregates rows from the input plan using group keys and aggregate expressions.
121    Aggregate {
122        /// Input plan to aggregate.
123        input: Box<LogicalPlan>,
124        /// Group-by key expressions (empty for global aggregation).
125        group_keys: Vec<TypedExpr>,
126        /// Aggregate expressions to compute.
127        aggregates: Vec<AggregateExpr>,
128        /// HAVING filter applied after aggregation.
129        having: Option<TypedExpr>,
130        /// Projection to apply after aggregation.
131        projection: Projection,
132    },
133
134    /// Sort operation (ORDER BY clause).
135    ///
136    /// Sorts rows from the input plan based on sort expressions.
137    Sort {
138        /// Input plan to sort.
139        input: Box<LogicalPlan>,
140        /// Sort expressions with direction.
141        order_by: Vec<SortExpr>,
142    },
143
144    /// Limit operation (LIMIT/OFFSET clause).
145    ///
146    /// Limits the number of rows from the input plan.
147    Limit {
148        /// Input plan to limit.
149        input: Box<LogicalPlan>,
150        /// Maximum number of rows to return.
151        limit: Option<u64>,
152        /// Number of rows to skip.
153        offset: Option<u64>,
154    },
155
156    // === DML Plans ===
157    /// INSERT operation.
158    ///
159    /// Inserts one or more rows into a table.
160    /// When columns are omitted in the SQL statement, the Planner fills in
161    /// all columns from TableMetadata in definition order.
162    Insert {
163        /// Target table name.
164        table: String,
165        /// Column names (always populated, never empty).
166        /// If omitted in SQL, filled from TableMetadata.column_names().
167        columns: Vec<String>,
168        /// Values to insert (one Vec per row, each value corresponds to a column).
169        values: Vec<Vec<TypedExpr>>,
170    },
171
172    /// UPDATE operation.
173    ///
174    /// Updates rows in a table that match an optional filter.
175    Update {
176        /// Target table name.
177        table: String,
178        /// Assignments (SET column = value).
179        assignments: Vec<TypedAssignment>,
180        /// Optional filter predicate (WHERE clause).
181        filter: Option<TypedExpr>,
182    },
183
184    /// DELETE operation.
185    ///
186    /// Deletes rows from a table that match an optional filter.
187    Delete {
188        /// Target table name.
189        table: String,
190        /// Optional filter predicate (WHERE clause).
191        filter: Option<TypedExpr>,
192    },
193
194    // === DDL Plans ===
195    /// CREATE TABLE operation.
196    ///
197    /// Creates a new table with the specified metadata.
198    CreateTable {
199        /// Table metadata (name, columns, constraints).
200        table: TableMetadata,
201        /// If true, don't error if table already exists.
202        if_not_exists: bool,
203        /// Raw WITH options to be validated during execution.
204        with_options: Vec<(String, String)>,
205    },
206
207    /// DROP TABLE operation.
208    ///
209    /// Drops an existing table.
210    DropTable {
211        /// Table name to drop.
212        name: String,
213        /// If true, don't error if table doesn't exist.
214        if_exists: bool,
215    },
216
217    /// CREATE INDEX operation.
218    ///
219    /// Creates a new index on a table column.
220    CreateIndex {
221        /// Index metadata (name, table, column, method, options).
222        index: IndexMetadata,
223        /// If true, don't error if index already exists.
224        if_not_exists: bool,
225    },
226
227    /// DROP INDEX operation.
228    ///
229    /// Drops an existing index.
230    DropIndex {
231        /// Index name to drop.
232        name: String,
233        /// If true, don't error if index doesn't exist.
234        if_exists: bool,
235    },
236}
237
238impl LogicalPlan {
239    pub fn operation_name(&self) -> &'static str {
240        match self {
241            LogicalPlan::Scan { .. }
242            | LogicalPlan::Filter { .. }
243            | LogicalPlan::Project { .. }
244            | LogicalPlan::Join { .. }
245            | LogicalPlan::Aggregate { .. }
246            | LogicalPlan::Sort { .. }
247            | LogicalPlan::Limit { .. } => "SELECT",
248            LogicalPlan::Insert { .. } => "INSERT",
249            LogicalPlan::Update { .. } => "UPDATE",
250            LogicalPlan::Delete { .. } => "DELETE",
251            LogicalPlan::CreateTable { .. } => "CREATE TABLE",
252            LogicalPlan::DropTable { .. } => "DROP TABLE",
253            LogicalPlan::CreateIndex { .. } => "CREATE INDEX",
254            LogicalPlan::DropIndex { .. } => "DROP INDEX",
255        }
256    }
257
258    /// Creates a new Scan plan.
259    pub fn scan(table: String, projection: Projection) -> Self {
260        LogicalPlan::Scan { table, projection }
261    }
262
263    /// Creates a new Filter plan.
264    pub fn filter(input: LogicalPlan, predicate: TypedExpr) -> Self {
265        LogicalPlan::Filter {
266            input: Box::new(input),
267            predicate,
268        }
269    }
270
271    /// Creates a new Project plan.
272    pub fn project(input: LogicalPlan, projection: Projection) -> Self {
273        LogicalPlan::Project {
274            input: Box::new(input),
275            projection,
276        }
277    }
278
279    /// Creates a new Join plan.
280    pub fn join(
281        left: LogicalPlan,
282        right: LogicalPlan,
283        join_type: JoinType,
284        condition: Option<TypedExpr>,
285        using: Option<Vec<String>>,
286    ) -> Self {
287        LogicalPlan::Join {
288            left: Box::new(left),
289            right: Box::new(right),
290            join_type,
291            condition,
292            using,
293        }
294    }
295
296    /// Creates a new Aggregate plan.
297    pub fn aggregate(
298        input: LogicalPlan,
299        group_keys: Vec<TypedExpr>,
300        aggregates: Vec<AggregateExpr>,
301        having: Option<TypedExpr>,
302        projection: Projection,
303    ) -> Self {
304        LogicalPlan::Aggregate {
305            input: Box::new(input),
306            group_keys,
307            aggregates,
308            having,
309            projection,
310        }
311    }
312
313    /// Creates a new Sort plan.
314    pub fn sort(input: LogicalPlan, order_by: Vec<SortExpr>) -> Self {
315        LogicalPlan::Sort {
316            input: Box::new(input),
317            order_by,
318        }
319    }
320
321    /// Creates a new Limit plan.
322    pub fn limit(input: LogicalPlan, limit: Option<u64>, offset: Option<u64>) -> Self {
323        LogicalPlan::Limit {
324            input: Box::new(input),
325            limit,
326            offset,
327        }
328    }
329
330    /// Creates a new Insert plan.
331    pub fn insert(table: String, columns: Vec<String>, values: Vec<Vec<TypedExpr>>) -> Self {
332        LogicalPlan::Insert {
333            table,
334            columns,
335            values,
336        }
337    }
338
339    /// Creates a new Update plan.
340    pub fn update(
341        table: String,
342        assignments: Vec<TypedAssignment>,
343        filter: Option<TypedExpr>,
344    ) -> Self {
345        LogicalPlan::Update {
346            table,
347            assignments,
348            filter,
349        }
350    }
351
352    /// Creates a new Delete plan.
353    pub fn delete(table: String, filter: Option<TypedExpr>) -> Self {
354        LogicalPlan::Delete { table, filter }
355    }
356
357    /// Creates a new CreateTable plan.
358    pub fn create_table(
359        table: TableMetadata,
360        if_not_exists: bool,
361        with_options: Vec<(String, String)>,
362    ) -> Self {
363        LogicalPlan::CreateTable {
364            table,
365            if_not_exists,
366            with_options,
367        }
368    }
369
370    /// Creates a new DropTable plan.
371    pub fn drop_table(name: String, if_exists: bool) -> Self {
372        LogicalPlan::DropTable { name, if_exists }
373    }
374
375    /// Creates a new CreateIndex plan.
376    pub fn create_index(index: IndexMetadata, if_not_exists: bool) -> Self {
377        LogicalPlan::CreateIndex {
378            index,
379            if_not_exists,
380        }
381    }
382
383    /// Creates a new DropIndex plan.
384    pub fn drop_index(name: String, if_exists: bool) -> Self {
385        LogicalPlan::DropIndex { name, if_exists }
386    }
387
388    /// Returns the name of this plan variant.
389    pub fn name(&self) -> &'static str {
390        match self {
391            LogicalPlan::Scan { .. } => "Scan",
392            LogicalPlan::Filter { .. } => "Filter",
393            LogicalPlan::Project { .. } => "Project",
394            LogicalPlan::Join { .. } => "Join",
395            LogicalPlan::Aggregate { .. } => "Aggregate",
396            LogicalPlan::Sort { .. } => "Sort",
397            LogicalPlan::Limit { .. } => "Limit",
398            LogicalPlan::Insert { .. } => "Insert",
399            LogicalPlan::Update { .. } => "Update",
400            LogicalPlan::Delete { .. } => "Delete",
401            LogicalPlan::CreateTable { .. } => "CreateTable",
402            LogicalPlan::DropTable { .. } => "DropTable",
403            LogicalPlan::CreateIndex { .. } => "CreateIndex",
404            LogicalPlan::DropIndex { .. } => "DropIndex",
405        }
406    }
407
408    /// Returns true if this is a query plan (Scan, Filter, Sort, Limit).
409    pub fn is_query(&self) -> bool {
410        matches!(
411            self,
412            LogicalPlan::Scan { .. }
413                | LogicalPlan::Filter { .. }
414                | LogicalPlan::Project { .. }
415                | LogicalPlan::Join { .. }
416                | LogicalPlan::Aggregate { .. }
417                | LogicalPlan::Sort { .. }
418                | LogicalPlan::Limit { .. }
419        )
420    }
421
422    /// Returns true if this is a DML plan (Insert, Update, Delete).
423    pub fn is_dml(&self) -> bool {
424        matches!(
425            self,
426            LogicalPlan::Insert { .. } | LogicalPlan::Update { .. } | LogicalPlan::Delete { .. }
427        )
428    }
429
430    /// Returns true if this is a DDL plan (CreateTable, DropTable, CreateIndex, DropIndex).
431    pub fn is_ddl(&self) -> bool {
432        matches!(
433            self,
434            LogicalPlan::CreateTable { .. }
435                | LogicalPlan::DropTable { .. }
436                | LogicalPlan::CreateIndex { .. }
437                | LogicalPlan::DropIndex { .. }
438        )
439    }
440
441    /// Returns the input plan if this is a transformation (Filter, Aggregate, Sort, Limit).
442    pub fn input(&self) -> Option<&LogicalPlan> {
443        match self {
444            LogicalPlan::Filter { input, .. }
445            | LogicalPlan::Project { input, .. }
446            | LogicalPlan::Aggregate { input, .. }
447            | LogicalPlan::Sort { input, .. }
448            | LogicalPlan::Limit { input, .. } => Some(input),
449            LogicalPlan::Join { .. } => None,
450            _ => None,
451        }
452    }
453
454    /// Returns the table name if this plan operates on a single table.
455    pub fn table_name(&self) -> Option<&str> {
456        match self {
457            LogicalPlan::Scan { table, .. }
458            | LogicalPlan::Insert { table, .. }
459            | LogicalPlan::Update { table, .. }
460            | LogicalPlan::Delete { table, .. } => Some(table),
461            LogicalPlan::CreateTable { table, .. } => Some(&table.name),
462            LogicalPlan::DropTable { name, .. } => Some(name),
463            LogicalPlan::CreateIndex { index, .. } => Some(&index.table),
464            LogicalPlan::DropIndex { .. } => None,
465            LogicalPlan::Filter { input, .. }
466            | LogicalPlan::Project { input, .. }
467            | LogicalPlan::Aggregate { input, .. }
468            | LogicalPlan::Sort { input, .. }
469            | LogicalPlan::Limit { input, .. } => input.table_name(),
470            LogicalPlan::Join { .. } => None,
471        }
472    }
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478    use crate::ast::expr::Literal;
479    use crate::ast::span::Span;
480    use crate::catalog::ColumnMetadata;
481    use crate::planner::typed_expr::ProjectedColumn;
482    use crate::planner::types::ResolvedType;
483
484    fn create_test_table_metadata() -> TableMetadata {
485        TableMetadata::new(
486            "users",
487            vec![
488                ColumnMetadata::new("id", ResolvedType::Integer)
489                    .with_primary_key(true)
490                    .with_not_null(true),
491                ColumnMetadata::new("name", ResolvedType::Text).with_not_null(true),
492                ColumnMetadata::new("email", ResolvedType::Text),
493            ],
494        )
495        .with_primary_key(vec!["id".to_string()])
496    }
497
498    #[test]
499    fn test_scan_plan() {
500        let plan = LogicalPlan::scan(
501            "users".to_string(),
502            Projection::All(vec![
503                "id".to_string(),
504                "name".to_string(),
505                "email".to_string(),
506            ]),
507        );
508
509        assert_eq!(plan.name(), "Scan");
510        assert!(plan.is_query());
511        assert!(!plan.is_dml());
512        assert!(!plan.is_ddl());
513        assert_eq!(plan.table_name(), Some("users"));
514        assert!(plan.input().is_none());
515    }
516
517    #[test]
518    fn test_filter_plan() {
519        let scan = LogicalPlan::scan("users".to_string(), Projection::All(vec![]));
520        let predicate = TypedExpr::column_ref(
521            "users".to_string(),
522            "id".to_string(),
523            0,
524            ResolvedType::Integer,
525            Span::default(),
526        );
527
528        let plan = LogicalPlan::filter(scan, predicate);
529
530        assert_eq!(plan.name(), "Filter");
531        assert!(plan.is_query());
532        assert!(plan.input().is_some());
533        assert_eq!(plan.table_name(), Some("users"));
534    }
535
536    #[test]
537    fn test_sort_plan() {
538        let scan = LogicalPlan::scan("users".to_string(), Projection::All(vec![]));
539        let sort_expr = SortExpr::asc(TypedExpr::column_ref(
540            "users".to_string(),
541            "name".to_string(),
542            1,
543            ResolvedType::Text,
544            Span::default(),
545        ));
546
547        let plan = LogicalPlan::sort(scan, vec![sort_expr]);
548
549        assert_eq!(plan.name(), "Sort");
550        assert!(plan.is_query());
551    }
552
553    #[test]
554    fn test_limit_plan() {
555        let scan = LogicalPlan::scan("users".to_string(), Projection::All(vec![]));
556        let plan = LogicalPlan::limit(scan, Some(10), Some(5));
557
558        assert_eq!(plan.name(), "Limit");
559        assert!(plan.is_query());
560
561        if let LogicalPlan::Limit { limit, offset, .. } = &plan {
562            assert_eq!(*limit, Some(10));
563            assert_eq!(*offset, Some(5));
564        } else {
565            panic!("Expected Limit plan");
566        }
567    }
568
569    #[test]
570    fn test_nested_query_plan() {
571        // SELECT * FROM users WHERE id > 5 ORDER BY name LIMIT 10
572        let scan = LogicalPlan::scan(
573            "users".to_string(),
574            Projection::All(vec!["id".to_string(), "name".to_string()]),
575        );
576
577        let predicate = TypedExpr::literal(
578            Literal::Boolean(true),
579            ResolvedType::Boolean,
580            Span::default(),
581        );
582        let filter = LogicalPlan::filter(scan, predicate);
583
584        let sort_expr = SortExpr::asc(TypedExpr::column_ref(
585            "users".to_string(),
586            "name".to_string(),
587            1,
588            ResolvedType::Text,
589            Span::default(),
590        ));
591        let sort = LogicalPlan::sort(filter, vec![sort_expr]);
592
593        let limit = LogicalPlan::limit(sort, Some(10), None);
594
595        // Verify the plan tree
596        assert_eq!(limit.name(), "Limit");
597        assert_eq!(limit.table_name(), Some("users"));
598
599        let sort_plan = limit.input().unwrap();
600        assert_eq!(sort_plan.name(), "Sort");
601
602        let filter_plan = sort_plan.input().unwrap();
603        assert_eq!(filter_plan.name(), "Filter");
604
605        let scan_plan = filter_plan.input().unwrap();
606        assert_eq!(scan_plan.name(), "Scan");
607        assert!(scan_plan.input().is_none());
608    }
609
610    #[test]
611    fn test_insert_plan() {
612        let value1 = TypedExpr::literal(
613            Literal::Number("1".to_string()),
614            ResolvedType::Integer,
615            Span::default(),
616        );
617        let value2 = TypedExpr::literal(
618            Literal::String("Alice".to_string()),
619            ResolvedType::Text,
620            Span::default(),
621        );
622
623        let plan = LogicalPlan::insert(
624            "users".to_string(),
625            vec!["id".to_string(), "name".to_string()],
626            vec![vec![value1, value2]],
627        );
628
629        assert_eq!(plan.name(), "Insert");
630        assert!(plan.is_dml());
631        assert!(!plan.is_query());
632        assert!(!plan.is_ddl());
633        assert_eq!(plan.table_name(), Some("users"));
634
635        if let LogicalPlan::Insert {
636            table,
637            columns,
638            values,
639        } = &plan
640        {
641            assert_eq!(table, "users");
642            assert_eq!(columns, &vec!["id".to_string(), "name".to_string()]);
643            assert_eq!(values.len(), 1);
644            assert_eq!(values[0].len(), 2);
645        } else {
646            panic!("Expected Insert plan");
647        }
648    }
649
650    #[test]
651    fn test_update_plan() {
652        let assignment = TypedAssignment::new(
653            "name".to_string(),
654            1,
655            TypedExpr::literal(
656                Literal::String("Bob".to_string()),
657                ResolvedType::Text,
658                Span::default(),
659            ),
660        );
661
662        let filter = TypedExpr::literal(
663            Literal::Boolean(true),
664            ResolvedType::Boolean,
665            Span::default(),
666        );
667
668        let plan = LogicalPlan::update("users".to_string(), vec![assignment], Some(filter));
669
670        assert_eq!(plan.name(), "Update");
671        assert!(plan.is_dml());
672        assert_eq!(plan.table_name(), Some("users"));
673    }
674
675    #[test]
676    fn test_delete_plan() {
677        let filter = TypedExpr::column_ref(
678            "users".to_string(),
679            "id".to_string(),
680            0,
681            ResolvedType::Integer,
682            Span::default(),
683        );
684
685        let plan = LogicalPlan::delete("users".to_string(), Some(filter));
686
687        assert_eq!(plan.name(), "Delete");
688        assert!(plan.is_dml());
689        assert_eq!(plan.table_name(), Some("users"));
690    }
691
692    #[test]
693    fn test_create_table_plan() {
694        let table = create_test_table_metadata();
695        let plan = LogicalPlan::create_table(table, false, vec![]);
696
697        assert_eq!(plan.name(), "CreateTable");
698        assert!(plan.is_ddl());
699        assert!(!plan.is_dml());
700        assert!(!plan.is_query());
701        assert_eq!(plan.table_name(), Some("users"));
702    }
703
704    #[test]
705    fn test_drop_table_plan() {
706        let plan = LogicalPlan::drop_table("users".to_string(), true);
707
708        assert_eq!(plan.name(), "DropTable");
709        assert!(plan.is_ddl());
710        assert_eq!(plan.table_name(), Some("users"));
711
712        if let LogicalPlan::DropTable { name, if_exists } = &plan {
713            assert_eq!(name, "users");
714            assert!(*if_exists);
715        } else {
716            panic!("Expected DropTable plan");
717        }
718    }
719
720    #[test]
721    fn test_create_index_plan() {
722        let index = IndexMetadata::new(0, "idx_users_name", "users", vec!["name".into()]);
723        let plan = LogicalPlan::create_index(index, false);
724
725        assert_eq!(plan.name(), "CreateIndex");
726        assert!(plan.is_ddl());
727        assert_eq!(plan.table_name(), Some("users"));
728    }
729
730    #[test]
731    fn test_drop_index_plan() {
732        let plan = LogicalPlan::drop_index("idx_users_name".to_string(), false);
733
734        assert_eq!(plan.name(), "DropIndex");
735        assert!(plan.is_ddl());
736        // DropIndex doesn't have table_name directly
737        assert!(plan.table_name().is_none());
738    }
739
740    #[test]
741    fn test_projection_columns() {
742        let col1 = ProjectedColumn::new(TypedExpr::column_ref(
743            "users".to_string(),
744            "id".to_string(),
745            0,
746            ResolvedType::Integer,
747            Span::default(),
748        ));
749        let col2 = ProjectedColumn::with_alias(
750            TypedExpr::column_ref(
751                "users".to_string(),
752                "name".to_string(),
753                1,
754                ResolvedType::Text,
755                Span::default(),
756            ),
757            "user_name".to_string(),
758        );
759
760        let plan = LogicalPlan::scan("users".to_string(), Projection::Columns(vec![col1, col2]));
761
762        if let LogicalPlan::Scan { projection, .. } = &plan {
763            assert_eq!(projection.len(), 2);
764        } else {
765            panic!("Expected Scan plan");
766        }
767    }
768}