use crate::builder::{
ConflictAction, Cte, Having, Join, JoinCond, JoinKind, Method, Order, QueryBuilder,
};
use crate::dialect::{Dialect, UpsertStyle};
use crate::ident::escape_identifier;
use crate::value::Value;
use crate::where_::{Conj, Predicate};
struct Ctx {
sql: String,
binds: Vec<Value>,
quote: char,
}
impl Ctx {
fn placeholder<D: Dialect>(&mut self, val: Value) {
self.binds.push(val);
D::write_placeholder(&mut self.sql, self.binds.len());
}
fn esc(&self, ident: &str) -> String {
escape_identifier(ident, self.quote)
}
fn qualify(&self, db: Option<&str>, table: &str) -> String {
match db {
Some(d) => format!("{}.{}", self.esc(d), self.esc(table)),
None => self.esc(table),
}
}
}
pub fn compile<D: Dialect>(qb: &QueryBuilder<D>) -> (String, Vec<Value>) {
let mut ctx = Ctx {
sql: String::new(),
binds: Vec::new(),
quote: D::quote_char(),
};
compile_into::<D>(&mut ctx, qb);
(ctx.sql, ctx.binds)
}
fn compile_into<D: Dialect>(ctx: &mut Ctx, qb: &QueryBuilder<D>) {
let table = ctx.qualify(qb.db.as_deref(), &qb.table);
match qb.method {
Method::Select => {
write_ctes::<D>(ctx, &qb.ctes);
if !qb.distinct_on.is_empty() {
if !D::supports_distinct_on() {
panic!("DISTINCT ON requires PostgreSQL");
}
ctx.sql.push_str("SELECT DISTINCT ON (");
let cols: Vec<String> = qb.distinct_on.iter().map(|c| ctx.esc(c)).collect();
ctx.sql.push_str(&cols.join(", "));
ctx.sql.push_str(") ");
} else if qb.distinct {
ctx.sql.push_str("SELECT DISTINCT ");
} else {
ctx.sql.push_str("SELECT ");
}
write_select_list::<D>(ctx, qb);
ctx.sql.push_str(" FROM ");
ctx.sql.push_str(&table);
write_joins::<D>(ctx, &qb.joins, qb.db.as_deref());
write_wheres::<D>(ctx, &qb.wheres);
write_group_by(ctx, &qb.groups, qb.group_by_raw.as_ref());
write_having::<D>(ctx, &qb.havings);
write_order_by(ctx, &qb.orders, qb.order_by_raw.as_ref());
write_limit_offset::<D>(ctx, qb.limit, qb.offset);
write_unions::<D>(ctx, &qb.unions);
}
Method::Insert => {
if qb.set.is_empty() && qb.insert_rows.is_empty() {
panic!("insert() requires at least one column");
}
let mut single_rows: Vec<&(String, Value)> = qb.set.iter().collect();
single_rows.sort_by(|a, b| a.0.cmp(&b.0));
let sorted_cols: Vec<&str> = if !qb.insert_rows.is_empty() {
let mut cols: Vec<&str> =
qb.insert_rows[0].iter().map(|(k, _)| k.as_str()).collect();
cols.sort_unstable();
cols
} else {
single_rows.iter().map(|(k, _)| k.as_str()).collect()
};
let mysql_ignore = D::upsert_style() == UpsertStyle::OnDuplicateKey
&& matches!(
qb.on_conflict.as_ref().map(|c| c.action),
Some(ConflictAction::DoNothing)
);
if mysql_ignore {
ctx.sql.push_str("INSERT IGNORE INTO ");
} else {
ctx.sql.push_str("INSERT INTO ");
}
ctx.sql.push_str(&table);
ctx.sql.push_str(" (");
let cols: Vec<String> = sorted_cols.iter().map(|k| ctx.esc(k)).collect();
ctx.sql.push_str(&cols.join(", "));
ctx.sql.push_str(") VALUES ");
if !qb.insert_rows.is_empty() {
for (ri, row) in qb.insert_rows.iter().enumerate() {
if ri > 0 {
ctx.sql.push_str(", ");
}
ctx.sql.push('(');
for (ci, col) in sorted_cols.iter().enumerate() {
if ci > 0 {
ctx.sql.push_str(", ");
}
let v = row
.iter()
.find(|(k, _)| k == col)
.map(|(_, v)| v.clone())
.unwrap_or(Value::Null);
ctx.placeholder::<D>(v);
}
ctx.sql.push(')');
}
} else {
ctx.sql.push('(');
for (i, (_, v)) in single_rows.iter().enumerate() {
if i > 0 {
ctx.sql.push_str(", ");
}
ctx.placeholder::<D>(v.clone());
}
ctx.sql.push(')');
}
if !mysql_ignore {
if let Some(oc) = &qb.on_conflict {
write_on_conflict::<D>(ctx, oc, &sorted_cols);
}
}
write_returning::<D>(ctx, &qb.returning);
}
Method::Update => {
if qb.set.is_empty() {
panic!("update() requires at least one column");
}
let mut rows: Vec<&(String, Value)> = qb.set.iter().collect();
rows.sort_by(|a, b| a.0.cmp(&b.0));
ctx.sql.push_str("UPDATE ");
ctx.sql.push_str(&table);
ctx.sql.push_str(" SET ");
for (i, (k, v)) in rows.iter().enumerate() {
if i > 0 {
ctx.sql.push_str(", ");
}
let col = ctx.esc(k);
ctx.sql.push_str(&col);
ctx.sql.push_str(" = ");
ctx.placeholder::<D>(v.clone());
}
write_wheres::<D>(ctx, &qb.wheres);
write_returning::<D>(ctx, &qb.returning);
}
Method::Delete => {
ctx.sql.push_str("DELETE FROM ");
ctx.sql.push_str(&table);
write_wheres::<D>(ctx, &qb.wheres);
write_returning::<D>(ctx, &qb.returning);
}
}
}
fn write_on_conflict<D: Dialect>(
ctx: &mut Ctx,
oc: &crate::builder::OnConflict,
inserted: &[&str],
) {
match D::upsert_style() {
UpsertStyle::OnDuplicateKey => {
ctx.sql.push_str(" ON DUPLICATE KEY UPDATE ");
let sets: Vec<String> = inserted
.iter()
.map(|c| {
let e = ctx.esc(c);
format!("{e} = VALUES({e})")
})
.collect();
ctx.sql.push_str(&sets.join(", "));
}
UpsertStyle::OnConflict => {
let targets = &oc.targets;
let set_cols: Vec<&&str> = inserted
.iter()
.filter(|c| !targets.iter().any(|t| t == **c))
.collect();
let do_update = matches!(oc.action, ConflictAction::Merge)
&& !targets.is_empty()
&& !set_cols.is_empty();
ctx.sql.push_str(" ON CONFLICT");
if !targets.is_empty() {
ctx.sql.push_str(" (");
let cols: Vec<String> = targets.iter().map(|t| ctx.esc(t)).collect();
ctx.sql.push_str(&cols.join(", "));
ctx.sql.push(')');
}
if do_update {
ctx.sql.push_str(" DO UPDATE SET ");
let sets: Vec<String> = set_cols
.iter()
.map(|c| {
let e = ctx.esc(c);
format!("{e} = EXCLUDED.{e}")
})
.collect();
ctx.sql.push_str(&sets.join(", "));
} else {
ctx.sql.push_str(" DO NOTHING");
}
}
}
}
fn write_returning<D: Dialect>(ctx: &mut Ctx, cols: &[String]) {
if !D::supports_returning() || cols.is_empty() {
return;
}
ctx.sql.push_str(" RETURNING ");
let parts: Vec<String> = cols
.iter()
.map(|c| if c == "*" { "*".to_owned() } else { ctx.esc(c) })
.collect();
ctx.sql.push_str(&parts.join(", "));
}
fn write_group_by(ctx: &mut Ctx, groups: &[String], raw: Option<&(String, Vec<Value>)>) {
if groups.is_empty() && raw.is_none() {
return;
}
ctx.sql.push_str(" GROUP BY ");
let cols: Vec<String> = groups.iter().map(|c| ctx.esc(c)).collect();
ctx.sql.push_str(&cols.join(", "));
if let Some((sql, binds)) = raw {
if !groups.is_empty() {
ctx.sql.push_str(", ");
}
ctx.sql.push_str(sql);
ctx.binds.extend(binds.iter().cloned());
}
}
fn write_select_list<D: Dialect>(ctx: &mut Ctx, qb: &QueryBuilder<D>) {
if qb.select_cols.is_empty() && qb.select_raw.is_empty() && qb.select_subqueries.is_empty() {
ctx.sql.push('*');
return;
}
let mut wrote_any = false;
for c in &qb.select_cols {
if wrote_any {
ctx.sql.push_str(", ");
}
let e = ctx.esc(c);
ctx.sql.push_str(&e);
wrote_any = true;
}
for (sql, binds) in &qb.select_raw {
if wrote_any {
ctx.sql.push_str(", ");
}
ctx.sql.push_str(sql);
ctx.binds.extend(binds.iter().cloned());
wrote_any = true;
}
for (alias, sub) in &qb.select_subqueries {
if wrote_any {
ctx.sql.push_str(", ");
}
ctx.sql.push('(');
compile_into::<D>(ctx, sub);
ctx.sql.push_str(") AS ");
let a = ctx.esc(alias);
ctx.sql.push_str(&a);
wrote_any = true;
}
}
fn write_joins<D: Dialect>(ctx: &mut Ctx, joins: &[Join], db: Option<&str>) {
for j in joins {
let kw = match j.kind {
JoinKind::Inner => " INNER JOIN ",
JoinKind::Left => " LEFT JOIN ",
JoinKind::Right => " RIGHT JOIN ",
JoinKind::FullOuter => " FULL OUTER JOIN ",
JoinKind::Cross => " CROSS JOIN ",
};
ctx.sql.push_str(kw);
let table = ctx.qualify(db, &j.table);
ctx.sql.push_str(&table);
if j.on.is_empty() {
continue;
}
ctx.sql.push_str(" ON ");
for (i, cond) in j.on.iter().enumerate() {
if i > 0 {
ctx.sql.push_str(" AND ");
}
match cond {
JoinCond::On(c, op, c2) => {
let l = ctx.esc(c);
let r = ctx.esc(c2);
ctx.sql.push_str(&l);
ctx.sql.push(' ');
ctx.sql.push_str(op);
ctx.sql.push(' ');
ctx.sql.push_str(&r);
}
JoinCond::OnVal(c, op, v) => {
let l = ctx.esc(c);
ctx.sql.push_str(&l);
ctx.sql.push(' ');
ctx.sql.push_str(op);
ctx.sql.push(' ');
ctx.placeholder::<D>(v.clone());
}
JoinCond::OnRaw(sql, binds) => {
ctx.sql.push_str(sql);
ctx.binds.extend(binds.iter().cloned());
}
}
}
}
}
fn write_having<D: Dialect>(ctx: &mut Ctx, havings: &[Having]) {
if havings.is_empty() {
return;
}
ctx.sql.push_str(" HAVING ");
for (i, h) in havings.iter().enumerate() {
if i > 0 {
ctx.sql.push_str(" AND ");
}
match h {
Having::Col { col, op, val } => {
let c = ctx.esc(col);
ctx.sql.push_str(&c);
ctx.sql.push(' ');
ctx.sql.push_str(op);
ctx.sql.push(' ');
ctx.placeholder::<D>(val.clone());
}
Having::Raw { sql, binds } => {
ctx.sql.push_str(sql);
ctx.binds.extend(binds.iter().cloned());
}
}
}
}
fn write_ctes<D: Dialect>(ctx: &mut Ctx, ctes: &[Cte<D>]) {
if ctes.is_empty() {
return;
}
ctx.sql.push_str("WITH ");
if ctes.iter().any(|c| c.recursive) {
ctx.sql.push_str("RECURSIVE ");
}
for (i, cte) in ctes.iter().enumerate() {
if i > 0 {
ctx.sql.push_str(", ");
}
let name = ctx.esc(&cte.name);
ctx.sql.push_str(&name);
ctx.sql.push_str(" AS (");
compile_into::<D>(ctx, &cte.query);
ctx.sql.push(')');
}
ctx.sql.push(' ');
}
fn write_unions<D: Dialect>(ctx: &mut Ctx, unions: &[(bool, QueryBuilder<D>)]) {
for (all, arm) in unions {
ctx.sql
.push_str(if *all { " UNION ALL " } else { " UNION " });
compile_into::<D>(ctx, arm);
}
}
fn write_order_by(ctx: &mut Ctx, orders: &[(String, Order)], raw: Option<&(String, Vec<Value>)>) {
if orders.is_empty() && raw.is_none() {
return;
}
ctx.sql.push_str(" ORDER BY ");
let cols: Vec<String> = orders
.iter()
.map(|(c, o)| {
let dir = match o {
Order::Asc => "ASC",
Order::Desc => "DESC",
};
format!("{} {}", ctx.esc(c), dir)
})
.collect();
ctx.sql.push_str(&cols.join(", "));
if let Some((sql, binds)) = raw {
if !orders.is_empty() {
ctx.sql.push_str(", ");
}
ctx.sql.push_str(sql);
ctx.binds.extend(binds.iter().cloned());
}
}
fn write_limit_offset<D: Dialect>(ctx: &mut Ctx, limit: Option<i64>, offset: Option<i64>) {
if offset.is_some() && limit.is_none() {
panic!("offset(...) requires limit(...)");
}
if let Some(n) = limit {
ctx.sql.push_str(" LIMIT ");
ctx.placeholder::<D>(Value::I64(n));
}
if let Some(n) = offset {
ctx.sql.push_str(" OFFSET ");
ctx.placeholder::<D>(Value::I64(n));
}
}
fn is_omitted<D: Dialect>(p: &Predicate<D>) -> bool {
matches!(p, Predicate::Group { preds, .. } if preds.is_empty())
}
fn write_wheres<D: Dialect>(ctx: &mut Ctx, wheres: &[Predicate<D>]) {
if wheres.iter().all(is_omitted) {
return;
}
ctx.sql.push_str(" WHERE ");
write_clause_list::<D>(ctx, wheres);
}
fn write_clause_list<D: Dialect>(ctx: &mut Ctx, preds: &[Predicate<D>]) {
let mut wrote_any = false;
for p in preds.iter() {
if is_omitted(p) {
continue;
}
if wrote_any {
let sep = match p {
Predicate::Group {
outer_conj: Conj::Or,
..
} => " OR ",
_ => " AND ",
};
ctx.sql.push_str(sep);
}
write_pred::<D>(ctx, p);
wrote_any = true;
}
}
fn write_pred<D: Dialect>(ctx: &mut Ctx, pred: &Predicate<D>) {
match pred {
Predicate::Binary { col, op, val } => {
let col = ctx.esc(col);
ctx.sql.push_str(&col);
ctx.sql.push(' ');
ctx.sql.push_str(op);
ctx.sql.push(' ');
ctx.placeholder::<D>(val.clone());
}
Predicate::In { col, neg, vals } => {
if vals.is_empty() {
ctx.sql.push_str(if *neg { "1 = 1" } else { "1 = 0" });
return;
}
let col = ctx.esc(col);
ctx.sql.push_str(&col);
ctx.sql.push_str(if *neg { " NOT IN (" } else { " IN (" });
for (i, v) in vals.iter().enumerate() {
if i > 0 {
ctx.sql.push_str(", ");
}
ctx.placeholder::<D>(v.clone());
}
ctx.sql.push(')');
}
Predicate::Null { col, neg } => {
let col = ctx.esc(col);
ctx.sql.push_str(&col);
ctx.sql
.push_str(if *neg { " IS NOT NULL" } else { " IS NULL" });
}
Predicate::Between { col, lo, hi } => {
let col = ctx.esc(col);
ctx.sql.push_str(&col);
ctx.sql.push_str(" BETWEEN ");
ctx.placeholder::<D>(lo.clone());
ctx.sql.push_str(" AND ");
ctx.placeholder::<D>(hi.clone());
}
Predicate::ILike { col, val } => {
let col = ctx.esc(col);
if D::ilike_is_native() {
ctx.sql.push_str(&col);
ctx.sql.push_str(" ILIKE ");
ctx.placeholder::<D>(val.clone());
} else {
ctx.sql.push_str("LOWER(");
ctx.sql.push_str(&col);
ctx.sql.push_str(") LIKE LOWER(");
ctx.placeholder::<D>(val.clone());
ctx.sql.push(')');
}
}
Predicate::JsonContains { col, val } => {
let col = ctx.esc(col);
ctx.sql.push_str(&col);
ctx.sql.push_str(" @> ");
ctx.placeholder::<D>(val.clone());
}
Predicate::Raw { sql, binds } => {
ctx.sql.push_str(sql);
ctx.binds.extend(binds.iter().cloned());
}
Predicate::Group {
outer_conj: _,
preds,
} => {
ctx.sql.push('(');
write_clause_list::<D>(ctx, preds);
ctx.sql.push(')');
}
Predicate::Column { lhs, op, rhs } => {
let l = ctx.esc(lhs);
let r = ctx.esc(rhs);
ctx.sql.push_str(&l);
ctx.sql.push(' ');
ctx.sql.push_str(op);
ctx.sql.push(' ');
ctx.sql.push_str(&r);
}
Predicate::Exists { neg, sub } => {
ctx.sql
.push_str(if *neg { "NOT EXISTS (" } else { "EXISTS (" });
compile_into::<D>(ctx, sub);
ctx.sql.push(')');
}
Predicate::InSubquery { col, neg, sub } => {
let col = ctx.esc(col);
ctx.sql.push_str(&col);
ctx.sql.push_str(if *neg { " NOT IN (" } else { " IN (" });
compile_into::<D>(ctx, sub);
ctx.sql.push(')');
}
}
}