use rusticx_core::{
column::ColumnDef,
model::TableSchema,
query::{CondOp, Conjunction, Direction, QueryBuilder},
value::Value,
};
use crate::dialect::SqlDialect;
pub struct SqlCompiler<'d, D: SqlDialect> {
dialect: &'d D,
}
impl<'d, D: SqlDialect> SqlCompiler<'d, D> {
pub fn new(dialect: &'d D) -> Self {
Self { dialect }
}
pub fn create_table(&self, schema: &TableSchema) -> String {
let table = self.dialect.quote_ident(&schema.table);
let col_defs: Vec<String> = schema.columns.iter().map(|c| self.col_def(c)).collect();
let pks: Vec<String> = schema
.columns
.iter()
.filter(|c| c.primary_key)
.map(|c| self.dialect.quote_ident(&c.name))
.collect();
let mut parts = col_defs;
if !pks.is_empty() {
parts.push(format!("PRIMARY KEY ({})", pks.join(", ")));
}
for col in schema.columns.iter().filter(|c| c.unique && !c.primary_key) {
parts.push(format!(
"UNIQUE ({})",
self.dialect.quote_ident(&col.name)
));
}
let mut sql = format!(
"CREATE TABLE IF NOT EXISTS {table} (\n {}\n)",
parts.join(",\n ")
);
let mut index_stmts = vec![];
for idx in &schema.indexes {
let cols: Vec<String> = idx.columns.iter().map(|c| self.dialect.quote_ident(c)).collect();
let unique = if idx.unique { "UNIQUE " } else { "" };
index_stmts.push(format!(
"CREATE {unique}INDEX IF NOT EXISTS {} ON {table} ({})",
self.dialect.quote_ident(&idx.name),
cols.join(", ")
));
}
if !index_stmts.is_empty() {
sql.push(';');
sql.push('\n');
sql.push_str(&index_stmts.join(";\n"));
}
sql
}
fn col_def(&self, col: &ColumnDef) -> String {
let name = self.dialect.quote_ident(&col.name);
let ty = self.dialect.sql_type(&col.col_type);
let mut parts = vec![format!("{name} {ty}")];
if !col.nullable {
parts.push("NOT NULL".into());
}
if let Some(ref d) = col.default {
parts.push(format!("DEFAULT {d}"));
}
parts.join(" ")
}
pub fn drop_table(&self, table: &str) -> String {
format!("DROP TABLE IF EXISTS {}", self.dialect.quote_ident(table))
}
pub fn table_exists(&self, table: &str) -> (String, Vec<Value>) {
let sql = "SELECT COUNT(*) FROM information_schema.tables WHERE table_name = $1".to_owned();
(sql, vec![Value::Text(table.to_owned())])
}
pub fn select(&self, qb: &QueryBuilder) -> (String, Vec<Value>) {
let table = self.dialect.quote_ident(&qb.table);
let cols = if qb.columns.is_empty() {
"*".to_owned()
} else {
qb.columns.iter().map(|c| self.dialect.quote_ident(c)).collect::<Vec<_>>().join(", ")
};
let mut sql = format!("SELECT {cols} FROM {table}");
let mut bindings: Vec<Value> = vec![];
let mut idx = 1usize;
if !qb.conditions.is_empty() {
let (where_clause, b, next_idx) = self.where_clause(&qb.conditions, idx);
sql.push_str(&format!(" WHERE {where_clause}"));
bindings.extend(b);
idx = next_idx;
}
if !qb.order_by.is_empty() {
let orders: Vec<String> = qb
.order_by
.iter()
.map(|o| {
let dir = match o.direction {
Direction::Asc => "ASC",
Direction::Desc => "DESC",
};
format!("{} {dir}", self.dialect.quote_ident(&o.column))
})
.collect();
sql.push_str(&format!(" ORDER BY {}", orders.join(", ")));
}
if let Some(limit) = qb.limit {
sql.push_str(&format!(" LIMIT {limit}"));
}
if let Some(offset) = qb.offset {
sql.push_str(&format!(" OFFSET {offset}"));
}
(sql, bindings)
}
pub fn insert(&self, table: &str, row: &[(String, Value)]) -> (String, Vec<Value>) {
let t = self.dialect.quote_ident(table);
let cols: Vec<String> = row.iter().map(|(c, _)| self.dialect.quote_ident(c)).collect();
let mut bindings: Vec<Value> = vec![];
let placeholders: Vec<String> = row
.iter()
.enumerate()
.map(|(i, (_, v))| {
bindings.push(v.clone());
self.dialect.placeholder(i + 1)
})
.collect();
let mut sql = format!(
"INSERT INTO {t} ({}) VALUES ({})",
cols.join(", "),
placeholders.join(", ")
);
if self.dialect.supports_returning() {
sql.push_str(" RETURNING *");
}
(sql, bindings)
}
pub fn update(&self, qb: &QueryBuilder) -> (String, Vec<Value>) {
let table = self.dialect.quote_ident(&qb.table);
let mut bindings: Vec<Value> = vec![];
let mut idx = 1usize;
let sets: Vec<String> = qb
.values
.iter()
.map(|(col, val)| {
let ph = self.dialect.placeholder(idx);
idx += 1;
bindings.push(val.clone());
format!("{} = {ph}", self.dialect.quote_ident(col))
})
.collect();
let mut sql = format!("UPDATE {table} SET {}", sets.join(", "));
if !qb.conditions.is_empty() {
let (where_clause, b, _) = self.where_clause(&qb.conditions, idx);
sql.push_str(&format!(" WHERE {where_clause}"));
bindings.extend(b);
}
(sql, bindings)
}
pub fn delete(&self, qb: &QueryBuilder) -> (String, Vec<Value>) {
let table = self.dialect.quote_ident(&qb.table);
let mut bindings: Vec<Value> = vec![];
let mut sql = format!("DELETE FROM {table}");
if !qb.conditions.is_empty() {
let (where_clause, b, _) = self.where_clause(&qb.conditions, 1);
sql.push_str(&format!(" WHERE {where_clause}"));
bindings.extend(b);
}
(sql, bindings)
}
pub fn count(&self, qb: &QueryBuilder) -> (String, Vec<Value>) {
let table = self.dialect.quote_ident(&qb.table);
let mut sql = format!("SELECT COUNT(*) AS count FROM {table}");
let mut bindings: Vec<Value> = vec![];
if !qb.conditions.is_empty() {
let (where_clause, b, _) = self.where_clause(&qb.conditions, 1);
sql.push_str(&format!(" WHERE {where_clause}"));
bindings.extend(b);
}
(sql, bindings)
}
fn where_clause(
&self,
conditions: &[rusticx_core::query::Condition],
start_idx: usize,
) -> (String, Vec<Value>, usize) {
let mut parts: Vec<String> = vec![];
let mut bindings: Vec<Value> = vec![];
let mut idx = start_idx;
for (i, cond) in conditions.iter().enumerate() {
let col = self.dialect.quote_ident(&cond.column);
let (op_str, needs_binding) = match &cond.op {
CondOp::Eq => ("=", true),
CondOp::Ne => ("!=", true),
CondOp::Gt => (">", true),
CondOp::Gte => (">=", true),
CondOp::Lt => ("<", true),
CondOp::Lte => ("<=", true),
CondOp::Like => ("LIKE", true),
CondOp::ILike => ("ILIKE", true),
CondOp::In => ("IN", true),
CondOp::NotIn => ("NOT IN", true),
CondOp::IsNull => ("IS NULL", false),
CondOp::IsNotNull => ("IS NOT NULL", false),
};
let part = if needs_binding {
let ph = self.dialect.placeholder(idx);
idx += 1;
bindings.push(cond.value.clone());
format!("{col} {op_str} {ph}")
} else {
format!("{col} {op_str}")
};
let conj = if i == 0 {
String::new()
} else {
match cond.conjunction {
Conjunction::And => "AND ".to_owned(),
Conjunction::Or => "OR ".to_owned(),
}
};
parts.push(format!("{conj}{part}"));
}
(parts.join(" "), bindings, idx)
}
}