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
487#[cfg(test)]
488mod tests {
489 use super::*;
490 use crate::ast::expr::Literal;
491 use crate::ast::span::Span;
492 use crate::catalog::ColumnMetadata;
493 use crate::planner::typed_expr::ProjectedColumn;
494 use crate::planner::types::ResolvedType;
495
496 fn create_test_table_metadata() -> TableMetadata {
497 TableMetadata::new(
498 "users",
499 vec![
500 ColumnMetadata::new("id", ResolvedType::Integer)
501 .with_primary_key(true)
502 .with_not_null(true),
503 ColumnMetadata::new("name", ResolvedType::Text).with_not_null(true),
504 ColumnMetadata::new("email", ResolvedType::Text),
505 ],
506 )
507 .with_primary_key(vec!["id".to_string()])
508 }
509
510 #[test]
511 fn test_scan_plan() {
512 let plan = LogicalPlan::scan(
513 "users".to_string(),
514 Projection::All(vec![
515 "id".to_string(),
516 "name".to_string(),
517 "email".to_string(),
518 ]),
519 );
520
521 assert_eq!(plan.name(), "Scan");
522 assert!(plan.is_query());
523 assert!(!plan.is_dml());
524 assert!(!plan.is_ddl());
525 assert_eq!(plan.table_name(), Some("users"));
526 assert!(plan.input().is_none());
527 }
528
529 #[test]
530 fn test_filter_plan() {
531 let scan = LogicalPlan::scan("users".to_string(), Projection::All(vec![]));
532 let predicate = TypedExpr::column_ref(
533 "users".to_string(),
534 "id".to_string(),
535 0,
536 ResolvedType::Integer,
537 Span::default(),
538 );
539
540 let plan = LogicalPlan::filter(scan, predicate);
541
542 assert_eq!(plan.name(), "Filter");
543 assert!(plan.is_query());
544 assert!(plan.input().is_some());
545 assert_eq!(plan.table_name(), Some("users"));
546 }
547
548 #[test]
549 fn test_sort_plan() {
550 let scan = LogicalPlan::scan("users".to_string(), Projection::All(vec![]));
551 let sort_expr = SortExpr::asc(TypedExpr::column_ref(
552 "users".to_string(),
553 "name".to_string(),
554 1,
555 ResolvedType::Text,
556 Span::default(),
557 ));
558
559 let plan = LogicalPlan::sort(scan, vec![sort_expr]);
560
561 assert_eq!(plan.name(), "Sort");
562 assert!(plan.is_query());
563 }
564
565 #[test]
566 fn test_limit_plan() {
567 let scan = LogicalPlan::scan("users".to_string(), Projection::All(vec![]));
568 let plan = LogicalPlan::limit(scan, Some(10), Some(5));
569
570 assert_eq!(plan.name(), "Limit");
571 assert!(plan.is_query());
572
573 if let LogicalPlan::Limit { limit, offset, .. } = &plan {
574 assert_eq!(*limit, Some(10));
575 assert_eq!(*offset, Some(5));
576 } else {
577 panic!("Expected Limit plan");
578 }
579 }
580
581 #[test]
582 fn test_nested_query_plan() {
583 let scan = LogicalPlan::scan(
585 "users".to_string(),
586 Projection::All(vec!["id".to_string(), "name".to_string()]),
587 );
588
589 let predicate = TypedExpr::literal(
590 Literal::Boolean(true),
591 ResolvedType::Boolean,
592 Span::default(),
593 );
594 let filter = LogicalPlan::filter(scan, predicate);
595
596 let sort_expr = SortExpr::asc(TypedExpr::column_ref(
597 "users".to_string(),
598 "name".to_string(),
599 1,
600 ResolvedType::Text,
601 Span::default(),
602 ));
603 let sort = LogicalPlan::sort(filter, vec![sort_expr]);
604
605 let limit = LogicalPlan::limit(sort, Some(10), None);
606
607 assert_eq!(limit.name(), "Limit");
609 assert_eq!(limit.table_name(), Some("users"));
610
611 let sort_plan = limit.input().unwrap();
612 assert_eq!(sort_plan.name(), "Sort");
613
614 let filter_plan = sort_plan.input().unwrap();
615 assert_eq!(filter_plan.name(), "Filter");
616
617 let scan_plan = filter_plan.input().unwrap();
618 assert_eq!(scan_plan.name(), "Scan");
619 assert!(scan_plan.input().is_none());
620 }
621
622 #[test]
623 fn test_insert_plan() {
624 let value1 = TypedExpr::literal(
625 Literal::Number("1".to_string()),
626 ResolvedType::Integer,
627 Span::default(),
628 );
629 let value2 = TypedExpr::literal(
630 Literal::String("Alice".to_string()),
631 ResolvedType::Text,
632 Span::default(),
633 );
634
635 let plan = LogicalPlan::insert(
636 "users".to_string(),
637 vec!["id".to_string(), "name".to_string()],
638 vec![vec![value1, value2]],
639 );
640
641 assert_eq!(plan.name(), "Insert");
642 assert!(plan.is_dml());
643 assert!(!plan.is_query());
644 assert!(!plan.is_ddl());
645 assert_eq!(plan.table_name(), Some("users"));
646
647 if let LogicalPlan::Insert {
648 table,
649 columns,
650 values,
651 } = &plan
652 {
653 assert_eq!(table, "users");
654 assert_eq!(columns, &vec!["id".to_string(), "name".to_string()]);
655 assert_eq!(values.len(), 1);
656 assert_eq!(values[0].len(), 2);
657 } else {
658 panic!("Expected Insert plan");
659 }
660 }
661
662 #[test]
663 fn test_update_plan() {
664 let assignment = TypedAssignment::new(
665 "name".to_string(),
666 1,
667 TypedExpr::literal(
668 Literal::String("Bob".to_string()),
669 ResolvedType::Text,
670 Span::default(),
671 ),
672 );
673
674 let filter = TypedExpr::literal(
675 Literal::Boolean(true),
676 ResolvedType::Boolean,
677 Span::default(),
678 );
679
680 let plan = LogicalPlan::update("users".to_string(), vec![assignment], Some(filter));
681
682 assert_eq!(plan.name(), "Update");
683 assert!(plan.is_dml());
684 assert_eq!(plan.table_name(), Some("users"));
685 }
686
687 #[test]
688 fn test_delete_plan() {
689 let filter = TypedExpr::column_ref(
690 "users".to_string(),
691 "id".to_string(),
692 0,
693 ResolvedType::Integer,
694 Span::default(),
695 );
696
697 let plan = LogicalPlan::delete("users".to_string(), Some(filter));
698
699 assert_eq!(plan.name(), "Delete");
700 assert!(plan.is_dml());
701 assert_eq!(plan.table_name(), Some("users"));
702 }
703
704 #[test]
705 fn test_create_table_plan() {
706 let table = create_test_table_metadata();
707 let plan = LogicalPlan::create_table(table, false, vec![]);
708
709 assert_eq!(plan.name(), "CreateTable");
710 assert!(plan.is_ddl());
711 assert!(!plan.is_dml());
712 assert!(!plan.is_query());
713 assert_eq!(plan.table_name(), Some("users"));
714 }
715
716 #[test]
717 fn test_drop_table_plan() {
718 let plan = LogicalPlan::drop_table("users".to_string(), true);
719
720 assert_eq!(plan.name(), "DropTable");
721 assert!(plan.is_ddl());
722 assert_eq!(plan.table_name(), Some("users"));
723
724 if let LogicalPlan::DropTable { name, if_exists } = &plan {
725 assert_eq!(name, "users");
726 assert!(*if_exists);
727 } else {
728 panic!("Expected DropTable plan");
729 }
730 }
731
732 #[test]
733 fn test_create_index_plan() {
734 let index = IndexMetadata::new(0, "idx_users_name", "users", vec!["name".into()]);
735 let plan = LogicalPlan::create_index(index, false);
736
737 assert_eq!(plan.name(), "CreateIndex");
738 assert!(plan.is_ddl());
739 assert_eq!(plan.table_name(), Some("users"));
740 }
741
742 #[test]
743 fn test_drop_index_plan() {
744 let plan = LogicalPlan::drop_index("idx_users_name".to_string(), false);
745
746 assert_eq!(plan.name(), "DropIndex");
747 assert!(plan.is_ddl());
748 assert!(plan.table_name().is_none());
750 }
751
752 #[test]
753 fn test_projection_columns() {
754 let col1 = ProjectedColumn::new(TypedExpr::column_ref(
755 "users".to_string(),
756 "id".to_string(),
757 0,
758 ResolvedType::Integer,
759 Span::default(),
760 ));
761 let col2 = ProjectedColumn::with_alias(
762 TypedExpr::column_ref(
763 "users".to_string(),
764 "name".to_string(),
765 1,
766 ResolvedType::Text,
767 Span::default(),
768 ),
769 "user_name".to_string(),
770 );
771
772 let plan = LogicalPlan::scan("users".to_string(), Projection::Columns(vec![col1, col2]));
773
774 if let LogicalPlan::Scan { projection, .. } = &plan {
775 assert_eq!(projection.len(), 2);
776 } else {
777 panic!("Expected Scan plan");
778 }
779 }
780}