use wasm_dbms_api::prelude::{Filter, Join, JoinType, OrderDirection, Query, SqlError, Value};
use super::coerce::row_count;
use super::condition::{Clause, Target, filter};
use super::scope::{Resolved, Scope};
use super::{Catalog, OutputColumn, SelectPlan, TablePlan, aggregate};
use crate::ast::{
Condition, JoinClause, JoinKind, Operand, OrderItem, Projection, Select, SelectItem,
};
pub(crate) fn select(
select: &Select,
catalog: &Catalog<'_>,
params: &[Value],
) -> Result<SelectPlan, SqlError> {
let mut scope = Scope::default();
scope.push(catalog, &select.from)?;
let joins = select
.joins
.iter()
.map(|clause| join(&mut scope, catalog, clause))
.collect::<Result<Vec<_>, _>>()?;
if is_aggregate(select) {
return aggregate::plan(select, &scope, params).map(SelectPlan::Aggregate);
}
let joined = !joins.is_empty();
let field = |resolved: &Resolved| {
if joined {
scope.qualified_name(resolved)
} else {
resolved.def.name.to_string()
}
};
let items: &[SelectItem] = match &select.projection {
Projection::All => &[],
Projection::Items(items) => items,
};
let resolved_items = items
.iter()
.map(|item| match &item.operand {
Operand::Column(column) => scope.resolve(column),
Operand::Aggregate(_) => unreachable!("aggregate queries are planned separately"),
})
.collect::<Result<Vec<_>, _>>()?;
let order_by = select
.order_by
.iter()
.map(|item| {
let resolved = order_column(&scope, items, &resolved_items, item)?;
Ok((field(&resolved), direction(item)))
})
.collect::<Result<Vec<_>, SqlError>>()?;
let distinct_by = if select.distinct {
distinct_columns(
&scope,
&select.projection,
&resolved_items,
&order_by,
joined,
)?
} else {
Vec::new()
};
let mut query = Query::default();
query.joins = joins;
query.filter = where_filter(&scope, select.filter.as_ref(), params, &field)?;
query.distinct_by = distinct_by;
query.order_by = order_by;
(query.limit, query.offset) = pagination(select, params)?;
let projection = match &select.projection {
Projection::All => None,
Projection::Items(items) => Some(
items
.iter()
.zip(&resolved_items)
.map(|(item, resolved)| OutputColumn {
table: joined.then(|| scope.table_name(resolved.table).to_string()),
column: resolved.def.name.to_string(),
name: item
.alias
.clone()
.unwrap_or_else(|| resolved.def.name.to_string()),
})
.collect(),
),
};
let plan = TablePlan {
table: scope.table_name(0).to_string(),
query,
projection,
};
Ok(if joined {
SelectPlan::Join(plan)
} else {
SelectPlan::Table(plan)
})
}
fn is_aggregate(select: &Select) -> bool {
let is_aggregate = |operand: &Operand| matches!(operand, Operand::Aggregate(_));
let in_projection = match &select.projection {
Projection::All => false,
Projection::Items(items) => items.iter().any(|item| is_aggregate(&item.operand)),
};
in_projection
|| !select.group_by.is_empty()
|| select.having.is_some()
|| select
.order_by
.iter()
.any(|item| is_aggregate(&item.operand))
}
fn join(scope: &mut Scope, catalog: &Catalog<'_>, clause: &JoinClause) -> Result<Join, SqlError> {
let joined = scope.push(catalog, &clause.table)?;
let left = scope.resolve(&clause.left)?;
let right = scope.resolve(&clause.right)?;
let (outer, inner) = match (left.table == joined, right.table == joined) {
(false, true) => (left, right),
(true, false) => (right, left),
_ => {
return Err(SqlError::Unsupported(format!(
"the JOIN condition of `{table}` must compare one of its columns with a column \
of a table that precedes it",
table = clause.table.name
)));
}
};
if outer.def.data_type != inner.def.data_type {
return Err(SqlError::TypeMismatch {
column: scope.qualified_name(&inner),
expected: outer.def.data_type.into(),
got: format!("{data_type:?}", data_type = inner.def.data_type),
});
}
Ok(Join {
join_type: match clause.kind {
JoinKind::Inner => JoinType::Inner,
JoinKind::Left => JoinType::Left,
JoinKind::Right => JoinType::Right,
JoinKind::Full => JoinType::Full,
},
table: clause.table.name.clone(),
left_column: scope.qualified_name(&outer),
right_column: inner.def.name.to_string(),
})
}
pub(super) fn where_filter(
scope: &Scope,
condition: Option<&Condition>,
params: &[Value],
field: &dyn Fn(&Resolved) -> String,
) -> Result<Option<Filter>, SqlError> {
condition
.map(|condition| {
filter(condition, params, Clause::Where, &mut |operand| {
let Operand::Column(column) = operand else {
return Err(SqlError::Unsupported(
"aggregate functions are not allowed in WHERE".to_string(),
));
};
let resolved = scope.resolve(column)?;
Ok(Target {
field: field(&resolved),
label: resolved.def.name.to_string(),
kind: resolved.def.data_type,
})
})
})
.transpose()
}
pub(super) fn pagination(
select: &Select,
params: &[Value],
) -> Result<(Option<usize>, Option<usize>), SqlError> {
let limit = select
.limit
.as_ref()
.map(|count| row_count(count, params, "LIMIT"))
.transpose()?;
let offset = select
.offset
.as_ref()
.map(|count| row_count(count, params, "OFFSET"))
.transpose()?;
Ok((limit, offset))
}
pub(super) fn direction(item: &OrderItem) -> OrderDirection {
if item.descending {
OrderDirection::Descending
} else {
OrderDirection::Ascending
}
}
pub(super) fn aliased_item(items: &[SelectItem], item: &OrderItem) -> Option<usize> {
let Operand::Column(column) = &item.operand else {
return None;
};
if column.table.is_some() {
return None;
}
items
.iter()
.position(|candidate| candidate.alias.as_deref() == Some(column.column.as_str()))
}
fn order_column(
scope: &Scope,
items: &[SelectItem],
resolved_items: &[Resolved],
item: &OrderItem,
) -> Result<Resolved, SqlError> {
if let Some(index) = aliased_item(items, item) {
return Ok(resolved_items[index]);
}
match &item.operand {
Operand::Column(column) => scope.resolve(column),
Operand::Aggregate(_) => unreachable!("aggregate queries are planned separately"),
}
}
fn distinct_columns(
scope: &Scope,
projection: &Projection,
resolved_items: &[Resolved],
order_by: &[(String, OrderDirection)],
joined: bool,
) -> Result<Vec<String>, SqlError> {
if joined {
return Err(SqlError::Unsupported(
"DISTINCT is not supported together with JOIN".to_string(),
));
}
let columns: Vec<String> = match projection {
Projection::All => scope
.columns(0)
.iter()
.map(|def| def.name.to_string())
.collect(),
Projection::Items(_) => resolved_items
.iter()
.map(|resolved| resolved.def.name.to_string())
.collect(),
};
if let Some((column, _)) = order_by
.iter()
.find(|(column, _)| !columns.contains(column))
{
return Err(SqlError::Unsupported(format!(
"ORDER BY column `{column}` must appear in the select list of a SELECT DISTINCT"
)));
}
Ok(columns)
}