use crate::backend::BackendKind;
use crate::generation::ManifestTable;
use crate::ir::filter::{ComparisonOp, LogicalFilter};
use crate::ir::operations::{AggregateExpr, AggregateFunc, LogicalAggregate};
use crate::ir::value::LogicalValue;
use super::CompileError;
pub(super) trait SqlDialect {
fn backend() -> BackendKind;
fn quote(ident: &str) -> String;
fn placeholder(index: usize) -> String;
fn false_literal() -> &'static str;
fn having_true_literal() -> &'static str;
fn having_false_literal() -> &'static str;
fn wrap_value_for_op(op: ComparisonOp, placeholder: &str) -> String;
fn sql_op_for(op: ComparisonOp) -> &'static str {
match op {
ComparisonOp::Eq => "=",
ComparisonOp::Ne => "<>",
ComparisonOp::Lt => "<",
ComparisonOp::Le => "<=",
ComparisonOp::Gt => ">",
ComparisonOp::Ge => ">=",
ComparisonOp::Like
| ComparisonOp::Contains
| ComparisonOp::StartsWith
| ComparisonOp::EndsWith => "LIKE",
ComparisonOp::ILike => "ILIKE",
}
}
}
pub(super) struct SqlCompiler<D: SqlDialect> {
_dialect: std::marker::PhantomData<D>,
}
impl<D: SqlDialect> SqlCompiler<D> {
pub(super) fn resolve_table<'a>(
message_type: &str,
manifest: &'a crate::generation::CatalogManifest,
) -> Result<&'a ManifestTable, CompileError> {
crate::broker::table_for_message(manifest, message_type).ok_or_else(|| {
CompileError::UnknownMessageType {
message_type: message_type.to_string(),
}
})
}
pub(super) fn column_for<'a>(
table: &'a ManifestTable,
field: &str,
message_type: &str,
) -> Result<&'a str, CompileError> {
table
.columns
.iter()
.find(|c| c.field_name.eq_ignore_ascii_case(field) || c.column_name == field)
.map(|c| c.column_name.as_str())
.ok_or_else(|| CompileError::UnknownField {
message_type: message_type.to_string(),
field: field.to_string(),
})
}
pub(super) fn push_param(params: &mut Vec<LogicalValue>, v: LogicalValue) -> String {
params.push(v);
D::placeholder(params.len())
}
pub(super) fn render_where(
filter: &LogicalFilter,
table: &ManifestTable,
message_type: &str,
params: &mut Vec<LogicalValue>,
) -> Result<Option<String>, CompileError> {
match filter {
LogicalFilter::And(clauses) if clauses.is_empty() => Ok(None),
LogicalFilter::Or(clauses) if clauses.is_empty() => {
Ok(Some(D::false_literal().to_string()))
}
_ => Ok(Some(Self::render_filter(
filter,
table,
message_type,
params,
)?)),
}
}
pub(super) fn render_filter(
filter: &LogicalFilter,
table: &ManifestTable,
message_type: &str,
params: &mut Vec<LogicalValue>,
) -> Result<String, CompileError> {
match filter {
LogicalFilter::And(clauses) => {
let parts = clauses
.iter()
.map(|c| Self::render_filter(c, table, message_type, params))
.collect::<Result<Vec<_>, _>>()?;
Ok(format!("({})", parts.join(" AND ")))
}
LogicalFilter::Or(clauses) => {
let parts = clauses
.iter()
.map(|c| Self::render_filter(c, table, message_type, params))
.collect::<Result<Vec<_>, _>>()?;
Ok(format!("({})", parts.join(" OR ")))
}
LogicalFilter::Not(inner) => {
let rendered = Self::render_filter(inner, table, message_type, params)?;
Ok(format!("(NOT {rendered})"))
}
LogicalFilter::Comparison { field, op, value } => {
let column = Self::column_for(table, field, message_type)?;
if value.is_null() {
return Err(CompileError::Malformed {
reason: format!(
"comparison with NULL on field '{field}' must use IsNull, not {}",
op.token()
),
});
}
let placeholder = Self::push_param(params, value.clone());
let sql_op = D::sql_op_for(*op);
let rhs = D::wrap_value_for_op(*op, &placeholder);
Ok(format!("{} {sql_op} {rhs}", D::quote(column)))
}
LogicalFilter::IsNull(field) => {
let column = Self::column_for(table, field, message_type)?;
Ok(format!("{} IS NULL", D::quote(column)))
}
LogicalFilter::InList { field, values } => {
if values.is_empty() {
return Ok(D::false_literal().to_string());
}
for value in values {
if matches!(value, LogicalValue::Array(_) | LogicalValue::Json(_)) {
return Err(CompileError::Malformed {
reason: format!(
"IN list for field '{field}' accepts scalar values only; got {}",
value.type_token()
),
});
}
if value.is_null() {
return Err(CompileError::Malformed {
reason: format!(
"IN list for field '{field}' cannot contain NULL; use IsNull explicitly"
),
});
}
}
let column = Self::column_for(table, field, message_type)?;
let placeholders = values
.iter()
.map(|v| Self::push_param(params, v.clone()))
.collect::<Vec<_>>()
.join(", ");
Ok(format!("{} IN ({placeholders})", D::quote(column)))
}
}
}
pub(super) fn render_aggregate(
agg: &AggregateExpr,
table: &ManifestTable,
message_type: &str,
) -> Result<String, CompileError> {
let token = agg.func.sql_token();
let body = match agg.func {
AggregateFunc::Count if agg.field == "*" => "*".to_string(),
AggregateFunc::Count => {
let col = Self::column_for(table, &agg.field, message_type)?;
D::quote(col)
}
AggregateFunc::CountDistinct => {
if agg.field == "*" {
return Err(CompileError::Malformed {
reason: "COUNT(DISTINCT *) is not allowed; specify a field".into(),
});
}
let col = Self::column_for(table, &agg.field, message_type)?;
format!("DISTINCT {}", D::quote(col))
}
AggregateFunc::Sum | AggregateFunc::Avg | AggregateFunc::Min | AggregateFunc::Max => {
if agg.field == "*" {
return Err(CompileError::Malformed {
reason: format!("{} requires a field name, not '*'", agg.func.sql_token()),
});
}
let col = Self::column_for(table, &agg.field, message_type)?;
D::quote(col)
}
};
Ok(format!("{token}({body}) AS {}", D::quote(&agg.alias)))
}
pub(super) fn resolve_sort_field(
field: &str,
op: &LogicalAggregate,
table: &ManifestTable,
) -> Result<String, CompileError> {
if op.aggregates.iter().any(|a| a.alias == field) {
return Ok(D::quote(field));
}
if op.group_by.iter().any(|f| f == field) {
let col = Self::column_for(table, field, &op.message_type)?;
return Ok(D::quote(col));
}
Err(CompileError::Malformed {
reason: format!(
"ORDER BY field '{field}' is neither an aggregate alias nor a GROUP BY column; \
aggregated queries can only sort by grouped or computed values"
),
})
}
pub(super) fn render_having(
filter: &LogicalFilter,
op: &LogicalAggregate,
table: &ManifestTable,
params: &mut Vec<LogicalValue>,
) -> Result<String, CompileError> {
match filter {
LogicalFilter::And(clauses) if clauses.is_empty() => {
Ok(D::having_true_literal().to_string())
}
LogicalFilter::Or(clauses) if clauses.is_empty() => {
Ok(D::having_false_literal().to_string())
}
LogicalFilter::And(clauses) => {
let parts = clauses
.iter()
.map(|c| Self::render_having(c, op, table, params))
.collect::<Result<Vec<_>, _>>()?;
Ok(format!("({})", parts.join(" AND ")))
}
LogicalFilter::Or(clauses) => {
let parts = clauses
.iter()
.map(|c| Self::render_having(c, op, table, params))
.collect::<Result<Vec<_>, _>>()?;
Ok(format!("({})", parts.join(" OR ")))
}
LogicalFilter::Not(inner) => {
let rendered = Self::render_having(inner, op, table, params)?;
Ok(format!("(NOT {rendered})"))
}
LogicalFilter::Comparison {
field,
op: cmp,
value,
} => {
if value.is_null() {
return Err(CompileError::Malformed {
reason: format!(
"HAVING comparison with NULL on '{field}' must use IsNull, not {}",
cmp.token()
),
});
}
let token = Self::resolve_having_field(field, op, table, &op.message_type)?;
let placeholder = Self::push_param(params, value.clone());
let sql_op = D::sql_op_for(*cmp);
let rhs = D::wrap_value_for_op(*cmp, &placeholder);
Ok(format!("{token} {sql_op} {rhs}"))
}
LogicalFilter::IsNull(field) => {
let token = Self::resolve_having_field(field, op, table, &op.message_type)?;
Ok(format!("{token} IS NULL"))
}
LogicalFilter::InList { field, values } => {
if values.is_empty() {
return Ok(D::having_false_literal().to_string());
}
let token = Self::resolve_having_field(field, op, table, &op.message_type)?;
let placeholders = values
.iter()
.map(|v| Self::push_param(params, v.clone()))
.collect::<Vec<_>>()
.join(", ");
Ok(format!("{token} IN ({placeholders})"))
}
}
}
pub(super) fn resolve_having_field(
field: &str,
op: &LogicalAggregate,
table: &ManifestTable,
message_type: &str,
) -> Result<String, CompileError> {
if op.aggregates.iter().any(|a| a.alias == field) {
return Ok(D::quote(field));
}
if op.group_by.iter().any(|f| f == field) {
let column = Self::column_for(table, field, message_type)?;
return Ok(D::quote(column));
}
Err(CompileError::Malformed {
reason: format!(
"HAVING field '{field}' is neither an aggregate alias nor a GROUP BY column"
),
})
}
}