#[cfg(test)]
mod tests;
use wasm_dbms::prelude::{DatabaseSchema, DbmsContext, WasmDbmsDatabase};
use wasm_dbms_api::prelude::{
AggregatedRow, AggregatedValue, ColumnDef, Database as _, JoinColumnDef, JoinResultSet,
SqlError, SqlResult, SqlRow, TransactionId, Value,
};
use wasm_dbms_memory::prelude::MemoryProvider;
use crate::ast::Statement;
use crate::parser::parse;
use crate::planner::{self, AggregateOutput, AggregateSource, OutputColumn, SelectPlan};
pub struct SqlEngine<S> {
schema: S,
}
impl<S> SqlEngine<S> {
pub fn new(schema: S) -> Self {
Self { schema }
}
pub fn execute<'ctx, M>(
&self,
ctx: &'ctx DbmsContext<M>,
tx: Option<TransactionId>,
sql: &str,
params: &[Value],
) -> Result<SqlResult, SqlError>
where
M: MemoryProvider,
S: DatabaseSchema<M> + Clone + 'ctx,
{
let parsed = parse(sql)?;
if parsed.parameter_count != params.len() {
return Err(SqlError::ParameterCountMismatch {
expected: parsed.parameter_count,
got: params.len(),
});
}
match (&parsed.statement, tx) {
(Statement::Begin, None) => Ok(SqlResult::TxBegin(ctx.begin_transaction())),
(Statement::Begin, Some(_)) => Err(SqlError::TransactionAlreadyActive),
(Statement::Commit | Statement::Rollback, None) => Err(SqlError::NoActiveTransaction),
(Statement::Commit, Some(id)) => {
WasmDbmsDatabase::from_transaction(ctx, self.schema.clone(), id).commit()?;
Ok(SqlResult::TxCommit)
}
(Statement::Rollback, Some(id)) => {
WasmDbmsDatabase::from_transaction(ctx, self.schema.clone(), id).rollback()?;
Ok(SqlResult::TxRollback)
}
(statement, Some(id)) => self.run(
&WasmDbmsDatabase::from_transaction(ctx, self.schema.clone(), id),
statement,
params,
),
(statement, None) => self.run(
&WasmDbmsDatabase::oneshot(ctx, self.schema.clone()),
statement,
params,
),
}
}
fn run<M>(
&self,
db: &WasmDbmsDatabase<'_, M>,
statement: &Statement,
params: &[Value],
) -> Result<SqlResult, SqlError>
where
M: MemoryProvider,
S: DatabaseSchema<M>,
{
let catalog = |table: &str| self.schema.table_columns(table).ok();
match statement {
Statement::Select(select) => {
let rows = match planner::select(select, &catalog, params)? {
SelectPlan::Table(plan) => db
.select_raw(&plan.table, plan.query)?
.into_iter()
.map(|row| table_row(row, plan.projection.as_deref()))
.collect(),
SelectPlan::Join(plan) => join_rows(
db.select_join(&plan.table, plan.query)?,
plan.projection.as_deref(),
),
SelectPlan::Aggregate(plan) => self
.schema
.aggregate(db, &plan.table, plan.query, &plan.aggregates)?
.into_iter()
.map(|row| aggregate_row(&row, &plan.outputs))
.collect(),
};
Ok(SqlResult::Rows(rows))
}
Statement::Insert(insert) => {
let plan = planner::insert(insert, &catalog, params)?;
let table = self.schema.static_table_name(&plan.table)?;
self.schema.insert(db, table, &plan.values)?;
Ok(SqlResult::RowsAffected(1))
}
Statement::Update(update) => {
let plan = planner::update(update, &catalog, params)?;
let table = self.schema.static_table_name(&plan.table)?;
let count = self
.schema
.update(db, table, &plan.values, Some(plan.filter))?;
Ok(SqlResult::RowsAffected(count))
}
Statement::Delete(delete) => {
let plan = planner::delete(delete, &catalog, params)?;
let table = self.schema.static_table_name(&plan.table)?;
let count = self
.schema
.delete(db, table, plan.behavior, Some(plan.filter))?;
Ok(SqlResult::RowsAffected(count))
}
Statement::Begin | Statement::Commit | Statement::Rollback => {
unreachable!("transaction statements are handled by `execute`")
}
}
}
}
impl<S> std::fmt::Debug for SqlEngine<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SqlEngine").finish_non_exhaustive()
}
}
fn table_row(row: Vec<(ColumnDef, Value)>, projection: Option<&[OutputColumn]>) -> SqlRow {
let Some(projection) = projection else {
return row
.into_iter()
.map(|(def, value)| (JoinColumnDef::from(def), value))
.collect();
};
projection
.iter()
.map(|output| {
let (def, value) = row
.iter()
.find(|(def, _)| def.name == output.column)
.expect("planned column must exist in the selected table row");
let mut def = JoinColumnDef::from(*def);
def.name = output.name.clone();
(def, value.clone())
})
.collect()
}
fn join_rows(result: JoinResultSet, projection: Option<&[OutputColumn]>) -> Vec<SqlRow> {
let JoinResultSet { columns, rows } = result;
let outputs: Vec<(usize, JoinColumnDef)> = match projection {
None => columns.into_iter().enumerate().collect(),
Some(projection) => projection
.iter()
.map(|output| {
let index = columns
.iter()
.position(|def| def.table == output.table && def.name == output.column)
.expect("planned column must exist in the selected join row");
let mut def = columns[index].clone();
def.name = output.name.clone();
(index, def)
})
.collect(),
};
rows.into_iter()
.map(|row| {
outputs
.iter()
.map(|(index, def)| (def.clone(), row[*index].clone()))
.collect()
})
.collect()
}
fn aggregate_row(row: &AggregatedRow, outputs: &[AggregateOutput]) -> SqlRow {
outputs
.iter()
.map(|output| {
let value = match output.source {
AggregateSource::GroupKey(index) => row
.group_keys
.get(index)
.cloned()
.expect("planned group key must exist in the aggregated row"),
AggregateSource::Aggregate(index) => row
.values
.get(index)
.map(|value| match value {
AggregatedValue::Count(count) => Value::from(*count),
AggregatedValue::Sum(value)
| AggregatedValue::Avg(value)
| AggregatedValue::Min(value)
| AggregatedValue::Max(value) => value.clone(),
})
.expect("planned aggregate must exist in the aggregated row"),
};
(output.def.clone(), value)
})
.collect()
}