quill-sql 0.3.1

An educational Rust relational database (RDBMS) inspired by CMU 15445
Documentation
use crate::error::{QuillSQLError, QuillSQLResult};

use crate::catalog::Catalog;
use crate::plan::logical_plan::{
    Analyze, LogicalPlan, OrderByExpr, TransactionModes, TransactionScope,
};
use crate::sql::ast;
use crate::sql::ast::Value;
use crate::utils::table_ref::TableReference;
use sqlparser::ast::{Ident, ObjectType};

pub struct PlannerContext<'a> {
    pub catalog: &'a Catalog,
}

pub struct LogicalPlanner<'a> {
    pub context: PlannerContext<'a>,
}
impl<'a> LogicalPlanner<'a> {
    pub fn plan(&mut self, stmt: &ast::Statement) -> QuillSQLResult<LogicalPlan> {
        match stmt {
            ast::Statement::CreateTable {
                name,
                columns,
                if_not_exists,
                engine,
                ..
            } => self.plan_create_table(name, columns, *if_not_exists, engine.as_deref()),
            ast::Statement::CreateIndex {
                name,
                table_name,
                using,
                columns,
                ..
            } => self.plan_create_index(name, table_name, using.as_ref(), columns),
            ast::Statement::Drop {
                object_type,
                if_exists,
                names,
                cascade,
                restrict: _,
                purge,
            } => match object_type {
                ObjectType::Table => self.plan_drop_table(names, *if_exists, *cascade, *purge),
                ObjectType::Index => self.plan_drop_index(names, *if_exists, *cascade, *purge),
                other => Err(QuillSQLError::NotSupport(format!(
                    "DROP {} is not supported",
                    other
                ))),
            },
            ast::Statement::Query(query) => self.plan_query(query),
            ast::Statement::Insert {
                table_name,
                columns,
                source,
                ..
            } => self.plan_insert(table_name, columns, source),
            ast::Statement::Update {
                table,
                assignments,
                selection,
                ..
            } => self.plan_update(table, assignments, selection),
            ast::Statement::Delete {
                tables,
                from,
                using,
                selection,
                returning,
            } => {
                if !tables.is_empty() {
                    return Err(QuillSQLError::Plan(
                        "DELETE with table aliases is not supported".to_string(),
                    ));
                }
                if using.is_some() {
                    return Err(QuillSQLError::Plan(
                        "DELETE USING clause is not supported".to_string(),
                    ));
                }
                if returning.is_some() {
                    return Err(QuillSQLError::Plan(
                        "DELETE RETURNING is not supported".to_string(),
                    ));
                }
                if from.len() != 1 {
                    return Err(QuillSQLError::Plan(
                        "DELETE must target exactly one table".to_string(),
                    ));
                }
                self.plan_delete(&from[0], selection)
            }
            ast::Statement::Explain { statement, .. } => self.plan_explain(statement),
            ast::Statement::StartTransaction { modes, .. } => self.plan_begin_transaction(modes),
            ast::Statement::Commit { .. } => Ok(LogicalPlan::CommitTransaction),
            ast::Statement::Rollback { .. } => Ok(LogicalPlan::RollbackTransaction),
            ast::Statement::SetTransaction {
                modes,
                snapshot,
                session,
            } => self.plan_set_transaction(*session, snapshot, modes),
            ast::Statement::Analyze {
                table_name,
                partitions,
                for_columns,
                columns,
                cache_metadata,
                noscan,
                compute_statistics: _,
            } => self.plan_analyze(
                table_name,
                partitions,
                *for_columns,
                columns,
                *cache_metadata,
                *noscan,
            ),
            _ => unimplemented!(),
        }
    }

    fn plan_begin_transaction(
        &self,
        modes: &[ast::TransactionMode],
    ) -> QuillSQLResult<LogicalPlan> {
        Ok(LogicalPlan::BeginTransaction(TransactionModes::from_modes(
            modes,
        )))
    }

    fn plan_set_transaction(
        &self,
        session_scope: bool,
        snapshot: &Option<Value>,
        modes: &[ast::TransactionMode],
    ) -> QuillSQLResult<LogicalPlan> {
        if snapshot.is_some() {
            return Err(QuillSQLError::Plan(
                "SET TRANSACTION SNAPSHOT is not supported".to_string(),
            ));
        };
        let logical_scope = if session_scope {
            TransactionScope::Session
        } else {
            TransactionScope::Transaction
        };
        Ok(LogicalPlan::SetTransaction {
            scope: logical_scope,
            modes: TransactionModes::from_modes(modes),
        })
    }

    pub fn bind_order_by_expr(&self, order_by: &ast::OrderByExpr) -> QuillSQLResult<OrderByExpr> {
        let expr = self.bind_expr(&order_by.expr)?;
        Ok(OrderByExpr {
            expr: Box::new(expr),
            asc: order_by.asc.unwrap_or(true),
            nulls_first: order_by.nulls_first.unwrap_or(false),
        })
    }

    pub fn bind_table_name(&self, table_name: &ast::ObjectName) -> QuillSQLResult<TableReference> {
        match table_name.0.as_slice() {
            [table] => Ok(TableReference::Bare {
                table: table.value.clone(),
            }),
            [schema, table] => Ok(TableReference::Partial {
                schema: schema.value.clone(),
                table: table.value.clone(),
            }),
            [catalog, schema, table] => Ok(TableReference::Full {
                catalog: catalog.value.clone(),
                schema: schema.value.clone(),
                table: table.value.clone(),
            }),
            _ => Err(QuillSQLError::Plan(format!(
                "Fail to plan table name: {}",
                table_name
            ))),
        }
    }

    fn plan_analyze(
        &self,
        table_name: &ast::ObjectName,
        partitions: &Option<Vec<ast::Expr>>,
        for_columns: bool,
        columns: &[Ident],
        cache_metadata: bool,
        noscan: bool,
    ) -> QuillSQLResult<LogicalPlan> {
        if partitions.as_ref().is_some_and(|p| !p.is_empty()) {
            return Err(QuillSQLError::Plan(
                "ANALYZE PARTITION is not supported".to_string(),
            ));
        }
        if for_columns || !columns.is_empty() {
            return Err(QuillSQLError::Plan(
                "ANALYZE FOR COLUMNS is not supported".to_string(),
            ));
        }
        if cache_metadata || noscan {
            return Err(QuillSQLError::Plan(
                "ANALYZE options NOSCAN/CACHE METADATA are not supported".to_string(),
            ));
        }
        let table_ref = self.bind_table_name(table_name)?;
        Ok(LogicalPlan::Analyze(Analyze { table: table_ref }))
    }
}