use crate::catalog::{IndexMetadata, TableMetadata};
use crate::planner::aggregate_expr::AggregateExpr;
use crate::planner::typed_expr::{Projection, SortExpr, TypedAssignment, TypedExpr};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JoinType {
Inner,
Left,
Right,
Full,
Cross,
}
#[derive(Debug, Clone)]
pub enum LogicalPlan {
Scan {
table: String,
projection: Projection,
},
Filter {
input: Box<LogicalPlan>,
predicate: TypedExpr,
},
Project {
input: Box<LogicalPlan>,
projection: Projection,
},
Join {
left: Box<LogicalPlan>,
right: Box<LogicalPlan>,
join_type: JoinType,
condition: Option<TypedExpr>,
using: Option<Vec<String>>,
},
Aggregate {
input: Box<LogicalPlan>,
group_keys: Vec<TypedExpr>,
aggregates: Vec<AggregateExpr>,
having: Option<TypedExpr>,
projection: Projection,
},
Sort {
input: Box<LogicalPlan>,
order_by: Vec<SortExpr>,
},
Limit {
input: Box<LogicalPlan>,
limit: Option<u64>,
offset: Option<u64>,
},
Insert {
table: String,
columns: Vec<String>,
values: Vec<Vec<TypedExpr>>,
},
Update {
table: String,
assignments: Vec<TypedAssignment>,
filter: Option<TypedExpr>,
},
Delete {
table: String,
filter: Option<TypedExpr>,
},
CreateTable {
table: TableMetadata,
if_not_exists: bool,
with_options: Vec<(String, String)>,
},
DropTable {
name: String,
if_exists: bool,
},
CreateIndex {
index: IndexMetadata,
if_not_exists: bool,
},
DropIndex {
name: String,
if_exists: bool,
},
}
impl LogicalPlan {
pub fn operation_name(&self) -> &'static str {
match self {
LogicalPlan::Scan { .. }
| LogicalPlan::Filter { .. }
| LogicalPlan::Project { .. }
| LogicalPlan::Join { .. }
| LogicalPlan::Aggregate { .. }
| LogicalPlan::Sort { .. }
| LogicalPlan::Limit { .. } => "SELECT",
LogicalPlan::Insert { .. } => "INSERT",
LogicalPlan::Update { .. } => "UPDATE",
LogicalPlan::Delete { .. } => "DELETE",
LogicalPlan::CreateTable { .. } => "CREATE TABLE",
LogicalPlan::DropTable { .. } => "DROP TABLE",
LogicalPlan::CreateIndex { .. } => "CREATE INDEX",
LogicalPlan::DropIndex { .. } => "DROP INDEX",
}
}
pub fn scan(table: String, projection: Projection) -> Self {
LogicalPlan::Scan { table, projection }
}
pub fn filter(input: LogicalPlan, predicate: TypedExpr) -> Self {
LogicalPlan::Filter {
input: Box::new(input),
predicate,
}
}
pub fn project(input: LogicalPlan, projection: Projection) -> Self {
LogicalPlan::Project {
input: Box::new(input),
projection,
}
}
pub fn join(
left: LogicalPlan,
right: LogicalPlan,
join_type: JoinType,
condition: Option<TypedExpr>,
using: Option<Vec<String>>,
) -> Self {
LogicalPlan::Join {
left: Box::new(left),
right: Box::new(right),
join_type,
condition,
using,
}
}
pub fn aggregate(
input: LogicalPlan,
group_keys: Vec<TypedExpr>,
aggregates: Vec<AggregateExpr>,
having: Option<TypedExpr>,
projection: Projection,
) -> Self {
LogicalPlan::Aggregate {
input: Box::new(input),
group_keys,
aggregates,
having,
projection,
}
}
pub fn sort(input: LogicalPlan, order_by: Vec<SortExpr>) -> Self {
LogicalPlan::Sort {
input: Box::new(input),
order_by,
}
}
pub fn limit(input: LogicalPlan, limit: Option<u64>, offset: Option<u64>) -> Self {
LogicalPlan::Limit {
input: Box::new(input),
limit,
offset,
}
}
pub fn insert(table: String, columns: Vec<String>, values: Vec<Vec<TypedExpr>>) -> Self {
LogicalPlan::Insert {
table,
columns,
values,
}
}
pub fn update(
table: String,
assignments: Vec<TypedAssignment>,
filter: Option<TypedExpr>,
) -> Self {
LogicalPlan::Update {
table,
assignments,
filter,
}
}
pub fn delete(table: String, filter: Option<TypedExpr>) -> Self {
LogicalPlan::Delete { table, filter }
}
pub fn create_table(
table: TableMetadata,
if_not_exists: bool,
with_options: Vec<(String, String)>,
) -> Self {
LogicalPlan::CreateTable {
table,
if_not_exists,
with_options,
}
}
pub fn drop_table(name: String, if_exists: bool) -> Self {
LogicalPlan::DropTable { name, if_exists }
}
pub fn create_index(index: IndexMetadata, if_not_exists: bool) -> Self {
LogicalPlan::CreateIndex {
index,
if_not_exists,
}
}
pub fn drop_index(name: String, if_exists: bool) -> Self {
LogicalPlan::DropIndex { name, if_exists }
}
pub fn name(&self) -> &'static str {
match self {
LogicalPlan::Scan { .. } => "Scan",
LogicalPlan::Filter { .. } => "Filter",
LogicalPlan::Project { .. } => "Project",
LogicalPlan::Join { .. } => "Join",
LogicalPlan::Aggregate { .. } => "Aggregate",
LogicalPlan::Sort { .. } => "Sort",
LogicalPlan::Limit { .. } => "Limit",
LogicalPlan::Insert { .. } => "Insert",
LogicalPlan::Update { .. } => "Update",
LogicalPlan::Delete { .. } => "Delete",
LogicalPlan::CreateTable { .. } => "CreateTable",
LogicalPlan::DropTable { .. } => "DropTable",
LogicalPlan::CreateIndex { .. } => "CreateIndex",
LogicalPlan::DropIndex { .. } => "DropIndex",
}
}
pub fn is_query(&self) -> bool {
matches!(
self,
LogicalPlan::Scan { .. }
| LogicalPlan::Filter { .. }
| LogicalPlan::Project { .. }
| LogicalPlan::Join { .. }
| LogicalPlan::Aggregate { .. }
| LogicalPlan::Sort { .. }
| LogicalPlan::Limit { .. }
)
}
pub fn is_dml(&self) -> bool {
matches!(
self,
LogicalPlan::Insert { .. } | LogicalPlan::Update { .. } | LogicalPlan::Delete { .. }
)
}
pub fn is_ddl(&self) -> bool {
matches!(
self,
LogicalPlan::CreateTable { .. }
| LogicalPlan::DropTable { .. }
| LogicalPlan::CreateIndex { .. }
| LogicalPlan::DropIndex { .. }
)
}
pub fn input(&self) -> Option<&LogicalPlan> {
match self {
LogicalPlan::Filter { input, .. }
| LogicalPlan::Project { input, .. }
| LogicalPlan::Aggregate { input, .. }
| LogicalPlan::Sort { input, .. }
| LogicalPlan::Limit { input, .. } => Some(input),
LogicalPlan::Join { .. } => None,
_ => None,
}
}
pub fn table_name(&self) -> Option<&str> {
match self {
LogicalPlan::Scan { table, .. }
| LogicalPlan::Insert { table, .. }
| LogicalPlan::Update { table, .. }
| LogicalPlan::Delete { table, .. } => Some(table),
LogicalPlan::CreateTable { table, .. } => Some(&table.name),
LogicalPlan::DropTable { name, .. } => Some(name),
LogicalPlan::CreateIndex { index, .. } => Some(&index.table),
LogicalPlan::DropIndex { .. } => None,
LogicalPlan::Filter { input, .. }
| LogicalPlan::Project { input, .. }
| LogicalPlan::Aggregate { input, .. }
| LogicalPlan::Sort { input, .. }
| LogicalPlan::Limit { input, .. } => input.table_name(),
LogicalPlan::Join { .. } => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::expr::Literal;
use crate::ast::span::Span;
use crate::catalog::ColumnMetadata;
use crate::planner::typed_expr::ProjectedColumn;
use crate::planner::types::ResolvedType;
fn create_test_table_metadata() -> TableMetadata {
TableMetadata::new(
"users",
vec![
ColumnMetadata::new("id", ResolvedType::Integer)
.with_primary_key(true)
.with_not_null(true),
ColumnMetadata::new("name", ResolvedType::Text).with_not_null(true),
ColumnMetadata::new("email", ResolvedType::Text),
],
)
.with_primary_key(vec!["id".to_string()])
}
#[test]
fn test_scan_plan() {
let plan = LogicalPlan::scan(
"users".to_string(),
Projection::All(vec![
"id".to_string(),
"name".to_string(),
"email".to_string(),
]),
);
assert_eq!(plan.name(), "Scan");
assert!(plan.is_query());
assert!(!plan.is_dml());
assert!(!plan.is_ddl());
assert_eq!(plan.table_name(), Some("users"));
assert!(plan.input().is_none());
}
#[test]
fn test_filter_plan() {
let scan = LogicalPlan::scan("users".to_string(), Projection::All(vec![]));
let predicate = TypedExpr::column_ref(
"users".to_string(),
"id".to_string(),
0,
ResolvedType::Integer,
Span::default(),
);
let plan = LogicalPlan::filter(scan, predicate);
assert_eq!(plan.name(), "Filter");
assert!(plan.is_query());
assert!(plan.input().is_some());
assert_eq!(plan.table_name(), Some("users"));
}
#[test]
fn test_sort_plan() {
let scan = LogicalPlan::scan("users".to_string(), Projection::All(vec![]));
let sort_expr = SortExpr::asc(TypedExpr::column_ref(
"users".to_string(),
"name".to_string(),
1,
ResolvedType::Text,
Span::default(),
));
let plan = LogicalPlan::sort(scan, vec![sort_expr]);
assert_eq!(plan.name(), "Sort");
assert!(plan.is_query());
}
#[test]
fn test_limit_plan() {
let scan = LogicalPlan::scan("users".to_string(), Projection::All(vec![]));
let plan = LogicalPlan::limit(scan, Some(10), Some(5));
assert_eq!(plan.name(), "Limit");
assert!(plan.is_query());
if let LogicalPlan::Limit { limit, offset, .. } = &plan {
assert_eq!(*limit, Some(10));
assert_eq!(*offset, Some(5));
} else {
panic!("Expected Limit plan");
}
}
#[test]
fn test_nested_query_plan() {
let scan = LogicalPlan::scan(
"users".to_string(),
Projection::All(vec!["id".to_string(), "name".to_string()]),
);
let predicate = TypedExpr::literal(
Literal::Boolean(true),
ResolvedType::Boolean,
Span::default(),
);
let filter = LogicalPlan::filter(scan, predicate);
let sort_expr = SortExpr::asc(TypedExpr::column_ref(
"users".to_string(),
"name".to_string(),
1,
ResolvedType::Text,
Span::default(),
));
let sort = LogicalPlan::sort(filter, vec![sort_expr]);
let limit = LogicalPlan::limit(sort, Some(10), None);
assert_eq!(limit.name(), "Limit");
assert_eq!(limit.table_name(), Some("users"));
let sort_plan = limit.input().unwrap();
assert_eq!(sort_plan.name(), "Sort");
let filter_plan = sort_plan.input().unwrap();
assert_eq!(filter_plan.name(), "Filter");
let scan_plan = filter_plan.input().unwrap();
assert_eq!(scan_plan.name(), "Scan");
assert!(scan_plan.input().is_none());
}
#[test]
fn test_insert_plan() {
let value1 = TypedExpr::literal(
Literal::Number("1".to_string()),
ResolvedType::Integer,
Span::default(),
);
let value2 = TypedExpr::literal(
Literal::String("Alice".to_string()),
ResolvedType::Text,
Span::default(),
);
let plan = LogicalPlan::insert(
"users".to_string(),
vec!["id".to_string(), "name".to_string()],
vec![vec![value1, value2]],
);
assert_eq!(plan.name(), "Insert");
assert!(plan.is_dml());
assert!(!plan.is_query());
assert!(!plan.is_ddl());
assert_eq!(plan.table_name(), Some("users"));
if let LogicalPlan::Insert {
table,
columns,
values,
} = &plan
{
assert_eq!(table, "users");
assert_eq!(columns, &vec!["id".to_string(), "name".to_string()]);
assert_eq!(values.len(), 1);
assert_eq!(values[0].len(), 2);
} else {
panic!("Expected Insert plan");
}
}
#[test]
fn test_update_plan() {
let assignment = TypedAssignment::new(
"name".to_string(),
1,
TypedExpr::literal(
Literal::String("Bob".to_string()),
ResolvedType::Text,
Span::default(),
),
);
let filter = TypedExpr::literal(
Literal::Boolean(true),
ResolvedType::Boolean,
Span::default(),
);
let plan = LogicalPlan::update("users".to_string(), vec![assignment], Some(filter));
assert_eq!(plan.name(), "Update");
assert!(plan.is_dml());
assert_eq!(plan.table_name(), Some("users"));
}
#[test]
fn test_delete_plan() {
let filter = TypedExpr::column_ref(
"users".to_string(),
"id".to_string(),
0,
ResolvedType::Integer,
Span::default(),
);
let plan = LogicalPlan::delete("users".to_string(), Some(filter));
assert_eq!(plan.name(), "Delete");
assert!(plan.is_dml());
assert_eq!(plan.table_name(), Some("users"));
}
#[test]
fn test_create_table_plan() {
let table = create_test_table_metadata();
let plan = LogicalPlan::create_table(table, false, vec![]);
assert_eq!(plan.name(), "CreateTable");
assert!(plan.is_ddl());
assert!(!plan.is_dml());
assert!(!plan.is_query());
assert_eq!(plan.table_name(), Some("users"));
}
#[test]
fn test_drop_table_plan() {
let plan = LogicalPlan::drop_table("users".to_string(), true);
assert_eq!(plan.name(), "DropTable");
assert!(plan.is_ddl());
assert_eq!(plan.table_name(), Some("users"));
if let LogicalPlan::DropTable { name, if_exists } = &plan {
assert_eq!(name, "users");
assert!(*if_exists);
} else {
panic!("Expected DropTable plan");
}
}
#[test]
fn test_create_index_plan() {
let index = IndexMetadata::new(0, "idx_users_name", "users", vec!["name".into()]);
let plan = LogicalPlan::create_index(index, false);
assert_eq!(plan.name(), "CreateIndex");
assert!(plan.is_ddl());
assert_eq!(plan.table_name(), Some("users"));
}
#[test]
fn test_drop_index_plan() {
let plan = LogicalPlan::drop_index("idx_users_name".to_string(), false);
assert_eq!(plan.name(), "DropIndex");
assert!(plan.is_ddl());
assert!(plan.table_name().is_none());
}
#[test]
fn test_projection_columns() {
let col1 = ProjectedColumn::new(TypedExpr::column_ref(
"users".to_string(),
"id".to_string(),
0,
ResolvedType::Integer,
Span::default(),
));
let col2 = ProjectedColumn::with_alias(
TypedExpr::column_ref(
"users".to_string(),
"name".to_string(),
1,
ResolvedType::Text,
Span::default(),
),
"user_name".to_string(),
);
let plan = LogicalPlan::scan("users".to_string(), Projection::Columns(vec![col1, col2]));
if let LogicalPlan::Scan { projection, .. } = &plan {
assert_eq!(projection.len(), 2);
} else {
panic!("Expected Scan plan");
}
}
}