1use crate::catalog::{IndexMetadata, TableMetadata};
47use crate::planner::aggregate_expr::AggregateExpr;
48use crate::planner::typed_expr::{Projection, SortExpr, TypedAssignment, TypedExpr};
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum JoinType {
53 Inner,
54 Left,
55 Right,
56 Full,
57 Cross,
58}
59
60#[derive(Debug, Clone)]
69pub enum LogicalPlan {
70 Pragma {
72 name: String,
74 value: Option<crate::ast::PragmaValue>,
76 },
77
78 Scan {
84 table: String,
86 projection: Projection,
88 },
89
90 Filter {
94 input: Box<LogicalPlan>,
96 predicate: TypedExpr,
98 },
99
100 Project {
106 input: Box<LogicalPlan>,
108 projection: Projection,
110 },
111
112 Join {
114 left: Box<LogicalPlan>,
116 right: Box<LogicalPlan>,
118 join_type: JoinType,
120 condition: Option<TypedExpr>,
122 using: Option<Vec<String>>,
124 },
125
126 Aggregate {
130 input: Box<LogicalPlan>,
132 group_keys: Vec<TypedExpr>,
134 aggregates: Vec<AggregateExpr>,
136 having: Option<TypedExpr>,
138 projection: Projection,
140 },
141
142 Sort {
146 input: Box<LogicalPlan>,
148 order_by: Vec<SortExpr>,
150 },
151
152 Limit {
156 input: Box<LogicalPlan>,
158 limit: Option<u64>,
160 offset: Option<u64>,
162 },
163
164 Insert {
171 table: String,
173 columns: Vec<String>,
176 values: Vec<Vec<TypedExpr>>,
178 },
179
180 Update {
184 table: String,
186 assignments: Vec<TypedAssignment>,
188 filter: Option<TypedExpr>,
190 },
191
192 Delete {
196 table: String,
198 filter: Option<TypedExpr>,
200 },
201
202 CreateTable {
207 table: TableMetadata,
209 if_not_exists: bool,
211 with_options: Vec<(String, String)>,
213 },
214
215 DropTable {
219 name: String,
221 if_exists: bool,
223 },
224
225 CreateIndex {
229 index: IndexMetadata,
231 if_not_exists: bool,
233 },
234
235 DropIndex {
239 name: String,
241 if_exists: bool,
243 },
244}
245
246impl LogicalPlan {
247 pub fn operation_name(&self) -> &'static str {
248 match self {
249 LogicalPlan::Pragma { .. } => "PRAGMA",
250 LogicalPlan::Scan { .. }
251 | LogicalPlan::Filter { .. }
252 | LogicalPlan::Project { .. }
253 | LogicalPlan::Join { .. }
254 | LogicalPlan::Aggregate { .. }
255 | LogicalPlan::Sort { .. }
256 | LogicalPlan::Limit { .. } => "SELECT",
257 LogicalPlan::Insert { .. } => "INSERT",
258 LogicalPlan::Update { .. } => "UPDATE",
259 LogicalPlan::Delete { .. } => "DELETE",
260 LogicalPlan::CreateTable { .. } => "CREATE TABLE",
261 LogicalPlan::DropTable { .. } => "DROP TABLE",
262 LogicalPlan::CreateIndex { .. } => "CREATE INDEX",
263 LogicalPlan::DropIndex { .. } => "DROP INDEX",
264 }
265 }
266
267 pub fn scan(table: String, projection: Projection) -> Self {
269 LogicalPlan::Scan { table, projection }
270 }
271
272 pub fn filter(input: LogicalPlan, predicate: TypedExpr) -> Self {
274 LogicalPlan::Filter {
275 input: Box::new(input),
276 predicate,
277 }
278 }
279
280 pub fn project(input: LogicalPlan, projection: Projection) -> Self {
282 LogicalPlan::Project {
283 input: Box::new(input),
284 projection,
285 }
286 }
287
288 pub fn join(
290 left: LogicalPlan,
291 right: LogicalPlan,
292 join_type: JoinType,
293 condition: Option<TypedExpr>,
294 using: Option<Vec<String>>,
295 ) -> Self {
296 LogicalPlan::Join {
297 left: Box::new(left),
298 right: Box::new(right),
299 join_type,
300 condition,
301 using,
302 }
303 }
304
305 pub fn aggregate(
307 input: LogicalPlan,
308 group_keys: Vec<TypedExpr>,
309 aggregates: Vec<AggregateExpr>,
310 having: Option<TypedExpr>,
311 projection: Projection,
312 ) -> Self {
313 LogicalPlan::Aggregate {
314 input: Box::new(input),
315 group_keys,
316 aggregates,
317 having,
318 projection,
319 }
320 }
321
322 pub fn sort(input: LogicalPlan, order_by: Vec<SortExpr>) -> Self {
324 LogicalPlan::Sort {
325 input: Box::new(input),
326 order_by,
327 }
328 }
329
330 pub fn limit(input: LogicalPlan, limit: Option<u64>, offset: Option<u64>) -> Self {
332 LogicalPlan::Limit {
333 input: Box::new(input),
334 limit,
335 offset,
336 }
337 }
338
339 pub fn insert(table: String, columns: Vec<String>, values: Vec<Vec<TypedExpr>>) -> Self {
341 LogicalPlan::Insert {
342 table,
343 columns,
344 values,
345 }
346 }
347
348 pub fn update(
350 table: String,
351 assignments: Vec<TypedAssignment>,
352 filter: Option<TypedExpr>,
353 ) -> Self {
354 LogicalPlan::Update {
355 table,
356 assignments,
357 filter,
358 }
359 }
360
361 pub fn delete(table: String, filter: Option<TypedExpr>) -> Self {
363 LogicalPlan::Delete { table, filter }
364 }
365
366 pub fn create_table(
368 table: TableMetadata,
369 if_not_exists: bool,
370 with_options: Vec<(String, String)>,
371 ) -> Self {
372 LogicalPlan::CreateTable {
373 table,
374 if_not_exists,
375 with_options,
376 }
377 }
378
379 pub fn drop_table(name: String, if_exists: bool) -> Self {
381 LogicalPlan::DropTable { name, if_exists }
382 }
383
384 pub fn create_index(index: IndexMetadata, if_not_exists: bool) -> Self {
386 LogicalPlan::CreateIndex {
387 index,
388 if_not_exists,
389 }
390 }
391
392 pub fn drop_index(name: String, if_exists: bool) -> Self {
394 LogicalPlan::DropIndex { name, if_exists }
395 }
396
397 pub fn name(&self) -> &'static str {
399 match self {
400 LogicalPlan::Pragma { .. } => "Pragma",
401 LogicalPlan::Scan { .. } => "Scan",
402 LogicalPlan::Filter { .. } => "Filter",
403 LogicalPlan::Project { .. } => "Project",
404 LogicalPlan::Join { .. } => "Join",
405 LogicalPlan::Aggregate { .. } => "Aggregate",
406 LogicalPlan::Sort { .. } => "Sort",
407 LogicalPlan::Limit { .. } => "Limit",
408 LogicalPlan::Insert { .. } => "Insert",
409 LogicalPlan::Update { .. } => "Update",
410 LogicalPlan::Delete { .. } => "Delete",
411 LogicalPlan::CreateTable { .. } => "CreateTable",
412 LogicalPlan::DropTable { .. } => "DropTable",
413 LogicalPlan::CreateIndex { .. } => "CreateIndex",
414 LogicalPlan::DropIndex { .. } => "DropIndex",
415 }
416 }
417
418 pub fn is_query(&self) -> bool {
420 matches!(
421 self,
422 LogicalPlan::Scan { .. }
423 | LogicalPlan::Filter { .. }
424 | LogicalPlan::Project { .. }
425 | LogicalPlan::Join { .. }
426 | LogicalPlan::Aggregate { .. }
427 | LogicalPlan::Sort { .. }
428 | LogicalPlan::Limit { .. }
429 )
430 }
431
432 pub fn is_dml(&self) -> bool {
434 matches!(
435 self,
436 LogicalPlan::Insert { .. } | LogicalPlan::Update { .. } | LogicalPlan::Delete { .. }
437 )
438 }
439
440 pub fn is_ddl(&self) -> bool {
442 matches!(
443 self,
444 LogicalPlan::CreateTable { .. }
445 | LogicalPlan::DropTable { .. }
446 | LogicalPlan::CreateIndex { .. }
447 | LogicalPlan::DropIndex { .. }
448 | LogicalPlan::Pragma { .. }
449 )
450 }
451
452 pub fn input(&self) -> Option<&LogicalPlan> {
454 match self {
455 LogicalPlan::Filter { input, .. }
456 | LogicalPlan::Project { input, .. }
457 | LogicalPlan::Aggregate { input, .. }
458 | LogicalPlan::Sort { input, .. }
459 | LogicalPlan::Limit { input, .. } => Some(input),
460 LogicalPlan::Join { .. } => None,
461 _ => None,
462 }
463 }
464
465 pub fn table_name(&self) -> Option<&str> {
467 match self {
468 LogicalPlan::Scan { table, .. }
469 | LogicalPlan::Insert { table, .. }
470 | LogicalPlan::Update { table, .. }
471 | LogicalPlan::Delete { table, .. } => Some(table),
472 LogicalPlan::CreateTable { table, .. } => Some(&table.name),
473 LogicalPlan::DropTable { name, .. } => Some(name),
474 LogicalPlan::CreateIndex { index, .. } => Some(&index.table),
475 LogicalPlan::DropIndex { .. } => None,
476 LogicalPlan::Pragma { .. } => None,
477 LogicalPlan::Filter { input, .. }
478 | LogicalPlan::Project { input, .. }
479 | LogicalPlan::Aggregate { input, .. }
480 | LogicalPlan::Sort { input, .. }
481 | LogicalPlan::Limit { input, .. } => input.table_name(),
482 LogicalPlan::Join { .. } => None,
483 }
484 }
485
486 pub fn contains_join(&self) -> bool {
493 match self {
494 LogicalPlan::Join { .. } => true,
495 LogicalPlan::Filter { input, .. }
496 | LogicalPlan::Project { input, .. }
497 | LogicalPlan::Aggregate { input, .. }
498 | LogicalPlan::Sort { input, .. }
499 | LogicalPlan::Limit { input, .. } => input.contains_join(),
500 _ => false,
501 }
502 }
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508 use crate::ast::expr::Literal;
509 use crate::ast::span::Span;
510 use crate::catalog::ColumnMetadata;
511 use crate::planner::typed_expr::ProjectedColumn;
512 use crate::planner::types::ResolvedType;
513
514 fn create_test_table_metadata() -> TableMetadata {
515 TableMetadata::new(
516 "users",
517 vec![
518 ColumnMetadata::new("id", ResolvedType::Integer)
519 .with_primary_key(true)
520 .with_not_null(true),
521 ColumnMetadata::new("name", ResolvedType::Text).with_not_null(true),
522 ColumnMetadata::new("email", ResolvedType::Text),
523 ],
524 )
525 .with_primary_key(vec!["id".to_string()])
526 }
527
528 #[test]
529 fn test_scan_plan() {
530 let plan = LogicalPlan::scan(
531 "users".to_string(),
532 Projection::All(vec![
533 "id".to_string(),
534 "name".to_string(),
535 "email".to_string(),
536 ]),
537 );
538
539 assert_eq!(plan.name(), "Scan");
540 assert!(plan.is_query());
541 assert!(!plan.is_dml());
542 assert!(!plan.is_ddl());
543 assert_eq!(plan.table_name(), Some("users"));
544 assert!(plan.input().is_none());
545 }
546
547 #[test]
548 fn test_filter_plan() {
549 let scan = LogicalPlan::scan("users".to_string(), Projection::All(vec![]));
550 let predicate = TypedExpr::column_ref(
551 "users".to_string(),
552 "id".to_string(),
553 0,
554 ResolvedType::Integer,
555 Span::default(),
556 );
557
558 let plan = LogicalPlan::filter(scan, predicate);
559
560 assert_eq!(plan.name(), "Filter");
561 assert!(plan.is_query());
562 assert!(plan.input().is_some());
563 assert_eq!(plan.table_name(), Some("users"));
564 }
565
566 #[test]
567 fn test_sort_plan() {
568 let scan = LogicalPlan::scan("users".to_string(), Projection::All(vec![]));
569 let sort_expr = SortExpr::asc(TypedExpr::column_ref(
570 "users".to_string(),
571 "name".to_string(),
572 1,
573 ResolvedType::Text,
574 Span::default(),
575 ));
576
577 let plan = LogicalPlan::sort(scan, vec![sort_expr]);
578
579 assert_eq!(plan.name(), "Sort");
580 assert!(plan.is_query());
581 }
582
583 #[test]
584 fn test_limit_plan() {
585 let scan = LogicalPlan::scan("users".to_string(), Projection::All(vec![]));
586 let plan = LogicalPlan::limit(scan, Some(10), Some(5));
587
588 assert_eq!(plan.name(), "Limit");
589 assert!(plan.is_query());
590
591 if let LogicalPlan::Limit { limit, offset, .. } = &plan {
592 assert_eq!(*limit, Some(10));
593 assert_eq!(*offset, Some(5));
594 } else {
595 panic!("Expected Limit plan");
596 }
597 }
598
599 #[test]
600 fn test_nested_query_plan() {
601 let scan = LogicalPlan::scan(
603 "users".to_string(),
604 Projection::All(vec!["id".to_string(), "name".to_string()]),
605 );
606
607 let predicate = TypedExpr::literal(
608 Literal::Boolean(true),
609 ResolvedType::Boolean,
610 Span::default(),
611 );
612 let filter = LogicalPlan::filter(scan, predicate);
613
614 let sort_expr = SortExpr::asc(TypedExpr::column_ref(
615 "users".to_string(),
616 "name".to_string(),
617 1,
618 ResolvedType::Text,
619 Span::default(),
620 ));
621 let sort = LogicalPlan::sort(filter, vec![sort_expr]);
622
623 let limit = LogicalPlan::limit(sort, Some(10), None);
624
625 assert_eq!(limit.name(), "Limit");
627 assert_eq!(limit.table_name(), Some("users"));
628
629 let sort_plan = limit.input().unwrap();
630 assert_eq!(sort_plan.name(), "Sort");
631
632 let filter_plan = sort_plan.input().unwrap();
633 assert_eq!(filter_plan.name(), "Filter");
634
635 let scan_plan = filter_plan.input().unwrap();
636 assert_eq!(scan_plan.name(), "Scan");
637 assert!(scan_plan.input().is_none());
638 }
639
640 #[test]
641 fn test_insert_plan() {
642 let value1 = TypedExpr::literal(
643 Literal::Number("1".to_string()),
644 ResolvedType::Integer,
645 Span::default(),
646 );
647 let value2 = TypedExpr::literal(
648 Literal::String("Alice".to_string()),
649 ResolvedType::Text,
650 Span::default(),
651 );
652
653 let plan = LogicalPlan::insert(
654 "users".to_string(),
655 vec!["id".to_string(), "name".to_string()],
656 vec![vec![value1, value2]],
657 );
658
659 assert_eq!(plan.name(), "Insert");
660 assert!(plan.is_dml());
661 assert!(!plan.is_query());
662 assert!(!plan.is_ddl());
663 assert_eq!(plan.table_name(), Some("users"));
664
665 if let LogicalPlan::Insert {
666 table,
667 columns,
668 values,
669 } = &plan
670 {
671 assert_eq!(table, "users");
672 assert_eq!(columns, &vec!["id".to_string(), "name".to_string()]);
673 assert_eq!(values.len(), 1);
674 assert_eq!(values[0].len(), 2);
675 } else {
676 panic!("Expected Insert plan");
677 }
678 }
679
680 #[test]
681 fn test_update_plan() {
682 let assignment = TypedAssignment::new(
683 "name".to_string(),
684 1,
685 TypedExpr::literal(
686 Literal::String("Bob".to_string()),
687 ResolvedType::Text,
688 Span::default(),
689 ),
690 );
691
692 let filter = TypedExpr::literal(
693 Literal::Boolean(true),
694 ResolvedType::Boolean,
695 Span::default(),
696 );
697
698 let plan = LogicalPlan::update("users".to_string(), vec![assignment], Some(filter));
699
700 assert_eq!(plan.name(), "Update");
701 assert!(plan.is_dml());
702 assert_eq!(plan.table_name(), Some("users"));
703 }
704
705 #[test]
706 fn test_delete_plan() {
707 let filter = TypedExpr::column_ref(
708 "users".to_string(),
709 "id".to_string(),
710 0,
711 ResolvedType::Integer,
712 Span::default(),
713 );
714
715 let plan = LogicalPlan::delete("users".to_string(), Some(filter));
716
717 assert_eq!(plan.name(), "Delete");
718 assert!(plan.is_dml());
719 assert_eq!(plan.table_name(), Some("users"));
720 }
721
722 #[test]
723 fn test_create_table_plan() {
724 let table = create_test_table_metadata();
725 let plan = LogicalPlan::create_table(table, false, vec![]);
726
727 assert_eq!(plan.name(), "CreateTable");
728 assert!(plan.is_ddl());
729 assert!(!plan.is_dml());
730 assert!(!plan.is_query());
731 assert_eq!(plan.table_name(), Some("users"));
732 }
733
734 #[test]
735 fn test_drop_table_plan() {
736 let plan = LogicalPlan::drop_table("users".to_string(), true);
737
738 assert_eq!(plan.name(), "DropTable");
739 assert!(plan.is_ddl());
740 assert_eq!(plan.table_name(), Some("users"));
741
742 if let LogicalPlan::DropTable { name, if_exists } = &plan {
743 assert_eq!(name, "users");
744 assert!(*if_exists);
745 } else {
746 panic!("Expected DropTable plan");
747 }
748 }
749
750 #[test]
751 fn test_create_index_plan() {
752 let index = IndexMetadata::new(0, "idx_users_name", "users", vec!["name".into()]);
753 let plan = LogicalPlan::create_index(index, false);
754
755 assert_eq!(plan.name(), "CreateIndex");
756 assert!(plan.is_ddl());
757 assert_eq!(plan.table_name(), Some("users"));
758 }
759
760 #[test]
761 fn test_drop_index_plan() {
762 let plan = LogicalPlan::drop_index("idx_users_name".to_string(), false);
763
764 assert_eq!(plan.name(), "DropIndex");
765 assert!(plan.is_ddl());
766 assert!(plan.table_name().is_none());
768 }
769
770 #[test]
771 fn test_projection_columns() {
772 let col1 = ProjectedColumn::new(TypedExpr::column_ref(
773 "users".to_string(),
774 "id".to_string(),
775 0,
776 ResolvedType::Integer,
777 Span::default(),
778 ));
779 let col2 = ProjectedColumn::with_alias(
780 TypedExpr::column_ref(
781 "users".to_string(),
782 "name".to_string(),
783 1,
784 ResolvedType::Text,
785 Span::default(),
786 ),
787 "user_name".to_string(),
788 );
789
790 let plan = LogicalPlan::scan("users".to_string(), Projection::Columns(vec![col1, col2]));
791
792 if let LogicalPlan::Scan { projection, .. } = &plan {
793 assert_eq!(projection.len(), 2);
794 } else {
795 panic!("Expected Scan plan");
796 }
797 }
798}