use std::fmt::Write as _;
use crate::core::{
AggregateExpr, AggregateQuery, BulkInsertQuery, BulkUpdateQuery, CountQuery, DeleteQuery,
Filter, InsertQuery, ModelSchema, Op, SearchClause, SelectQuery, SqlValue, UpdateQuery,
WhereExpr, LIKE_ESCAPE_CLAUSE,
};
use super::{CompiledStatement, Dialect, SqlError};
#[allow(clippy::struct_field_names)] pub(super) struct Sql<'d> {
pub d: &'d dyn Dialect,
pub sql: String,
pub params: Vec<SqlValue>,
pub scope_stack: Vec<Scope>,
pub qualifier: Option<(usize, Qualifier)>,
pub aggregate_allowed: bool,
pub derived_joins: Option<DerivedJoins>,
pub join_types: Vec<(usize, &'static str, &'static ModelSchema)>,
pub total: Option<CountQuery>,
pub child_count: Option<ChildCount>,
pub label_model_cols: bool,
pub group_concat: bool,
pub dml_target: Option<&'static str>,
pub read_dml_target: bool,
pub derive_dml_target: Option<usize>,
pub outer_reach: Option<usize>,
}
const SELF_ALIAS: &str = "__rustango_self";
#[derive(Clone, Copy)]
pub(super) struct Scope {
model: &'static ModelSchema,
name: &'static str,
}
impl Scope {
fn plain(model: &'static ModelSchema) -> Self {
Self {
model,
name: model.table,
}
}
fn write_from(self, b: &mut Sql<'_>) {
let alias = (self.name != self.model.table).then_some(self.name);
b.write_table(self.model.table, alias);
}
fn rename<'a>(self, alias: &'a str) -> &'a str {
if alias == self.model.table {
self.name
} else {
alias
}
}
}
#[derive(Clone, Copy)]
pub(super) enum Qualifier {
Join(&'static str),
Base(&'static str),
}
pub(super) struct DerivedJoins {
depth: usize,
table: &'static str,
aliases: Vec<&'static str>,
}
impl<'d> Sql<'d> {
pub(super) fn new(d: &'d dyn Dialect) -> Self {
Self {
d,
sql: String::new(),
params: Vec::new(),
scope_stack: Vec::new(),
qualifier: None,
aggregate_allowed: false,
derived_joins: None,
join_types: Vec::new(),
total: None,
child_count: None,
label_model_cols: false,
group_concat: false,
dml_target: None,
read_dml_target: false,
derive_dml_target: None,
outer_reach: None,
}
}
pub(super) fn with_capacity(d: &'d dyn Dialect, cap: usize) -> Self {
Self {
d,
sql: String::new(),
params: Vec::with_capacity(cap),
scope_stack: Vec::new(),
qualifier: None,
aggregate_allowed: false,
derived_joins: None,
join_types: Vec::new(),
total: None,
child_count: None,
label_model_cols: false,
group_concat: false,
dml_target: None,
read_dml_target: false,
derive_dml_target: None,
outer_reach: None,
}
}
fn correlated_scope(&self, model: &'static ModelSchema) -> Scope {
match self.scope_stack.last() {
Some(outer) if outer.name == model.table => Scope {
model,
name: SELF_ALIAS,
},
_ => Scope::plain(model),
}
}
fn scope_or(&self, model: &'static ModelSchema) -> Scope {
self.scope_stack
.last()
.copied()
.unwrap_or_else(|| Scope::plain(model))
}
fn scope_model(&self) -> Option<&'static ModelSchema> {
self.scope_stack.last().map(|s| s.model)
}
fn qualifier(&self) -> Option<Qualifier> {
self.qualifier
.filter(|(depth, _)| *depth == self.scope_stack.len())
.map(|(_, q)| q)
}
fn qualify_alias(&self, column: &str) -> Option<std::borrow::Cow<'static, str>> {
match self.qualifier()? {
Qualifier::Join(a) => Some(self.alias_ref(a)),
Qualifier::Base(t) => self
.scope_model()
.map_or(true, |m| m.field_by_column(column).is_some())
.then_some(t.into()),
}
}
fn write_alias(&mut self, alias: &str) {
self.write_ident(&crate::core::joins::sql_label(alias));
}
fn alias_ref<'a>(&self, alias: &'a str) -> std::borrow::Cow<'a, str> {
if self.join_types.iter().any(|(_, a, _)| *a == alias) {
crate::core::joins::sql_label(alias)
} else {
alias.into()
}
}
fn with_qualifier<R>(&mut self, q: Option<Qualifier>, f: impl FnOnce(&mut Self) -> R) -> R {
let depth = self.scope_stack.len();
let prior = std::mem::replace(&mut self.qualifier, q.map(|q| (depth, q)));
let r = f(self);
self.qualifier = prior;
r
}
pub(super) fn write_ident(&mut self, name: &str) {
self.sql.push_str(&self.d.quote_ident(name));
}
fn write_scope_col(&mut self, col: &str) {
let s = render_qualified_col(self, col);
self.sql.push_str(&s);
}
fn write_table(&mut self, name: &str, alias: Option<&str>) {
let target = self.dml_target == Some(name);
let derive = target && self.derive_dml_target == Some(self.scope_stack.len());
self.read_dml_target |= target && !derive;
if derive {
self.sql.push_str("(SELECT * FROM ");
self.write_ident(name);
self.sql.push(')');
} else {
self.write_ident(name);
}
if let Some(alias) = alias.or(derive.then_some(name)) {
self.sql.push_str(" AS ");
self.write_ident(alias);
}
}
fn read_outer(&mut self, depth: usize) {
self.outer_reach = Some(self.outer_reach.map_or(depth, |d| d.min(depth)));
}
fn with_derived_dml_target<R>(&mut self, depth: usize, f: impl FnOnce(&mut Self) -> R) -> R {
let direct = self.scope_stack.len() == 1;
let needs =
direct && self.dml_target.is_some() && self.d.in_subquery_limit_needs_derived_table();
let outer = self.derive_dml_target;
if needs {
self.derive_dml_target = Some(depth);
}
let r = f(self);
self.derive_dml_target = outer;
r
}
pub(super) fn push_param_typed(&mut self, value: SqlValue, cast: Option<BindCast>) {
let untyped = matches!(value, SqlValue::Null | SqlValue::RangeLiteral(_));
let p = self.bind(value);
self.sql.push_str(&p);
let ty = match cast {
Some(BindCast::Always(ty)) => Some(ty),
Some(BindCast::Untyped(ty)) if untyped => Some(ty),
_ => None,
};
if let Some(ty) = ty {
self.sql.push_str("::");
self.sql.push_str(ty);
}
}
pub(super) fn bind(&mut self, value: SqlValue) -> String {
let wrap: Option<fn(&dyn Dialect, &mut String, &str)> = match value {
SqlValue::Json(_) => Some(|d, s, p| d.write_json_param(s, p)),
SqlValue::Decimal(_) => Some(|d, s, p| d.write_decimal_param(s, p)),
_ => None,
};
self.params.push(value);
let p = self.d.placeholder(self.params.len());
let Some(wrap) = wrap else {
return p;
};
let mut wrapped = String::new();
wrap(self.d, &mut wrapped, &p);
wrapped
}
pub(super) fn push_param(&mut self, value: SqlValue) {
self.push_param_typed(value, None);
}
pub(super) fn finish(mut self) -> Result<CompiledStatement, SqlError> {
super::refuse_unbindable_values(self.d, &self.params)?;
if self.group_concat {
let start = self.sql.len() - self.sql.trim_start_matches('(').len();
let kw = ["SELECT", "INSERT", "UPDATE", "DELETE"]
.into_iter()
.find(|kw| self.sql[start..].starts_with(kw));
if let Some(kw) = kw {
self.sql.insert_str(
start + kw.len(),
" /*+ SET_VAR(group_concat_max_len = 4294967295) */",
);
}
}
Ok(CompiledStatement {
sql: self.sql,
params: self.params,
})
}
}
#[derive(Clone, Copy)]
pub(super) enum BindCast {
Untyped(&'static str),
Always(&'static str),
}
pub(super) fn null_cast_for(
d: &dyn Dialect,
model: &ModelSchema,
column: &str,
) -> Option<BindCast> {
let field = model.field_by_column(column)?;
if field.case_insensitive && field.ty == crate::core::FieldType::String {
if let Some(ty) = d.ci_text_bind_cast() {
return Some(BindCast::Always(ty));
}
}
d.null_cast(field.ty).map(BindCast::Untyped)
}
fn write_in_list(b: &mut Sql<'_>, op: Op, elements: &[SqlValue], cast: Option<BindCast>) {
let negated = matches!(op, Op::NotIn);
if binds_as_array(b.d, elements) {
b.sql.push_str(if negated { " <> ALL(" } else { " = ANY(" });
let p = b.bind(SqlValue::Array(elements.to_vec()));
b.sql.push_str(&p);
if let Some(BindCast::Always(ty)) = cast {
b.sql.push_str("::");
b.sql.push_str(ty);
b.sql.push_str("[]");
}
b.sql.push(')');
return;
}
b.sql.push_str(if negated { " NOT IN (" } else { " IN (" });
for (i, elem) in elements.iter().enumerate() {
if i > 0 {
b.sql.push_str(", ");
}
b.push_param_typed(elem.clone(), cast);
}
b.sql.push(')');
}
fn binds_as_array(d: &dyn Dialect, elements: &[SqlValue]) -> bool {
d.binds_pg_only_values() && array_bindable(elements)
}
pub(crate) fn in_list_binds(d: &dyn Dialect, elements: &[SqlValue]) -> usize {
if binds_as_array(d, elements) {
1
} else {
elements.len()
}
}
fn array_bindable(elements: &[SqlValue]) -> bool {
let Some(first) = elements.first() else {
return false;
};
matches!(
first,
SqlValue::I16(_)
| SqlValue::I32(_)
| SqlValue::I64(_)
| SqlValue::String(_)
| SqlValue::Bool(_)
| SqlValue::Uuid(_)
) && elements
.iter()
.all(|e| std::mem::discriminant(e) == std::mem::discriminant(first))
}
pub(super) fn write_select(b: &mut Sql<'_>, query: &SelectQuery) -> Result<(), SqlError> {
let scope = b.correlated_scope(query.model);
write_select_as(b, query, scope)
}
fn write_derived_select(b: &mut Sql<'_>, query: &SelectQuery) -> Result<(), SqlError> {
write_select_as(b, query, Scope::plain(query.model))
}
fn write_select_as(b: &mut Sql<'_>, query: &SelectQuery, scope: Scope) -> Result<(), SqlError> {
let top_level = b.scope_stack.is_empty();
b.scope_stack.push(scope);
let result = if query.compound.is_empty() {
write_select_inner(b, query, top_level)
} else {
write_compound_select(b, query)
};
b.scope_stack.pop();
result
}
pub(super) fn write_compound_with_total(
b: &mut Sql<'_>,
query: &SelectQuery,
) -> Result<(), SqlError> {
let merged = SelectQuery {
compound_order_by: Vec::new(),
compound_limit: None,
compound_offset: None,
lock_mode: None,
..query.clone()
};
let extra = order_cols_outside_projection(query);
if !extra.is_empty() {
return write_compound_ordered_outside(b, query, &extra, true);
}
b.sql.push_str("SELECT ");
b.write_ident("__rustango_page");
b.sql.push_str(".*, COUNT(*) OVER () AS ");
b.write_ident("__rustango_total");
b.sql.push_str(" FROM (");
write_select(b, &merged)?;
b.sql.push_str(") AS ");
b.write_ident("__rustango_page");
write_order_limit_offset(
b,
&query.compound_order_by,
query.compound_limit,
query.compound_offset,
)
}
pub(super) fn write_distinct_with_total(
b: &mut Sql<'_>,
query: &SelectQuery,
) -> Result<(), SqlError> {
b.total = Some(CountQuery::from_select(SelectQuery {
limit: None,
offset: None,
lock_mode: None,
..query.clone()
}));
write_select(b, query)
}
fn write_compound_select(b: &mut Sql<'_>, query: &SelectQuery) -> Result<(), SqlError> {
let extra = order_cols_outside_projection(query);
if !extra.is_empty() {
return write_compound_ordered_outside(b, query, &extra, false);
}
let SelectQuery {
model,
where_clause,
search,
joins,
subquery_joins,
order_by,
limit,
offset,
lock_mode: _,
compound: _,
projection,
distinct,
compound_order_by: _,
compound_limit: _,
compound_offset: _,
select_related: _,
} = query;
let head = SelectQuery {
where_clause: where_clause.clone(),
search: search.clone(),
joins: joins.clone(),
subquery_joins: subquery_joins.clone(),
order_by: order_by.clone(),
limit: *limit,
offset: *offset,
projection: projection.clone(),
distinct: distinct.clone(),
..SelectQuery::new(model)
};
let mut mixed = false;
let wraps: Vec<bool> = query
.compound
.iter()
.map(|branch| {
let intersect = branch.op == crate::core::SetOp::Intersection;
let wrap = intersect && mixed;
mixed = (mixed && !wrap) || !intersect;
wrap
})
.collect();
for _ in wraps.iter().filter(|w| **w) {
b.sql.push_str("SELECT * FROM (");
}
let head_scoped = !head.order_by.is_empty() || head.limit.is_some() || head.offset.is_some();
b.label_model_cols = true;
if head_scoped {
b.sql.push_str("SELECT * FROM (");
write_select_inner(b, &head, false)?;
b.sql.push(')');
b.sql.push_str(" AS ");
b.write_ident("__rustango_b0");
} else {
write_select_inner(b, &head, false)?;
}
for (i, branch) in query.compound.iter().enumerate() {
if wraps[i] {
b.sql.push_str(") AS ");
b.write_ident(&format!("__rustango_l{i}"));
}
b.sql.push(' ');
b.sql.push_str(branch.op.keyword());
b.sql.push(' ');
let head_frame = b.scope_stack.pop();
let frame = b.correlated_scope(branch.query.model);
b.scope_stack.push(frame);
let scoped = !branch.query.order_by.is_empty()
|| branch.query.limit.is_some()
|| branch.query.offset.is_some();
let r = if branch.query.compound.is_empty() {
if scoped {
b.sql.push_str("SELECT * FROM (");
let r = write_select_inner(b, &branch.query, false);
b.sql.push(')');
if r.is_ok() {
b.sql.push_str(" AS ");
b.write_ident(&format!("__rustango_b{}", i + 1));
}
r
} else {
write_select_inner(b, &branch.query, false)
}
} else {
b.sql.push_str("SELECT * FROM (");
let r = write_compound_select(b, &branch.query);
b.sql.push(')');
if r.is_ok() {
b.sql.push_str(" AS ");
b.write_ident(&format!("__rustango_b{}", i + 1));
}
r
};
b.scope_stack.pop();
b.scope_stack.extend(head_frame);
r?;
}
write_order_limit_offset(
b,
&query.compound_order_by,
query.compound_limit,
query.compound_offset,
)?;
if let Some(lock) = &query.lock_mode {
write_lock_clause(b, lock);
}
Ok(())
}
fn order_cols_outside_projection(query: &SelectQuery) -> Vec<&'static str> {
let Some(projection) = &query.projection else {
return Vec::new();
};
let mut extra = Vec::new();
for item in &query.compound_order_by {
if let crate::core::OrderItem::Column { column, .. } = item {
if !projection.contains(column)
&& !extra.contains(column)
&& query.model.field_by_column(column).is_some()
{
extra.push(*column);
}
}
}
extra
}
fn write_compound_ordered_outside(
b: &mut Sql<'_>,
query: &SelectQuery,
extra: &[&'static str],
with_total: bool,
) -> Result<(), SqlError> {
fn dedupes(q: &SelectQuery) -> bool {
matches!(q.distinct, Some(crate::core::DistinctMode::All))
|| q.compound
.iter()
.any(|branch| branch.op != crate::core::SetOp::UnionAll || dedupes(&branch.query))
}
if dedupes(query) {
return Err(SqlError::SetOpOrderOutsideProjection { column: extra[0] });
}
fn widen(q: &mut SelectQuery, extra: &[&'static str]) {
if let Some(projection) = &mut q.projection {
for col in extra {
if !projection.contains(col) {
projection.push(col);
}
}
}
for branch in &mut q.compound {
widen(&mut branch.query, extra);
}
}
let mut merged = SelectQuery {
compound_order_by: Vec::new(),
compound_limit: None,
compound_offset: None,
lock_mode: None,
..query.clone()
};
widen(&mut merged, extra);
b.sql.push_str("SELECT ");
let labels = query
.projection
.iter()
.flatten()
.map(|col| match col.split_once('.') {
Some((alias, c)) => crate::core::joins::joined_label(alias, c),
None => (*col).to_owned(),
})
.chain(query.joins.iter().flat_map(|j| {
j.project
.iter()
.map(|c| crate::core::joins::joined_label(j.alias, c))
}));
for (i, label) in labels.enumerate() {
if i > 0 {
b.sql.push_str(", ");
}
b.write_ident(&label);
}
if with_total {
b.sql.push_str(", COUNT(*) OVER () AS ");
b.write_ident("__rustango_total");
}
b.sql.push_str(" FROM (");
write_compound_select(b, &merged)?;
b.sql.push_str(") AS ");
b.write_ident("__rustango_set");
write_order_limit_offset(
b,
&query.compound_order_by,
query.compound_limit,
query.compound_offset,
)?;
if let Some(lock) = query.lock_mode.as_ref().filter(|_| !with_total) {
write_lock_clause(b, lock);
}
Ok(())
}
fn write_joined_projection(b: &mut Sql<'_>, col: &str) -> bool {
let Some((alias, c)) = col.split_once('.') else {
return false;
};
b.write_alias(alias);
b.sql.push('.');
b.write_ident(c);
b.sql.push_str(" AS ");
b.write_ident(&crate::core::joins::joined_label(alias, c));
true
}
fn write_distinct_on_via_window(
b: &mut Sql<'_>,
query: &SelectQuery,
distinct_cols: &[&'static str],
) -> Result<(), SqlError> {
let qualify = !query.joins.is_empty() || !query.subquery_joins.is_empty();
let scope = b.scope_or(query.model);
b.sql.push_str("SELECT ");
let mut first_col = true;
if let Some(cols) = query.projection.as_ref() {
for col in cols {
if !first_col {
b.sql.push_str(", ");
}
first_col = false;
match col.split_once('.') {
Some((alias, c)) => b.write_ident(&crate::core::joins::joined_label(alias, c)),
None => b.write_ident(col),
}
}
} else {
for field in query.model.scalar_fields() {
if !first_col {
b.sql.push_str(", ");
}
first_col = false;
b.write_ident(field.column);
}
}
for join in &query.joins {
for col in &join.project {
b.sql.push_str(", ");
b.write_ident(&crate::core::joins::joined_label(join.alias, col));
}
}
if let Some(child) = b.child_count.take() {
write_child_count(b, "sub", child);
}
b.sql.push_str(" FROM (");
b.sql.push_str("SELECT ");
let mut inner_first = true;
if let Some(cols) = query.projection.as_ref() {
for col in cols {
if !inner_first {
b.sql.push_str(", ");
}
inner_first = false;
if !write_joined_projection(b, col) {
write_model_col(b, scope.name, col, qualify, false);
}
}
} else {
for field in query.model.scalar_fields() {
if !inner_first {
b.sql.push_str(", ");
}
inner_first = false;
write_model_col(b, scope.name, field.column, qualify, false);
}
}
for join in &query.joins {
for col in &join.project {
b.sql.push_str(", ");
b.write_alias(join.alias);
b.sql.push('.');
b.write_ident(col);
b.sql.push_str(" AS ");
b.write_ident(&crate::core::joins::joined_label(join.alias, col));
}
}
for (i, item) in query.order_by.iter().enumerate() {
if !matches!(item, crate::core::OrderItem::Random) {
b.sql.push_str(", ");
write_order_target(b, item)?;
b.sql.push_str(" AS ");
b.write_ident(&distinct_on_order_label(i));
}
}
b.sql.push_str(", ROW_NUMBER() OVER (PARTITION BY ");
for (i, col) in distinct_cols.iter().enumerate() {
if i > 0 {
b.sql.push_str(", ");
}
write_model_col(b, scope.name, col, qualify, false);
}
write_order_limit_offset_with(b, &query.order_by, None, None, |b, _, item| {
if let crate::core::OrderItem::Column { column, .. } = item {
write_model_col(b, scope.name, column, qualify, false);
Ok(())
} else {
write_order_target(b, item)
}
})?;
b.sql.push_str(") AS __rn FROM ");
scope.write_from(b);
write_model_joins(b, &query.joins)?;
write_subquery_joins(b, &query.subquery_joins)?;
write_where_with_search(
b,
&query.where_clause,
query.search.as_ref(),
Some(query.model),
)?;
b.sql.push_str(") sub WHERE sub.__rn = 1");
b.with_qualifier(None, |b| {
write_order_limit_offset_with(
b,
&query.order_by,
query.limit,
query.offset,
|b, i, item| {
if matches!(item, crate::core::OrderItem::Random) {
return write_order_target(b, item);
}
b.write_ident(&distinct_on_order_label(i));
Ok(())
},
)
})?;
Ok(())
}
fn distinct_on_order_label(i: usize) -> String {
format!("__rustango_order{i}")
}
fn write_subquery_joins(
b: &mut Sql<'_>,
subquery_joins: &[crate::core::SubqueryJoin],
) -> Result<(), SqlError> {
for sj in subquery_joins {
use crate::core::JoinKind;
let kind_kw = match sj.kind {
JoinKind::Inner => "INNER JOIN",
JoinKind::Left => "LEFT JOIN",
JoinKind::Right => {
return Err(SqlError::JoinKindNotSupported {
kind: "RIGHT (subquery)",
dialect: b.d.name(),
});
}
JoinKind::Full => {
return Err(SqlError::JoinKindNotSupported {
kind: "FULL (subquery)",
dialect: b.d.name(),
});
}
};
b.sql.push(' ');
b.sql.push_str(kind_kw);
if sj.lateral {
if b.d.name() == "sqlite" {
return Err(SqlError::LateralJoinNotSupported {
dialect: b.d.name(),
});
}
b.sql.push_str(" LATERAL");
}
b.sql.push_str(" (");
match (&sj.subquery, sj.lateral) {
(crate::core::DerivedSource::Select(s), true) => write_select(b, s)?,
(crate::core::DerivedSource::Select(s), false) => write_derived_select(b, s)?,
(crate::core::DerivedSource::Aggregate(a), true) => write_aggregate(b, a)?,
(crate::core::DerivedSource::Aggregate(a), false) => {
write_aggregate_as(b, a, Scope::plain(a.model))?;
}
}
b.sql.push_str(") AS ");
b.write_ident(sj.alias);
if sj.on.is_empty() {
b.sql.push_str(" ON true");
} else {
b.sql.push_str(" ON ");
let alias = sj.alias;
b.with_qualifier(Some(Qualifier::Join(alias)), |b| {
write_where_expr(b, &sj.on, None)
})?;
}
}
Ok(())
}
fn write_select_inner(
b: &mut Sql<'_>,
query: &SelectQuery,
order_helpers: bool,
) -> Result<(), SqlError> {
with_join_types(b, &query.joins, None, |b| {
write_select_body(b, query, order_helpers)
})
}
fn with_join_types(
b: &mut Sql<'_>,
joins: &[crate::core::Join],
source: Option<&SelectQuery>,
f: impl FnOnce(&mut Sql<'_>) -> Result<(), SqlError>,
) -> Result<(), SqlError> {
let mark = b.join_types.len();
let depth = b.scope_stack.len();
let src = source.map_or(&[][..], |s| &s.joins[..]);
for j in joins.iter().chain(src) {
b.join_types.push((depth, j.alias, j.target));
}
let r = f(b);
b.join_types.truncate(mark);
r
}
fn write_select_body(
b: &mut Sql<'_>,
query: &SelectQuery,
order_helpers: bool,
) -> Result<(), SqlError> {
let qualify = !query.joins.is_empty() || !query.subquery_joins.is_empty();
let q = qualify.then_some(Qualifier::Base(b.scope_or(query.model).name));
b.with_qualifier(q, |b| {
write_select_body_qualified(b, query, qualify, order_helpers)
})
}
fn write_select_body_qualified(
b: &mut Sql<'_>,
query: &SelectQuery,
qualify: bool,
order_helpers: bool,
) -> Result<(), SqlError> {
let label = std::mem::take(&mut b.label_model_cols) && qualify;
if let Some(crate::core::DistinctMode::On(cols)) = &query.distinct {
if b.d.name() != "postgres" {
return write_distinct_on_via_window(b, query, cols);
}
}
let scope = b.scope_or(query.model);
b.sql.push_str("SELECT ");
if let Some(distinct) = &query.distinct {
match distinct {
crate::core::DistinctMode::All => b.sql.push_str("DISTINCT "),
crate::core::DistinctMode::On(cols) => {
b.sql.push_str("DISTINCT ON (");
for (i, col) in cols.iter().enumerate() {
if i > 0 {
b.sql.push_str(", ");
}
b.write_ident(col);
}
b.sql.push_str(") ");
}
}
}
let mut first_col = true;
if let Some(cols) = query.projection.as_ref() {
for col in cols {
if !first_col {
b.sql.push_str(", ");
}
first_col = false;
if write_joined_projection(b, col) {
continue;
}
write_model_col(b, scope.name, col, qualify, label);
}
} else {
for field in query.model.scalar_fields() {
if !first_col {
b.sql.push_str(", ");
}
first_col = false;
write_model_col(b, scope.name, field.column, qualify, label);
}
}
for join in &query.joins {
for col in &join.project {
b.sql.push_str(", ");
b.write_alias(join.alias);
b.sql.push('.');
b.write_ident(col);
b.sql.push_str(" AS ");
b.write_ident(&crate::core::joins::joined_label(join.alias, col));
}
}
let needs_helpers = query.distinct.is_some() && distinct_order_helpers(query).next().is_some();
if order_helpers && needs_helpers {
write_distinct_order_helpers(b, query)?;
}
if let Some(count) = b.total.take() {
b.sql.push_str(", (");
write_count(b, &count)?;
b.sql.push_str(") AS ");
b.write_ident("__rustango_total");
}
if let Some(child) = b.child_count.take() {
write_child_count(b, scope.name, child);
}
b.sql.push_str(" FROM ");
scope.write_from(b);
write_model_joins(b, &query.joins)?;
write_subquery_joins(b, &query.subquery_joins)?;
write_where_with_search(
b,
&query.where_clause,
query.search.as_ref(),
Some(query.model),
)?;
let order_by: &[crate::core::OrderItem] =
if needs_helpers && !order_helpers && query.limit.is_none() && query.offset.is_none() {
&[]
} else {
&query.order_by
};
write_order_limit_offset(b, order_by, query.limit, query.offset)?;
if let Some(lock) = &query.lock_mode {
write_lock_clause(b, lock);
}
Ok(())
}
fn write_model_col(b: &mut Sql<'_>, table: &str, col: &str, qualify: bool, label: bool) {
if qualify {
b.write_ident(table);
b.sql.push('.');
}
b.write_ident(col);
if label {
b.sql.push_str(" AS ");
b.write_ident(col);
}
}
fn write_distinct_order_helpers(b: &mut Sql<'_>, query: &SelectQuery) -> Result<(), SqlError> {
for (i, expr) in distinct_order_helpers(query) {
b.sql.push_str(", ");
write_expr(b, &expr, None)?;
b.sql.push_str(" AS ");
b.write_ident(&format!("__rustango_order{i}"));
}
Ok(())
}
fn distinct_order_helpers(
query: &SelectQuery,
) -> impl Iterator<Item = (usize, crate::core::Expr)> + '_ {
use crate::core::{Expr, OrderItem};
query
.order_by
.iter()
.enumerate()
.filter_map(|(i, item)| match item {
OrderItem::Expr {
expr: expr @ Expr::AliasedColumn { alias, column },
..
} => {
let projected = query
.joins
.iter()
.any(|j| j.alias == *alias && j.project.contains(column));
(!projected).then(|| (i, expr.clone()))
}
OrderItem::Column { column, .. } => {
let projected = query
.projection
.as_ref()
.is_none_or(|cols| cols.contains(column));
(!projected).then_some((i, Expr::Column(column)))
}
_ => None,
})
}
fn write_lock_clause(b: &mut Sql<'_>, lock: &crate::core::LockMode) {
if b.d.name() == "sqlite" {
if !lock.silent_on_sqlite {
tracing::warn!(
target: "rustango::sql::lock",
skip_locked = lock.skip_locked,
nowait = lock.nowait,
"select_for_update modifier dropped — SQLite has no row-level lock syntax. \
The transaction's implicit global writer lock applies. \
Set `LockMode {{ silent_on_sqlite: true, .. }}` (or call \
`.silent_on_sqlite()` on the QuerySet) to suppress this warning."
);
}
return;
}
b.sql.push_str(" FOR ");
if lock.no_key && b.d.name() == "postgres" {
b.sql.push_str("NO KEY UPDATE");
} else {
b.sql.push_str("UPDATE");
}
if !lock.of.is_empty() {
b.sql.push_str(" OF ");
let mut first = true;
for t in &lock.of {
if !first {
b.sql.push_str(", ");
}
first = false;
b.sql.push_str(&b.d.quote_ident(t));
}
}
if lock.skip_locked {
b.sql.push_str(" SKIP LOCKED");
} else if lock.nowait {
b.sql.push_str(" NOWAIT");
}
}
pub(super) fn write_count(b: &mut Sql<'_>, query: &CountQuery) -> Result<(), SqlError> {
let scope = Scope::plain(query.model);
b.scope_stack.push(scope);
let r = (|| {
b.sql.push_str("SELECT COUNT(*)");
write_from_source(b, scope, query.source.as_deref())?;
write_where_with_search(
b,
&query.where_clause,
query.search.as_ref(),
Some(query.model),
)?;
Ok(())
})();
b.scope_stack.pop();
r
}
fn write_from_source(
b: &mut Sql<'_>,
scope: Scope,
source: Option<&SelectQuery>,
) -> Result<(), SqlError> {
b.sql.push_str(" FROM ");
if let Some(sub) = source {
b.sql.push('(');
write_derived_select(b, sub)?;
b.sql.push_str(") AS ");
b.write_ident(scope.name);
} else {
scope.write_from(b);
}
Ok(())
}
fn write_model_joins(b: &mut Sql<'_>, joins: &[crate::core::Join]) -> Result<(), SqlError> {
use crate::core::JoinKind;
for join in joins {
let kind_kw = match (join.kind, b.d.name()) {
(JoinKind::Inner, _) => "INNER JOIN",
(JoinKind::Left, _) => "LEFT JOIN",
(JoinKind::Right, "sqlite") => {
return Err(SqlError::JoinKindNotSupported {
kind: "RIGHT",
dialect: b.d.name(),
});
}
(JoinKind::Right, _) => "RIGHT JOIN",
(JoinKind::Full, "postgres") => "FULL OUTER JOIN",
(JoinKind::Full, _) => {
return Err(SqlError::JoinKindNotSupported {
kind: "FULL",
dialect: b.d.name(),
});
}
};
if join.on.is_empty() {
return Err(SqlError::EmptyJoinOnCondition);
}
b.sql.push(' ');
b.sql.push_str(kind_kw);
b.sql.push(' ');
b.write_table(
join.target.table,
Some(&crate::core::joins::sql_label(join.alias)),
);
b.sql.push_str(" ON ");
b.with_qualifier(Some(Qualifier::Join(join.alias)), |b| {
write_where_expr(b, &join.on, Some(join.target))
})?;
}
Ok(())
}
pub(super) fn write_aggregate(b: &mut Sql<'_>, query: &AggregateQuery) -> Result<(), SqlError> {
let scope = b.correlated_scope(query.model);
write_aggregate_as(b, query, scope)
}
fn write_aggregate_as(
b: &mut Sql<'_>,
query: &AggregateQuery,
scope: Scope,
) -> Result<(), SqlError> {
b.scope_stack.push(scope);
let r = with_join_types(b, &query.joins, query.source.as_deref(), |b| {
write_aggregate_inner(b, query, scope)
});
b.scope_stack.pop();
r
}
fn write_agg_group_col(
b: &mut Sql<'_>,
col: &str,
scope: Scope,
has_joins: bool,
derived: bool,
project: bool,
) {
let model_table = scope.name;
if let Some((alias, c)) = col.split_once('.') {
let flat = crate::core::joins::joined_label(alias, c);
if derived && alias != scope.model.table {
b.write_ident(model_table);
b.sql.push('.');
b.write_ident(&flat);
} else {
let alias = b.alias_ref(scope.rename(alias)).into_owned();
b.write_ident(&alias);
b.sql.push('.');
b.write_ident(c);
}
if project {
b.sql.push_str(" AS ");
b.write_ident(&flat);
}
} else if has_joins {
b.write_ident(model_table);
b.sql.push('.');
b.write_ident(col);
if project {
b.sql.push_str(" AS ");
b.write_ident(col);
}
} else {
b.write_ident(col);
}
}
fn write_aggregate_inner(
b: &mut Sql<'_>,
query: &AggregateQuery,
scope: Scope,
) -> Result<(), SqlError> {
b.sql.push_str("SELECT ");
let has_joins = !query.joins.is_empty();
let derived = query.source.is_some();
for (i, col) in query.group_by.iter().enumerate() {
if i > 0 {
b.sql.push_str(", ");
}
write_agg_group_col(b, col, scope, has_joins, derived, true);
}
let q = has_joins.then_some(Qualifier::Base(scope.name));
b.with_qualifier(q, |b| {
for (i, (alias, expr)) in query.aggregates.iter().enumerate() {
if !query.group_by.is_empty() || i > 0 {
b.sql.push_str(", ");
}
write_aggregate_expr(b, expr, query.model)?;
b.sql.push_str(" AS ");
b.write_ident(alias.as_ref());
}
Ok::<_, SqlError>(())
})?;
write_from_source(b, scope, query.source.as_deref())?;
write_model_joins(b, &query.joins)?;
b.with_qualifier(q, |b| {
write_where(b, &query.where_clause, Some(query.model))
})?;
if !query.group_by.is_empty() {
b.sql.push_str(" GROUP BY ");
for (i, col) in query.group_by.iter().enumerate() {
if i > 0 {
b.sql.push_str(", ");
}
write_agg_group_col(b, col, scope, has_joins, derived, false);
}
}
let prev_derived = b.derived_joins.take();
if let Some(src) = query.source.as_deref() {
b.derived_joins = Some(DerivedJoins {
depth: b.scope_stack.len(),
table: scope.name,
aliases: src.joins.iter().map(|j| j.alias).collect(),
});
}
let r = b.with_qualifier(q, |b| write_aggregate_tail(b, query));
b.derived_joins = prev_derived;
r
}
fn write_aggregate_tail(b: &mut Sql<'_>, query: &AggregateQuery) -> Result<(), SqlError> {
if let Some(having) = &query.having {
b.sql.push_str(" HAVING ");
let prev = b.aggregate_allowed;
b.aggregate_allowed = true;
let r = write_where_expr(b, having, Some(query.model));
b.aggregate_allowed = prev;
r?;
}
let prev = b.aggregate_allowed;
b.aggregate_allowed = true;
let r = write_order_limit_offset(b, &query.order_by, query.limit, query.offset);
b.aggregate_allowed = prev;
r
}
#[derive(Debug, Clone, Copy)]
enum AggCast {
Int,
Float,
}
fn aggregate_cast_kind(b: &Sql<'_>, expr: &AggregateExpr) -> Option<AggCast> {
match expr {
AggregateExpr::Sum(col) => sum_cast(
b.scope_model()
.and_then(|m| m.field_by_column(col))
.map(|f| f.ty),
),
AggregateExpr::Avg(_)
| AggregateExpr::StdDev(_)
| AggregateExpr::StdDevPop(_)
| AggregateExpr::Variance(_)
| AggregateExpr::VariancePop(_) => Some(AggCast::Float),
_ => None,
}
}
fn sum_cast(ty: Option<crate::core::FieldType>) -> Option<AggCast> {
use crate::core::FieldType;
match ty {
Some(FieldType::F32 | FieldType::F64) => Some(AggCast::Float),
Some(FieldType::Decimal) => None,
_ => Some(AggCast::Int),
}
}
fn apply_agg_cast(d: &dyn Dialect, kind: AggCast, inner: &str) -> String {
match kind {
AggCast::Int => d.cast_aggregate_to_int(inner),
AggCast::Float => d.cast_aggregate_to_float(inner),
}
}
fn render_agg_order_by(b: &Sql<'_>, order_by: &[crate::core::OrderClause]) -> String {
if order_by.is_empty() {
return String::new();
}
let mut s = String::from(" ORDER BY ");
for (i, o) in order_by.iter().enumerate() {
if i > 0 {
s.push_str(", ");
}
s.push_str(&render_qualified_col(b, o.column));
if o.desc {
s.push_str(" DESC");
}
}
s
}
fn format_bare_aggregate(b: &Sql<'_>, expr: &AggregateExpr) -> Result<String, SqlError> {
let col_sql = |col: &str| render_qualified_col(b, col);
Ok(match expr {
AggregateExpr::Count(None) => "COUNT(*)".into(),
AggregateExpr::Count(Some(col)) => format!("COUNT({})", col_sql(col)),
AggregateExpr::CountDistinct(col) => {
format!("COUNT(DISTINCT {})", col_sql(col))
}
AggregateExpr::Sum(col) => format!("SUM({})", col_sql(col)),
AggregateExpr::Avg(col) => format!("AVG({})", col_sql(col)),
AggregateExpr::Max(col) => format!("MAX({})", col_sql(col)),
AggregateExpr::Min(col) => format!("MIN({})", col_sql(col)),
AggregateExpr::AnyValue(col) => {
let ident = col_sql(col);
match b.d.name() {
"mysql" => format!("ANY_VALUE({ident})"),
"sqlite" => format!("min({ident})"),
_ => format!("any_value({ident})"),
}
}
AggregateExpr::StdDev(col)
| AggregateExpr::StdDevPop(col)
| AggregateExpr::Variance(col)
| AggregateExpr::VariancePop(col) => {
if b.d.name() == "sqlite" {
return Err(SqlError::AggregateNotSupported {
aggregate: stddev_variance_name(expr),
dialect: b.d.name(),
});
}
format!("{}({})", stddev_variance_name(expr), col_sql(col))
}
AggregateExpr::Filtered { .. } | AggregateExpr::Coalesced { .. } => {
return Err(SqlError::NestedAggregateWrapper {
wrapper: "wrapper at format_bare_aggregate site",
});
}
AggregateExpr::Window(_) => {
return Err(SqlError::NestedAggregateWrapper {
wrapper: "Window at format_bare_aggregate site",
});
}
AggregateExpr::ArrayAgg { .. }
| AggregateExpr::StringAgg { .. }
| AggregateExpr::JsonbAgg { .. } => {
return Err(SqlError::NestedAggregateWrapper {
wrapper: "PG-aggregate at format_bare_aggregate site",
});
}
AggregateExpr::RelatedAggregate(_) => {
return Err(SqlError::NestedAggregateWrapper {
wrapper: "RelatedAggregate at format_bare_aggregate site",
});
}
})
}
fn write_aggregate_expr(
b: &mut Sql<'_>,
expr: &AggregateExpr,
model: &'static ModelSchema,
) -> Result<(), SqlError> {
match expr {
AggregateExpr::Coalesced { inner, default } => {
if matches!(inner.as_ref(), AggregateExpr::Coalesced { .. }) {
return Err(SqlError::NestedAggregateWrapper {
wrapper: "Coalesced",
});
}
b.sql.push_str("COALESCE(");
write_aggregate_expr(b, inner, model)?;
b.sql.push_str(", ");
let cast = aggregate_column(inner).and_then(|c| null_cast_for(b.d, model, c));
b.push_param_typed(default.clone(), cast);
b.sql.push(')');
Ok(())
}
AggregateExpr::Filtered { inner, filter } => {
if matches!(inner.as_ref(), AggregateExpr::Filtered { .. }) {
return Err(SqlError::NestedAggregateWrapper {
wrapper: "Filtered",
});
}
if matches!(inner.as_ref(), AggregateExpr::Coalesced { .. }) {
return Err(SqlError::NestedAggregateWrapper {
wrapper: "Filtered(Coalesced)",
});
}
if matches!(inner.as_ref(), AggregateExpr::Window(_)) {
return Err(SqlError::NestedAggregateWrapper {
wrapper: "Filtered(Window)",
});
}
if b.d.name() == "mysql" {
return write_aggregate_as_case_when(b, inner, filter);
}
let bare = format_bare_aggregate(b, inner)?;
let prior = b.sql.len();
b.sql.push_str(&bare);
b.sql.push_str(" FILTER (WHERE ");
write_where_expr(b, filter, Some(model))?;
b.sql.push(')');
if let Some(kind) = aggregate_cast_kind(b, inner) {
let emitted = b.sql[prior..].to_string();
b.sql.truncate(prior);
let wrapped = apply_agg_cast(b.d, kind, &format!("({emitted})"));
b.sql.push_str(&wrapped);
}
Ok(())
}
AggregateExpr::Window(w) => write_window_expr(b, w),
AggregateExpr::ArrayAgg { column, distinct } => {
if b.d.name() != "postgres" {
return Err(SqlError::AggregateNotSupportedInDialect {
aggregate: "array_agg",
dialect: b.d.name(),
});
}
b.sql.push_str("array_agg(");
if *distinct {
b.sql.push_str("DISTINCT ");
}
b.write_scope_col(column);
b.sql.push(')');
Ok(())
}
AggregateExpr::StringAgg {
column,
delimiter,
distinct,
order_by,
} => {
if *distinct && order_by.iter().any(|o| o.column != *column) {
return Err(SqlError::AggregateNotSupportedInDialect {
aggregate: "string_agg(DISTINCT) ORDER BY a non-aggregated column",
dialect: b.d.name(),
});
}
let order_sql = render_agg_order_by(b, order_by);
match b.d.name() {
"mysql" => {
b.group_concat = true;
b.sql.push_str("GROUP_CONCAT(");
if *distinct {
b.sql.push_str("DISTINCT ");
}
b.write_scope_col(column);
b.sql.push_str(&order_sql);
b.sql.push_str(" SEPARATOR ");
b.sql.push_str(&b.d.quote_literal(delimiter));
b.sql.push(')');
}
"sqlite" => {
if *distinct {
if delimiter.as_str() != "," {
return Err(SqlError::AggregateNotSupportedInDialect {
aggregate: "string_agg(DISTINCT) with a custom delimiter (SQLite group_concat DISTINCT takes one arg)",
dialect: b.d.name(),
});
}
b.sql.push_str("group_concat(DISTINCT ");
b.write_scope_col(column);
b.sql.push_str(&order_sql);
b.sql.push(')');
} else {
b.sql.push_str("group_concat(");
b.write_scope_col(column);
b.sql.push_str(", ");
b.push_param(crate::core::SqlValue::String(delimiter.clone()));
b.sql.push_str(&order_sql);
b.sql.push(')');
}
}
_ => {
b.sql.push_str("string_agg(");
if *distinct {
b.sql.push_str("DISTINCT ");
}
b.write_scope_col(column);
b.sql.push_str(", ");
b.push_param(crate::core::SqlValue::String(delimiter.clone()));
b.sql.push_str(&order_sql);
b.sql.push(')');
}
}
Ok(())
}
AggregateExpr::JsonbAgg { column } => {
if b.d.name() != "postgres" {
return Err(SqlError::AggregateNotSupportedInDialect {
aggregate: "jsonb_agg",
dialect: b.d.name(),
});
}
b.sql.push_str("jsonb_agg(");
b.write_scope_col(column);
b.sql.push(')');
Ok(())
}
AggregateExpr::RelatedAggregate(e) => write_expr(b, e, None),
_ => write_aggregate_kind(b, expr),
}
}
fn write_aggregate_kind(b: &mut Sql<'_>, expr: &AggregateExpr) -> Result<(), SqlError> {
let bare = format_bare_aggregate(b, expr)?;
let out = match aggregate_cast_kind(b, expr) {
Some(kind) => apply_agg_cast(b.d, kind, &bare),
None => bare,
};
b.sql.push_str(&out);
Ok(())
}
fn write_aggregate_as_case_when(
b: &mut Sql<'_>,
inner: &AggregateExpr,
filter: &WhereExpr,
) -> Result<(), SqlError> {
let (agg_kw, case_then, distinct_prefix) = match inner {
AggregateExpr::Count(None) => ("COUNT", None, ""),
AggregateExpr::Count(Some(col)) => ("COUNT", Some(*col), ""),
AggregateExpr::CountDistinct(col) => ("COUNT", Some(*col), "DISTINCT "),
AggregateExpr::Sum(col) => ("SUM", Some(*col), ""),
AggregateExpr::Avg(col) => ("AVG", Some(*col), ""),
AggregateExpr::Max(col) => ("MAX", Some(*col), ""),
AggregateExpr::Min(col) => ("MIN", Some(*col), ""),
AggregateExpr::AnyValue(col) => ("ANY_VALUE", Some(*col), ""),
AggregateExpr::StdDev(col)
| AggregateExpr::StdDevPop(col)
| AggregateExpr::Variance(col)
| AggregateExpr::VariancePop(col) => (stddev_variance_name(inner), Some(*col), ""),
AggregateExpr::Filtered { .. } | AggregateExpr::Coalesced { .. } => {
return Err(SqlError::NestedAggregateWrapper {
wrapper: "wrapper inside Filtered fallback",
});
}
AggregateExpr::Window(_) => {
return Err(SqlError::NestedAggregateWrapper {
wrapper: "Filtered(Window)",
});
}
AggregateExpr::ArrayAgg { .. }
| AggregateExpr::StringAgg { .. }
| AggregateExpr::JsonbAgg { .. } => {
return Err(SqlError::NestedAggregateWrapper {
wrapper: "Filtered(PG-aggregate)",
});
}
AggregateExpr::RelatedAggregate(_) => {
return Err(SqlError::NestedAggregateWrapper {
wrapper: "Filtered(RelatedAggregate)",
});
}
};
let prior = b.sql.len();
b.sql.push_str(agg_kw);
b.sql.push('(');
b.sql.push_str(distinct_prefix);
b.sql.push_str("CASE WHEN ");
write_where_expr(b, filter, None)?;
b.sql.push_str(" THEN ");
match case_then {
Some(col) => b.write_scope_col(col),
None => b.sql.push('1'),
}
b.sql.push_str(" END)");
if let Some(kind) = aggregate_cast_kind(b, inner) {
let emitted = b.sql[prior..].to_string();
b.sql.truncate(prior);
let wrapped = apply_agg_cast(b.d, kind, &emitted);
b.sql.push_str(&wrapped);
}
Ok(())
}
fn aggregate_column(expr: &AggregateExpr) -> Option<&'static str> {
match expr {
AggregateExpr::Count(c) => *c,
AggregateExpr::CountDistinct(c)
| AggregateExpr::Sum(c)
| AggregateExpr::Avg(c)
| AggregateExpr::Max(c)
| AggregateExpr::Min(c)
| AggregateExpr::AnyValue(c)
| AggregateExpr::StdDev(c)
| AggregateExpr::StdDevPop(c)
| AggregateExpr::Variance(c)
| AggregateExpr::VariancePop(c) => Some(c),
AggregateExpr::ArrayAgg { column, .. }
| AggregateExpr::StringAgg { column, .. }
| AggregateExpr::JsonbAgg { column } => Some(column),
AggregateExpr::Filtered { inner, .. } | AggregateExpr::Coalesced { inner, .. } => {
aggregate_column(inner)
}
AggregateExpr::Window(w) => w.args.iter().find_map(|a| match a {
crate::core::Expr::Column(c) => Some(*c),
_ => None,
}),
AggregateExpr::RelatedAggregate(_) => None,
}
}
fn stddev_variance_name(expr: &AggregateExpr) -> &'static str {
match expr {
AggregateExpr::StdDev(_) => "STDDEV_SAMP",
AggregateExpr::StdDevPop(_) => "STDDEV_POP",
AggregateExpr::Variance(_) => "VAR_SAMP",
AggregateExpr::VariancePop(_) => "VAR_POP",
_ => "(unknown)", }
}
pub(super) fn write_insert(b: &mut Sql<'_>, query: &InsertQuery) -> Result<(), SqlError> {
if query.columns.is_empty() && query.returning.is_empty() {
return Err(SqlError::EmptyInsert);
}
write_insert_rows(b, query)?;
write_returning(b, &query.returning)
}
pub(super) fn write_insert_rows(b: &mut Sql<'_>, query: &InsertQuery) -> Result<(), SqlError> {
if query.columns.len() != query.values.len() {
return Err(SqlError::InsertShapeMismatch {
columns: query.columns.len(),
values: query.values.len(),
});
}
b.sql.push_str("INSERT INTO ");
b.write_ident(query.model.table);
if query.columns.is_empty() {
b.sql.push_str(b.d.default_values_clause());
} else {
b.sql.push_str(" (");
let mut first = true;
for col in &query.columns {
if !first {
b.sql.push_str(", ");
}
first = false;
b.write_ident(col);
}
b.sql.push_str(") VALUES (");
let mut first = true;
for (col, value) in query.columns.iter().zip(&query.values) {
if !first {
b.sql.push_str(", ");
}
first = false;
let cast = null_cast_for(b.d, query.model, col);
b.push_param_typed(value.clone(), cast);
}
b.sql.push(')');
}
if let Some(conflict) = &query.on_conflict {
write_on_conflict(b, query.model, &query.columns, &query.generated, conflict)?;
}
Ok(())
}
fn write_on_conflict(
b: &mut Sql<'_>,
model: &ModelSchema,
columns: &[&'static str],
generated: &[&str],
conflict: &crate::core::ConflictClause,
) -> Result<(), SqlError> {
if let crate::core::ConflictClause::DoUpdate { update_columns, .. } = conflict {
if update_columns.is_empty() {
return Err(SqlError::EmptyUpdateSet);
}
}
let set = caller_set(columns, generated);
b.d.write_conflict_clause(&mut b.sql, model, &set, conflict)
}
fn caller_set(columns: &[&'static str], generated: &[&str]) -> Vec<&'static str> {
columns
.iter()
.copied()
.filter(|c| !generated.contains(c))
.collect()
}
pub(super) fn write_bulk_insert(b: &mut Sql<'_>, query: &BulkInsertQuery) -> Result<(), SqlError> {
if query.rows.is_empty() {
return Err(SqlError::EmptyBulkInsert);
}
if query.columns.is_empty() && query.returning.is_empty() {
return Err(SqlError::EmptyInsert);
}
for row in &query.rows {
if row.len() != query.columns.len() {
return Err(SqlError::InsertShapeMismatch {
columns: query.columns.len(),
values: row.len(),
});
}
}
b.sql.push_str("INSERT INTO ");
b.write_ident(query.model.table);
if query.columns.is_empty() {
let pk = query
.returning
.first()
.copied()
.ok_or(SqlError::EmptyInsert)?;
let cell = query
.model
.field_by_column(pk)
.map_or("DEFAULT", |f| b.d.default_pk_cell(f));
b.sql.push_str(" (");
b.write_ident(pk);
b.sql.push_str(") VALUES ");
let mut first_row = true;
for _ in &query.rows {
if !first_row {
b.sql.push_str(", ");
}
first_row = false;
b.sql.push('(');
b.sql.push_str(cell);
b.sql.push(')');
}
} else {
b.sql.push_str(" (");
let mut first = true;
for col in &query.columns {
if !first {
b.sql.push_str(", ");
}
first = false;
b.write_ident(col);
}
b.sql.push_str(") VALUES ");
let mut first_row = true;
for row in &query.rows {
if !first_row {
b.sql.push_str(", ");
}
first_row = false;
b.sql.push('(');
let mut first_v = true;
for (col, value) in query.columns.iter().zip(row) {
if !first_v {
b.sql.push_str(", ");
}
first_v = false;
let cast = null_cast_for(b.d, query.model, col);
b.push_param_typed(value.clone(), cast);
}
b.sql.push(')');
}
}
if let Some(conflict) = &query.on_conflict {
write_on_conflict(b, query.model, &query.columns, &query.generated, conflict)?;
}
write_returning(b, &query.returning)?;
Ok(())
}
pub(super) fn write_update(b: &mut Sql<'_>, query: &UpdateQuery) -> Result<(), SqlError> {
if query.set.is_empty() {
return Err(SqlError::EmptyUpdateSet);
}
b.scope_stack.push(Scope::plain(query.model));
b.dml_target = Some(query.model.table);
let r = (|| {
b.sql.push_str("UPDATE ");
b.write_ident(query.model.table);
b.sql.push_str(" SET ");
let mut first = true;
for assignment in &query.set {
if !first {
b.sql.push_str(", ");
}
first = false;
b.write_ident(assignment.column);
b.sql.push_str(" = ");
let cast = null_cast_for(b.d, query.model, assignment.column);
write_expr(b, &assignment.value, cast)?;
}
write_where(b, &query.where_clause, Some(query.model))?;
Ok(())
})();
b.scope_stack.pop();
r
}
fn write_expr(
b: &mut Sql<'_>,
expr: &crate::core::Expr,
cast: Option<BindCast>,
) -> Result<(), SqlError> {
use crate::core::{BinOp as BO, Expr};
match expr {
Expr::Literal(v) => {
b.push_param_typed(v.clone(), cast);
Ok(())
}
Expr::Column(name) => {
if let Some(alias) = b.qualify_alias(name) {
let qualified = format!("{}.{}", b.d.quote_ident(&alias), b.d.quote_ident(name),);
b.sql.push_str(&qualified);
} else {
b.write_ident(name);
}
Ok(())
}
Expr::BinOp { left, op, right } => {
if matches!(op, BO::BitXor) && b.d.name() == "sqlite" {
return Err(SqlError::OpNotSupportedInDialect {
op: "BitXor",
dialect: b.d.name(),
});
}
if matches!(op, BO::L2Distance | BO::CosineDistance | BO::InnerProduct)
&& b.d.name() != "postgres"
{
return Err(SqlError::OpNotSupportedInDialect {
op: match op {
BO::L2Distance => "<-> (pgvector L2 distance)",
BO::CosineDistance => "<=> (pgvector cosine distance)",
_ => "<#> (pgvector inner product)",
},
dialect: b.d.name(),
});
}
b.sql.push('(');
write_expr(b, left, None)?;
b.sql.push(' ');
b.sql.push_str(match op {
BO::Add => "+",
BO::Sub => "-",
BO::Mul => "*",
BO::Div if b.d.name() == "mysql" && is_int_expr(b, expr) => "DIV",
BO::Div => "/",
BO::Mod => "%",
BO::BitAnd => "&",
BO::BitOr => "|",
BO::BitXor => {
if b.d.name() == "postgres" {
"#"
} else {
"^"
}
}
BO::BitShl => "<<",
BO::BitShr => ">>",
BO::L2Distance => "<->",
BO::CosineDistance => "<=>",
BO::InnerProduct => "<#>",
});
b.sql.push(' ');
write_expr(b, right, None)?;
b.sql.push(')');
Ok(())
}
Expr::Function { kind, args } => write_function(b, *kind, args),
Expr::Cast { expr, ty } => {
let ty_token =
b.d.cast_type(*ty)
.ok_or(SqlError::OpNotSupportedInDialect {
op: "CAST: dialect cannot map this FieldType",
dialect: b.d.name(),
})?;
b.sql.push_str("CAST(");
write_expr(b, expr, None)?;
b.sql.push_str(" AS ");
b.sql.push_str(ty_token);
b.sql.push(')');
Ok(())
}
Expr::Case { branches, default } => write_case(b, branches, default.as_deref()),
Expr::Subquery(inner) => {
b.sql.push('(');
let prev = b.aggregate_allowed;
b.aggregate_allowed = false;
let r = write_select(b, inner);
b.aggregate_allowed = prev;
r?;
b.sql.push(')');
Ok(())
}
Expr::AggregateSubquery(inner) => {
b.sql.push('(');
let prev = b.aggregate_allowed;
b.aggregate_allowed = false;
let r = write_aggregate(b, inner);
b.aggregate_allowed = prev;
r?;
b.sql.push(')');
Ok(())
}
Expr::RelAggregate {
kind,
column,
table,
correlation,
} => {
use crate::core::RelAggKind;
let col_sql = |c: Option<&'static str>, k: &'static str| -> Result<String, SqlError> {
c.map(|c| b.d.quote_ident(c))
.ok_or(SqlError::RelAggregateMissingColumn { kind: k })
};
let agg = match kind {
RelAggKind::Count => "COUNT(*)".to_string(),
RelAggKind::Sum => {
let bare = format!("SUM({})", col_sql(*column, "SUM")?);
let ty = crate::core::ModelEntry::for_table(table)
.and_then(|e| e.schema.field_by_column((*column)?))
.map(|f| f.ty);
match sum_cast(ty) {
Some(kind) => apply_agg_cast(b.d, kind, &bare),
None => bare,
}
}
RelAggKind::Avg => {
b.d.cast_aggregate_to_float(&format!("AVG({})", col_sql(*column, "AVG")?))
}
RelAggKind::Max => format!("MAX({})", col_sql(*column, "MAX")?),
RelAggKind::Min => format!("MIN({})", col_sql(*column, "MIN")?),
};
b.sql.push_str("(SELECT ");
b.sql.push_str(&agg);
b.sql.push_str(" FROM ");
write_rel_correlation(b, table, correlation)?;
b.sql.push(')');
Ok(())
}
Expr::OuterRef(col) => {
let len = b.scope_stack.len();
if len < 2 {
return Err(SqlError::OuterRefOutsideSubquery { column: col });
}
let outer = b.scope_stack[len - 2];
b.read_outer(len - 1);
let qualified = format!("{}.{}", b.d.quote_ident(outer.name), b.d.quote_ident(col),);
b.sql.push_str(&qualified);
Ok(())
}
Expr::AliasedColumn { alias, column } => {
let qualified = match &b.derived_joins {
Some(dj) if dj.depth == b.scope_stack.len() && dj.aliases.contains(alias) => {
format!(
"{}.{}",
b.d.quote_ident(dj.table),
b.d.quote_ident(&crate::core::joins::joined_label(alias, column))
)
}
_ => {
let alias = b.scope_stack.last().map_or(*alias, |s| s.rename(alias));
format!(
"{}.{}",
b.d.quote_ident(&b.alias_ref(alias)),
b.d.quote_ident(column)
)
}
};
b.sql.push_str(&qualified);
Ok(())
}
Expr::Window(w) => write_window_expr(b, w),
Expr::Aggregate(agg) => {
if !b.aggregate_allowed {
return Err(SqlError::AggregateOutsideAggregateContext);
}
let model = b
.scope_model()
.expect("Expr::Aggregate emitted outside any scope frame");
write_aggregate_expr(b, agg, model)
}
Expr::JsonPath {
source,
path,
as_text,
} => write_json_path(b, source, path, *as_text),
}
}
fn expr_type(b: &Sql<'_>, expr: &crate::core::Expr) -> Option<crate::core::FieldType> {
use crate::core::{BinOp as BO, Expr, FieldType};
match expr {
Expr::Literal(v) => v.field_type(),
Expr::Column(c) if !matches!(b.qualifier(), Some(Qualifier::Join(_))) => {
b.scope_model()?.field_by_column(c).map(|f| f.ty)
}
Expr::AliasedColumn { alias, column } => {
alias_model(b, alias)?.field_by_column(column).map(|f| f.ty)
}
Expr::BinOp {
op: BO::Add | BO::Sub | BO::Mul | BO::Div | BO::Mod,
..
} if is_int_expr(b, expr) => Some(FieldType::I64),
_ => None,
}
}
fn alias_model(b: &Sql<'_>, alias: &str) -> Option<&'static ModelSchema> {
(1..=b.scope_stack.len()).rev().find_map(|depth| {
b.join_types
.iter()
.rev()
.find(|(d, a, _)| *d == depth && *a == alias)
.map(|(_, _, m)| *m)
.or_else(|| {
let s = b.scope_stack[depth - 1];
(s.name == alias || s.model.table == alias).then_some(s.model)
})
})
}
fn is_int_expr(b: &Sql<'_>, expr: &crate::core::Expr) -> bool {
use crate::core::{Expr, FieldType as T};
let int = |e: &Expr| matches!(expr_type(b, e), Some(T::I16 | T::I32 | T::I64));
matches!(expr, Expr::BinOp { left, right, .. } if int(left) && int(right))
}
fn write_pg_utc_arg(b: &mut Sql<'_>, arg: &crate::core::Expr) -> Result<(), SqlError> {
if expr_type(b, arg) != Some(crate::core::FieldType::DateTime) {
return write_expr(b, arg, None);
}
b.sql.push('(');
write_expr(b, arg, None)?;
b.sql.push_str(" AT TIME ZONE 'UTC')");
Ok(())
}
fn write_json_path(
b: &mut Sql<'_>,
source: &crate::core::Expr,
path: &[crate::core::JsonPathStep],
as_text: bool,
) -> Result<(), SqlError> {
use crate::core::{JsonPathStep, SqlValue};
let dialect = b.d.name();
if dialect == "postgres" {
check_json_path(path, dialect)?;
write_expr(b, source, None)?;
for (i, step) in path.iter().enumerate() {
let is_last = i + 1 == path.len();
let op = if is_last && as_text { " ->> " } else { " -> " };
b.sql.push_str(op);
match step {
JsonPathStep::Key(k) => {
b.params.push(SqlValue::String(k.clone()));
let p = b.d.placeholder(b.params.len());
b.sql.push_str(&p);
}
JsonPathStep::Index(n) => {
b.sql.push_str(&n.to_string());
}
}
}
return Ok(());
}
let json_path = json_path_text(path, dialect)?;
if dialect == "mysql" {
if as_text {
b.sql.push_str("JSON_UNQUOTE(NULLIF(JSON_EXTRACT(");
write_expr(b, source, None)?;
b.sql.push_str(", '");
b.sql.push_str(&json_path);
b.sql.push_str("'), ");
b.d.write_json_param(&mut b.sql, "'null'");
b.sql.push_str("))");
} else {
b.sql.push_str("JSON_EXTRACT(");
write_expr(b, source, None)?;
b.sql.push_str(", '");
b.sql.push_str(&json_path);
b.sql.push_str("')");
}
return Ok(());
}
if as_text {
b.sql.push_str("CASE json_type(");
write_expr(b, source, None)?;
b.sql.push_str(", '");
b.sql.push_str(&json_path);
b.sql.push_str(
"') WHEN 'true' THEN 'true' WHEN 'false' THEN 'false' ELSE CAST(json_extract(",
);
write_expr(b, source, None)?;
b.sql.push_str(", '");
b.sql.push_str(&json_path);
b.sql.push_str("') AS TEXT) END");
return Ok(());
}
b.sql.push_str("json_extract(");
write_expr(b, source, None)?;
b.sql.push_str(", '");
b.sql.push_str(&json_path);
b.sql.push_str("')");
Ok(())
}
fn check_json_path(
path: &[crate::core::JsonPathStep],
dialect: &'static str,
) -> Result<(), SqlError> {
use crate::core::JsonPathStep;
if path.is_empty() {
return Err(SqlError::OpNotSupportedInDialect {
op: "JsonPath requires at least one path step",
dialect,
});
}
let key_safe =
|s: &str| !s.is_empty() && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
for step in path {
if let JsonPathStep::Key(k) = step {
if !key_safe(k) {
return Err(SqlError::OpNotSupportedInDialect {
op: "JsonPath key contains characters outside the safe set [A-Za-z0-9_]",
dialect,
});
}
}
}
Ok(())
}
fn json_path_text(
path: &[crate::core::JsonPathStep],
dialect: &'static str,
) -> Result<String, SqlError> {
use crate::core::JsonPathStep;
check_json_path(path, dialect)?;
let mut json_path = String::from("$");
for step in path {
match step {
JsonPathStep::Key(k) if dialect == "mysql" => {
json_path.push_str(".\"");
json_path.push_str(k);
json_path.push('"');
}
JsonPathStep::Key(k) => {
json_path.push('.');
json_path.push_str(k);
}
JsonPathStep::Index(n) => {
if *n < 0 {
if dialect == "sqlite" {
json_path.push_str("[#");
json_path.push_str(&n.to_string());
json_path.push(']');
} else {
return Err(SqlError::OpNotSupportedInDialect {
op: "JsonPath negative indices are unsupported on MySQL (the $[N] path grammar has no negative form; PG + SQLite support them)",
dialect,
});
}
} else {
json_path.push('[');
json_path.push_str(&n.to_string());
json_path.push(']');
}
}
}
}
Ok(json_path)
}
fn write_sqlite_json_value_eq(
b: &mut Sql<'_>,
lhs: &crate::core::Expr,
op: Op,
rhs: &crate::core::Expr,
) -> Result<bool, SqlError> {
use crate::core::Expr;
let (
Expr::JsonPath {
source,
path,
as_text: false,
},
Expr::Literal(SqlValue::Json(v)),
Op::Eq | Op::Ne,
"sqlite",
) = (lhs, rhs, op, b.d.name())
else {
return Ok(false);
};
if !(v.is_null() || v.is_object() || v.is_array()) {
return Ok(false);
}
let p = json_path_text(path, "sqlite")?;
let ne = matches!(op, Op::Ne);
if !v.is_null() {
b.sql.push('(');
}
b.sql.push_str("json_type(");
write_expr(b, source, None)?;
if v.is_null() {
b.sql.push_str(&format!(", '{p}') "));
b.sql.push_str(if ne { "<> 'null'" } else { "= 'null'" });
return Ok(true);
}
let ty = "CASE WHEN type IN ('integer', 'real') THEN 'number' ELSE type END";
let col = format!(
"SELECT substr(fullkey, min(length(fullkey)) OVER () + 1), {ty}, atom FROM json_tree("
);
let lit = format!("SELECT substr(fullkey, 2), {ty}, atom FROM json_tree(");
b.sql.push_str(&format!(", '{p}') IS NOT NULL AND "));
b.sql.push_str(if ne { "NOT (" } else { "(" });
b.sql.push_str("NOT EXISTS (");
b.sql.push_str(&col);
write_expr(b, source, None)?;
b.sql.push_str(&format!(", '{p}') EXCEPT {lit}"));
b.push_param(SqlValue::Json(v.clone()));
b.sql.push_str(&format!(")) AND NOT EXISTS ({lit}"));
b.push_param(SqlValue::Json(v.clone()));
b.sql.push_str(&format!(") EXCEPT {col}"));
write_expr(b, source, None)?;
b.sql.push_str(&format!(", '{p}'))))"));
Ok(true)
}
fn write_window_expr(b: &mut Sql<'_>, w: &crate::core::WindowExpr) -> Result<(), SqlError> {
use crate::core::{Expr, WindowFn};
let fn_name = match w.kind {
WindowFn::RowNumber => "ROW_NUMBER",
WindowFn::Rank => "RANK",
WindowFn::DenseRank => "DENSE_RANK",
WindowFn::Ntile => "NTILE",
WindowFn::Lag => "LAG",
WindowFn::Lead => "LEAD",
WindowFn::FirstValue => "FIRST_VALUE",
WindowFn::LastValue => "LAST_VALUE",
WindowFn::Sum => "SUM",
WindowFn::Avg => "AVG",
WindowFn::Min => "MIN",
WindowFn::Max => "MAX",
WindowFn::Count => "COUNT",
};
b.sql.push_str(fn_name);
b.sql.push('(');
if matches!(w.kind, WindowFn::Count) && w.args.is_empty() {
b.sql.push('*');
}
let integer_arg_index: Option<usize> = match w.kind {
WindowFn::Lag | WindowFn::Lead => Some(1),
WindowFn::Ntile => Some(0),
_ => None,
};
for (i, arg) in w.args.iter().enumerate() {
if i > 0 {
b.sql.push_str(", ");
}
if integer_arg_index == Some(i) {
if let Expr::Literal(SqlValue::I64(n)) = arg {
use std::fmt::Write as _;
let _ = write!(b.sql, "{n}");
continue;
}
}
write_expr(b, arg, None)?;
}
b.sql.push_str(") OVER (");
let mut first_clause = true;
if !w.partition_by.is_empty() {
b.sql.push_str("PARTITION BY ");
for (i, col) in w.partition_by.iter().enumerate() {
if i > 0 {
b.sql.push_str(", ");
}
b.write_scope_col(col);
}
first_clause = false;
}
if !w.order_by.is_empty() {
if !first_clause {
b.sql.push(' ');
}
b.sql.push_str("ORDER BY ");
for (i, o) in w.order_by.iter().enumerate() {
if i > 0 {
b.sql.push_str(", ");
}
b.write_scope_col(o.column);
if o.desc {
b.sql.push_str(" DESC");
}
}
first_clause = false;
}
if let Some(frame) = &w.frame {
if !first_clause {
b.sql.push(' ');
}
write_window_frame(b, frame);
}
b.sql.push(')');
Ok(())
}
fn write_window_frame(b: &mut Sql<'_>, frame: &crate::core::WindowFrame) {
use crate::core::{FrameBoundary, FrameKind};
b.sql.push_str(match frame.kind {
FrameKind::Rows => "ROWS",
FrameKind::Range => "RANGE",
});
b.sql.push(' ');
if frame.end.is_some() {
b.sql.push_str("BETWEEN ");
}
write_frame_boundary(b, frame.start);
if let Some(end) = frame.end {
b.sql.push_str(" AND ");
write_frame_boundary(b, end);
}
fn write_frame_boundary(b: &mut Sql<'_>, bound: FrameBoundary) {
match bound {
FrameBoundary::UnboundedPreceding => b.sql.push_str("UNBOUNDED PRECEDING"),
FrameBoundary::Preceding(n) => {
use std::fmt::Write as _;
let _ = write!(b.sql, "{n} PRECEDING");
}
FrameBoundary::CurrentRow => b.sql.push_str("CURRENT ROW"),
FrameBoundary::Following(n) => {
use std::fmt::Write as _;
let _ = write!(b.sql, "{n} FOLLOWING");
}
FrameBoundary::UnboundedFollowing => b.sql.push_str("UNBOUNDED FOLLOWING"),
}
}
}
fn write_case(
b: &mut Sql<'_>,
branches: &[crate::core::CaseBranch],
default: Option<&crate::core::Expr>,
) -> Result<(), SqlError> {
if branches.is_empty() {
return Err(SqlError::EmptyCaseBranches);
}
b.sql.push_str("CASE");
for branch in branches {
if branch.condition.is_empty() {
return Err(SqlError::EmptyCaseWhenCondition);
}
b.sql.push_str(" WHEN ");
write_where_expr(b, &branch.condition, None)?;
b.sql.push_str(" THEN ");
write_expr(b, &branch.then, None)?;
}
if let Some(d) = default {
b.sql.push_str(" ELSE ");
write_expr(b, d, None)?;
}
b.sql.push_str(" END");
Ok(())
}
#[allow(clippy::too_many_lines)] fn write_function(
b: &mut Sql<'_>,
kind: crate::core::ScalarFn,
args: &[crate::core::Expr],
) -> Result<(), SqlError> {
use crate::core::ScalarFn as F;
match kind {
F::Lower => write_call_unary(b, "LOWER", args),
F::Upper => write_call_unary(b, "UPPER", args),
F::Length => write_call_unary(b, "LENGTH", args),
F::Trim => write_call_unary(b, "TRIM", args),
F::LTrim => write_call_unary(b, "LTRIM", args),
F::RTrim => write_call_unary(b, "RTRIM", args),
F::Replace => {
if args.len() != 3 {
return Err(SqlError::FunctionArityMismatch {
func: "REPLACE",
expected: "3",
got: args.len(),
});
}
write_call(b, "REPLACE", args)
}
F::Concat => {
if args.is_empty() {
return Err(SqlError::FunctionArityMismatch {
func: "CONCAT",
expected: ">= 1",
got: 0,
});
}
if b.d.name() == "mysql" {
return write_call(b, "CONCAT", args);
}
let pg = b.d.name() == "postgres";
b.sql.push('(');
for (i, a) in args.iter().enumerate() {
if i > 0 {
b.sql.push_str(" || ");
}
if pg {
b.sql.push_str("CAST(");
}
write_expr(b, a, None)?;
if pg {
b.sql.push_str(" AS TEXT)");
}
}
b.sql.push(')');
Ok(())
}
F::Substr => {
if args.len() != 3 {
return Err(SqlError::FunctionArityMismatch {
func: "SUBSTRING",
expected: "3",
got: args.len(),
});
}
if b.d.name() == "postgres" {
b.sql.push_str("SUBSTRING(");
write_expr(b, &args[0], None)?;
b.sql.push_str(" FROM ");
write_expr(b, &args[1], None)?;
b.sql.push_str(" FOR ");
write_expr(b, &args[2], None)?;
b.sql.push(')');
Ok(())
} else {
let name = if b.d.name() == "mysql" {
"SUBSTRING"
} else {
"SUBSTR"
};
write_call(b, name, args)
}
}
F::Abs => write_call_unary(b, "ABS", args),
F::Floor => write_call_unary(b, "FLOOR", args),
F::Ceil => write_call_unary(b, "CEIL", args),
F::Round => {
if args.is_empty() || args.len() > 2 {
return Err(SqlError::FunctionArityMismatch {
func: "ROUND",
expected: "1 or 2",
got: args.len(),
});
}
let float = match expr_type(b, &args[0]) {
Some(crate::core::FieldType::F64) => Some("DOUBLE PRECISION"),
Some(crate::core::FieldType::F32) => Some("REAL"),
_ => None,
};
match float {
Some(ty) if args.len() == 2 && b.d.name() == "postgres" => {
b.sql.push_str("CAST(ROUND(CAST(");
write_expr(b, &args[0], None)?;
b.sql.push_str(" AS NUMERIC), CAST(");
write_expr(b, &args[1], None)?;
b.sql.push_str(" AS INTEGER)) AS ");
b.sql.push_str(ty);
b.sql.push(')');
Ok(())
}
_ => write_call(b, "ROUND", args),
}
}
F::Coalesce => {
if args.is_empty() {
return Err(SqlError::FunctionArityMismatch {
func: "COALESCE",
expected: ">= 1",
got: 0,
});
}
write_call(b, "COALESCE", args)
}
F::Greatest => {
if args.is_empty() {
return Err(SqlError::FunctionArityMismatch {
func: "GREATEST",
expected: ">= 1",
got: 0,
});
}
if b.d.name() == "sqlite" && args.len() == 1 {
return Err(SqlError::OpNotSupportedInDialect {
op: "GREATEST with 1 argument (SQLite collides with the aggregate MAX)",
dialect: "sqlite",
});
}
let name = if b.d.name() == "sqlite" {
"MAX"
} else {
"GREATEST"
};
write_call(b, name, args)
}
F::Least => {
if args.is_empty() {
return Err(SqlError::FunctionArityMismatch {
func: "LEAST",
expected: ">= 1",
got: 0,
});
}
if b.d.name() == "sqlite" && args.len() == 1 {
return Err(SqlError::OpNotSupportedInDialect {
op: "LEAST with 1 argument (SQLite collides with the aggregate MIN)",
dialect: "sqlite",
});
}
let name = if b.d.name() == "sqlite" {
"MIN"
} else {
"LEAST"
};
write_call(b, name, args)
}
F::NullIf => {
if args.len() != 2 {
return Err(SqlError::FunctionArityMismatch {
func: "NULLIF",
expected: "2",
got: args.len(),
});
}
write_call(b, "NULLIF", args)
}
F::Now => {
if !args.is_empty() {
return Err(SqlError::FunctionArityMismatch {
func: "NOW",
expected: "0",
got: args.len(),
});
}
if b.d.name() == "sqlite" {
let _ = write!(
b.sql,
"strftime('{}','now')",
crate::sql::SQLITE_DATETIME_FORMAT
);
} else if b.d.name() == "mysql" {
b.sql.push_str("NOW(6)");
} else {
b.sql.push_str("NOW()");
}
Ok(())
}
F::ExtractYear
| F::ExtractMonth
| F::ExtractDay
| F::ExtractHour
| F::ExtractMinute
| F::ExtractSecond
| F::ExtractWeek => write_extract_int(b, kind, args),
F::ExtractWeekDay => write_extract_weekday(b, args),
F::ExtractQuarter => {
if args.len() != 1 {
return Err(SqlError::FunctionArityMismatch {
func: "EXTRACT(QUARTER)",
expected: "1",
got: args.len(),
});
}
if b.d.name() == "sqlite" {
write_extract_quarter_sqlite(b, &args[0])
} else {
write_extract_int(b, kind, args)
}
}
F::TruncDate => {
if args.len() != 1 {
return Err(SqlError::FunctionArityMismatch {
func: "DATE",
expected: "1",
got: args.len(),
});
}
b.sql.push_str("DATE(");
if b.d.name() == "postgres" {
write_pg_utc_arg(b, &args[0])?;
} else {
write_expr(b, &args[0], None)?;
}
b.sql.push(')');
Ok(())
}
F::TruncYear | F::TruncMonth | F::TruncDay => write_trunc(b, kind, args),
F::JsonArrayLength => {
if args.len() != 1 {
return Err(SqlError::FunctionArityMismatch {
func: "JSON_ARRAY_LENGTH",
expected: "1",
got: args.len(),
});
}
let fname = match b.d.name() {
"postgres" => "jsonb_array_length",
"mysql" => "JSON_LENGTH",
_ => "json_array_length",
};
b.sql.push_str(fname);
b.sql.push('(');
write_expr(b, &args[0], None)?;
b.sql.push(')');
Ok(())
}
F::TrigramSimilarity | F::TrigramWordSimilarity => {
let fname = if matches!(kind, F::TrigramWordSimilarity) {
"WORD_SIMILARITY"
} else {
"SIMILARITY"
};
if args.len() != 2 {
return Err(SqlError::FunctionArityMismatch {
func: if matches!(kind, F::TrigramWordSimilarity) {
"WORD_SIMILARITY"
} else {
"SIMILARITY"
},
expected: "2",
got: args.len(),
});
}
if b.d.name() != "postgres" {
return Err(SqlError::OpNotSupportedInDialect {
op: if matches!(kind, F::TrigramWordSimilarity) {
"WORD_SIMILARITY (pg_trgm) is Postgres-only"
} else {
"SIMILARITY (pg_trgm) is Postgres-only"
},
dialect: b.d.name(),
});
}
b.sql.push_str(fname);
b.sql.push('(');
write_expr(b, &args[0], None)?;
b.sql.push_str(", ");
write_expr(b, &args[1], None)?;
b.sql.push(')');
Ok(())
}
F::ToTsVector | F::PlainToTsQuery => {
let fname = if matches!(kind, F::ToTsVector) {
"to_tsvector"
} else {
"plainto_tsquery"
};
if args.len() != 1 {
return Err(SqlError::FunctionArityMismatch {
func: if matches!(kind, F::ToTsVector) {
"to_tsvector"
} else {
"plainto_tsquery"
},
expected: "1",
got: args.len(),
});
}
if b.d.name() != "postgres" {
return Err(SqlError::OpNotSupportedInDialect {
op: if matches!(kind, F::ToTsVector) {
"to_tsvector (FTS) is Postgres-only"
} else {
"plainto_tsquery (FTS) is Postgres-only"
},
dialect: b.d.name(),
});
}
b.sql.push_str(fname);
b.sql.push('(');
write_expr(b, &args[0], None)?;
b.sql.push(')');
Ok(())
}
F::TsRank => {
if args.len() != 2 {
return Err(SqlError::FunctionArityMismatch {
func: "ts_rank",
expected: "2",
got: args.len(),
});
}
if b.d.name() != "postgres" {
return Err(SqlError::OpNotSupportedInDialect {
op: "ts_rank (FTS) is Postgres-only",
dialect: b.d.name(),
});
}
b.sql.push_str("ts_rank(");
write_expr(b, &args[0], None)?;
b.sql.push_str(", ");
write_expr(b, &args[1], None)?;
b.sql.push(')');
Ok(())
}
F::TsHeadline => {
if args.len() != 2 && args.len() != 3 {
return Err(SqlError::FunctionArityMismatch {
func: "ts_headline",
expected: "2 or 3",
got: args.len(),
});
}
if b.d.name() != "postgres" {
return Err(SqlError::OpNotSupportedInDialect {
op: "ts_headline (FTS) is Postgres-only",
dialect: b.d.name(),
});
}
b.sql.push_str("ts_headline(");
for (i, a) in args.iter().enumerate() {
if i > 0 {
b.sql.push_str(", ");
}
write_expr(b, a, None)?;
}
b.sql.push(')');
Ok(())
}
F::PhraseToTsQuery | F::WebsearchToTsQuery | F::ToTsQuery => {
let fname = match kind {
F::PhraseToTsQuery => "phraseto_tsquery",
F::WebsearchToTsQuery => "websearch_to_tsquery",
F::ToTsQuery => "to_tsquery",
_ => unreachable!(),
};
if args.len() != 1 {
return Err(SqlError::FunctionArityMismatch {
func: fname,
expected: "1",
got: args.len(),
});
}
if b.d.name() != "postgres" {
return Err(SqlError::OpNotSupportedInDialect {
op: match kind {
F::PhraseToTsQuery => "phraseto_tsquery (FTS) is Postgres-only",
F::WebsearchToTsQuery => "websearch_to_tsquery (FTS) is Postgres-only",
F::ToTsQuery => "to_tsquery (FTS) is Postgres-only",
_ => unreachable!(),
},
dialect: b.d.name(),
});
}
b.sql.push_str(fname);
b.sql.push('(');
write_expr(b, &args[0], None)?;
b.sql.push(')');
Ok(())
}
F::TsRankCd => {
if args.len() != 2 {
return Err(SqlError::FunctionArityMismatch {
func: "ts_rank_cd",
expected: "2",
got: args.len(),
});
}
if b.d.name() != "postgres" {
return Err(SqlError::OpNotSupportedInDialect {
op: "ts_rank_cd (FTS) is Postgres-only",
dialect: b.d.name(),
});
}
b.sql.push_str("ts_rank_cd(");
write_expr(b, &args[0], None)?;
b.sql.push_str(", ");
write_expr(b, &args[1], None)?;
b.sql.push(')');
Ok(())
}
F::LPad | F::RPad => write_pad(b, kind, args),
F::Md5 | F::Sha1 | F::Sha256 => write_hash(b, kind, args),
F::Position => write_position(b, args),
F::Repeat => write_repeat(b, args),
F::Reverse => {
if args.len() != 1 {
return Err(SqlError::FunctionArityMismatch {
func: "REVERSE",
expected: "1",
got: args.len(),
});
}
if b.d.name() == "sqlite" {
return Err(SqlError::OpNotSupportedInDialect {
op: "REVERSE (SQLite has no built-in; reverse the string in Rust before binding)",
dialect: b.d.name(),
});
}
write_call(b, "REVERSE", args)
}
F::Sign => write_sign(b, args),
F::Power => {
if args.len() != 2 {
return Err(SqlError::FunctionArityMismatch {
func: "POWER",
expected: "2",
got: args.len(),
});
}
if b.d.name() == "sqlite" {
return Err(SqlError::OpNotSupportedInDialect {
op: "POWER (SQLite needs SQLITE_ENABLE_MATH_FUNCTIONS at build time; not enabled in sqlx-sqlite default build)",
dialect: b.d.name(),
});
}
write_call(b, "POWER", args)
}
F::Sqrt => {
if b.d.name() == "sqlite" {
return Err(SqlError::OpNotSupportedInDialect {
op: "SQRT (SQLite needs SQLITE_ENABLE_MATH_FUNCTIONS at build time; not enabled in sqlx-sqlite default build)",
dialect: b.d.name(),
});
}
write_call_unary(b, "SQRT", args)
}
F::Log => write_log(b, args),
F::LogWithBase => write_log_with_base(b, args),
F::Exp => write_exp(b, args),
F::Pi => write_pi(b, args),
F::Random => write_random(b, args),
F::MakeInterval => write_make_interval(b, args),
F::Age => write_age(b, args),
F::TruncWithTz => write_trunc_with_tz(b, args),
F::SetWeight => write_setweight(b, args),
F::TsConcat => write_ts_concat(b, args),
F::StDistance | F::StDWithin | F::StContains | F::StWithin | F::StIntersects => {
write_spatial_fn(b, kind, args)
}
}
}
fn write_spatial_fn(
b: &mut Sql<'_>,
kind: crate::core::ScalarFn,
args: &[crate::core::Expr],
) -> Result<(), SqlError> {
use crate::core::ScalarFn as F;
let (name, arity): (&'static str, usize) = match kind {
F::StDistance => ("ST_Distance", 2),
F::StDWithin => ("ST_DWithin", 3),
F::StContains => ("ST_Contains", 2),
F::StWithin => ("ST_Within", 2),
F::StIntersects => ("ST_Intersects", 2),
_ => unreachable!("write_spatial_fn only handles ST_* variants"),
};
if args.len() != arity {
return Err(SqlError::FunctionArityMismatch {
func: name,
expected: if arity == 3 { "3" } else { "2" },
got: args.len(),
});
}
if b.d.name() != "postgres" {
return Err(SqlError::OpNotSupportedInDialect {
op: match kind {
F::StDistance => "ST_Distance (PostGIS) is Postgres-only",
F::StDWithin => "ST_DWithin (PostGIS) is Postgres-only",
F::StContains => "ST_Contains (PostGIS) is Postgres-only",
F::StWithin => "ST_Within (PostGIS) is Postgres-only",
_ => "ST_Intersects (PostGIS) is Postgres-only",
},
dialect: b.d.name(),
});
}
write_call(b, name, args)
}
fn write_pad(
b: &mut Sql<'_>,
kind: crate::core::ScalarFn,
args: &[crate::core::Expr],
) -> Result<(), SqlError> {
use crate::core::ScalarFn as F;
if args.len() != 3 {
return Err(SqlError::FunctionArityMismatch {
func: match kind {
F::LPad => "LPAD",
_ => "RPAD",
},
expected: "3",
got: args.len(),
});
}
let name = match kind {
F::LPad => "LPAD",
_ => "RPAD",
};
if b.d.name() != "sqlite" {
return write_call(b, name, args);
}
let copies = |b: &mut Sql<'_>| -> Result<(), SqlError> {
b.sql.push_str("replace(printf('%.*c', ");
write_expr(b, &args[1], None)?;
b.sql.push_str(", ' '), ' ', ");
write_expr(b, &args[2], None)?;
b.sql.push(')');
Ok(())
};
if matches!(kind, F::LPad) {
b.sql.push_str("CASE WHEN length(");
write_expr(b, &args[0], None)?;
b.sql.push_str(") >= ");
write_expr(b, &args[1], None)?;
b.sql.push_str(" THEN substr(");
write_expr(b, &args[0], None)?;
b.sql.push_str(", 1, ");
write_expr(b, &args[1], None)?;
b.sql.push_str(") ELSE substr(");
copies(b)?;
b.sql.push_str(", 1, ");
write_expr(b, &args[1], None)?;
b.sql.push_str(" - length(");
write_expr(b, &args[0], None)?;
b.sql.push_str(")) || ");
write_expr(b, &args[0], None)?;
b.sql.push_str(" END");
} else {
b.sql.push_str("substr(");
write_expr(b, &args[0], None)?;
b.sql.push_str(" || ");
copies(b)?;
b.sql.push_str(", 1, ");
write_expr(b, &args[1], None)?;
b.sql.push(')');
}
Ok(())
}
fn write_hash(
b: &mut Sql<'_>,
kind: crate::core::ScalarFn,
args: &[crate::core::Expr],
) -> Result<(), SqlError> {
use crate::core::ScalarFn as F;
if args.len() != 1 {
return Err(SqlError::FunctionArityMismatch {
func: match kind {
F::Md5 => "MD5",
F::Sha1 => "SHA1",
_ => "SHA256",
},
expected: "1",
got: args.len(),
});
}
if b.d.name() == "sqlite" {
return Err(SqlError::OpNotSupportedInDialect {
op: match kind {
F::Md5 => "MD5 (SQLite has no built-in hash; hash before binding)",
F::Sha1 => "SHA1 (SQLite has no built-in hash; hash before binding)",
_ => "SHA256 (SQLite has no built-in hash; hash before binding)",
},
dialect: b.d.name(),
});
}
if b.d.name() == "postgres" {
match kind {
F::Md5 => {
b.sql.push_str("md5(");
write_expr(b, &args[0], None)?;
b.sql.push(')');
}
F::Sha1 => {
b.sql.push_str("encode(digest(");
write_expr(b, &args[0], None)?;
b.sql.push_str(", 'sha1'), 'hex')");
}
_ => {
b.sql.push_str("encode(digest(");
write_expr(b, &args[0], None)?;
b.sql.push_str(", 'sha256'), 'hex')");
}
}
Ok(())
} else {
match kind {
F::Md5 => write_call(b, "MD5", args),
F::Sha1 => write_call(b, "SHA1", args),
_ => {
b.sql.push_str("SHA2(");
write_expr(b, &args[0], None)?;
b.sql.push_str(", 256)");
Ok(())
}
}
}
}
fn write_position(b: &mut Sql<'_>, args: &[crate::core::Expr]) -> Result<(), SqlError> {
if args.len() != 2 {
return Err(SqlError::FunctionArityMismatch {
func: "POSITION",
expected: "2",
got: args.len(),
});
}
let needle = &args[0];
let hay = &args[1];
match b.d.name() {
"postgres" => {
b.sql.push_str("POSITION(");
write_expr(b, needle, None)?;
b.sql.push_str(" IN ");
write_expr(b, hay, None)?;
b.sql.push(')');
}
"mysql" => {
b.sql.push_str("LOCATE(");
write_expr(b, needle, None)?;
b.sql.push_str(", ");
write_expr(b, hay, None)?;
b.sql.push(')');
}
_ => {
b.sql.push_str("INSTR(");
write_expr(b, hay, None)?;
b.sql.push_str(", ");
write_expr(b, needle, None)?;
b.sql.push(')');
}
}
Ok(())
}
fn write_repeat(b: &mut Sql<'_>, args: &[crate::core::Expr]) -> Result<(), SqlError> {
if args.len() != 2 {
return Err(SqlError::FunctionArityMismatch {
func: "REPEAT",
expected: "2",
got: args.len(),
});
}
if b.d.name() == "sqlite" {
b.sql.push_str("replace(printf('%.*c', ");
write_expr(b, &args[1], None)?;
b.sql.push_str(", '_'), '_', ");
write_expr(b, &args[0], None)?;
b.sql.push(')');
return Ok(());
}
write_call(b, "REPEAT", args)
}
fn write_sign(b: &mut Sql<'_>, args: &[crate::core::Expr]) -> Result<(), SqlError> {
if args.len() != 1 {
return Err(SqlError::FunctionArityMismatch {
func: "SIGN",
expected: "1",
got: args.len(),
});
}
if b.d.name() != "sqlite" {
return write_call(b, "SIGN", args);
}
b.sql.push_str("(CASE WHEN ");
write_expr(b, &args[0], None)?;
b.sql.push_str(" > 0 THEN 1 WHEN ");
write_expr(b, &args[0], None)?;
b.sql.push_str(" < 0 THEN -1 ELSE 0 END)");
Ok(())
}
fn write_log(b: &mut Sql<'_>, args: &[crate::core::Expr]) -> Result<(), SqlError> {
if args.len() != 1 {
return Err(SqlError::FunctionArityMismatch {
func: "LN",
expected: "1",
got: args.len(),
});
}
if b.d.name() == "sqlite" {
return Err(SqlError::OpNotSupportedInDialect {
op: "LN (SQLite needs SQLITE_ENABLE_MATH_FUNCTIONS at build time; not enabled in sqlx-sqlite default build)",
dialect: b.d.name(),
});
}
write_call_unary(b, "LN", args)
}
fn write_log_with_base(b: &mut Sql<'_>, args: &[crate::core::Expr]) -> Result<(), SqlError> {
if args.len() != 2 {
return Err(SqlError::FunctionArityMismatch {
func: "LOG",
expected: "2",
got: args.len(),
});
}
if b.d.name() == "sqlite" {
return Err(SqlError::OpNotSupportedInDialect {
op: "LOG(base, x) (SQLite needs SQLITE_ENABLE_MATH_FUNCTIONS at build time)",
dialect: b.d.name(),
});
}
write_call(b, "LOG", args)
}
fn write_exp(b: &mut Sql<'_>, args: &[crate::core::Expr]) -> Result<(), SqlError> {
if args.len() != 1 {
return Err(SqlError::FunctionArityMismatch {
func: "EXP",
expected: "1",
got: args.len(),
});
}
if b.d.name() == "sqlite" {
return Err(SqlError::OpNotSupportedInDialect {
op: "EXP (SQLite needs SQLITE_ENABLE_MATH_FUNCTIONS at build time; not enabled in sqlx-sqlite default build)",
dialect: b.d.name(),
});
}
write_call_unary(b, "EXP", args)
}
fn write_pi(b: &mut Sql<'_>, args: &[crate::core::Expr]) -> Result<(), SqlError> {
if !args.is_empty() {
return Err(SqlError::FunctionArityMismatch {
func: "PI",
expected: "0",
got: args.len(),
});
}
if b.d.name() == "sqlite" {
b.sql.push_str("3.141592653589793");
} else {
b.sql.push_str("PI()");
}
Ok(())
}
fn write_random(b: &mut Sql<'_>, args: &[crate::core::Expr]) -> Result<(), SqlError> {
if !args.is_empty() {
return Err(SqlError::FunctionArityMismatch {
func: "RANDOM",
expected: "0",
got: args.len(),
});
}
match b.d.name() {
"postgres" => b.sql.push_str("random()"),
"mysql" => b.sql.push_str("RAND()"),
_ => b.sql.push_str("random()"),
}
Ok(())
}
fn write_make_interval(b: &mut Sql<'_>, args: &[crate::core::Expr]) -> Result<(), SqlError> {
if args.len() != 6 {
return Err(SqlError::FunctionArityMismatch {
func: "MAKE_INTERVAL",
expected: "6",
got: args.len(),
});
}
if b.d.name() != "postgres" {
return Err(SqlError::OpNotSupportedInDialect {
op: "MAKE_INTERVAL (PG-only; MySQL/SQLite have no native interval type — compute the duration app-side)",
dialect: b.d.name(),
});
}
let kws = ["years", "months", "days", "hours", "mins", "secs"];
b.sql.push_str("make_interval(");
for (i, kw) in kws.iter().enumerate() {
if i > 0 {
b.sql.push_str(", ");
}
b.sql.push_str(kw);
b.sql.push_str(" => ");
write_expr(b, &args[i], None)?;
}
b.sql.push(')');
Ok(())
}
fn write_age(b: &mut Sql<'_>, args: &[crate::core::Expr]) -> Result<(), SqlError> {
if args.len() != 2 {
return Err(SqlError::FunctionArityMismatch {
func: "AGE",
expected: "2",
got: args.len(),
});
}
match b.d.name() {
"postgres" => {
b.sql.push_str("age(");
write_expr(b, &args[0], None)?;
b.sql.push_str(", ");
write_expr(b, &args[1], None)?;
b.sql.push(')');
}
"mysql" => {
b.sql.push_str("TIMESTAMPDIFF(SECOND, ");
write_expr(b, &args[1], None)?;
b.sql.push_str(", ");
write_expr(b, &args[0], None)?;
b.sql.push(')');
}
_ => {
b.sql.push_str("((julianday(");
write_expr(b, &args[0], None)?;
b.sql.push_str(") - julianday(");
write_expr(b, &args[1], None)?;
b.sql.push_str(")) * 86400.0)");
}
}
Ok(())
}
fn write_trunc_with_tz(b: &mut Sql<'_>, args: &[crate::core::Expr]) -> Result<(), SqlError> {
use crate::core::SqlValue;
if args.len() != 3 {
return Err(SqlError::FunctionArityMismatch {
func: "TRUNC_WITH_TZ",
expected: "3",
got: args.len(),
});
}
let (unit, tz) = match (&args[1], &args[2]) {
(
crate::core::Expr::Literal(SqlValue::String(u)),
crate::core::Expr::Literal(SqlValue::String(z)),
) => (u.as_str(), z.as_str()),
_ => {
return Err(SqlError::OpNotSupportedInDialect {
op: "TRUNC_WITH_TZ requires string literals for `unit` and `tz` (build via funcs::trunc_with_tz)",
dialect: b.d.name(),
});
}
};
let unit_lc = unit.to_ascii_lowercase();
if !matches!(
unit_lc.as_str(),
"year" | "month" | "day" | "hour" | "minute" | "second"
) {
return Err(SqlError::OpNotSupportedInDialect {
op: "TRUNC_WITH_TZ unit (allowed: year, month, day, hour, minute, second)",
dialect: b.d.name(),
});
}
let safe_charset = |s: &str| {
s.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '/' | '+' | '-' | ':' | ' '))
};
if !safe_charset(tz) {
return Err(SqlError::OpNotSupportedInDialect {
op: "TRUNC_WITH_TZ tz contains characters outside the safe set [A-Za-z0-9_/+-: ]",
dialect: b.d.name(),
});
}
match b.d.name() {
"postgres" => {
b.sql.push_str("date_trunc('");
b.sql.push_str(&unit_lc);
b.sql.push_str("', ");
write_expr(b, &args[0], None)?;
b.sql.push_str(" AT TIME ZONE '");
b.sql.push_str(tz);
b.sql.push_str("')");
}
"mysql" => {
let mask = mysql_trunc_mask(&unit_lc);
b.sql.push_str("DATE_FORMAT(CONVERT_TZ(");
write_expr(b, &args[0], None)?;
b.sql.push_str(", '+00:00', '");
b.sql.push_str(tz);
b.sql.push_str("'), '");
b.sql.push_str(mask);
b.sql.push_str("')");
}
_ => {
let mask = sqlite_trunc_mask(&unit_lc);
b.sql.push_str("strftime('");
b.sql.push_str(mask);
b.sql.push_str("', ");
write_expr(b, &args[0], None)?;
b.sql.push_str(", '");
b.sql.push_str(tz);
b.sql.push_str("')");
}
}
Ok(())
}
fn mysql_trunc_mask(unit: &str) -> &'static str {
match unit {
"year" => "%Y-01-01 00:00:00",
"month" => "%Y-%m-01 00:00:00",
"day" => "%Y-%m-%d 00:00:00",
"hour" => "%Y-%m-%d %H:00:00",
"minute" => "%Y-%m-%d %H:%i:00",
_ => "%Y-%m-%d %H:%i:%S",
}
}
fn sqlite_trunc_mask(unit: &str) -> &'static str {
match unit {
"year" => "%Y-01-01 00:00:00",
"month" => "%Y-%m-01 00:00:00",
"day" => "%Y-%m-%d 00:00:00",
"hour" => "%Y-%m-%d %H:00:00",
"minute" => "%Y-%m-%d %H:%M:00",
_ => "%Y-%m-%d %H:%M:%S",
}
}
fn write_setweight(b: &mut Sql<'_>, args: &[crate::core::Expr]) -> Result<(), SqlError> {
use crate::core::{Expr, SqlValue};
if args.len() != 2 {
return Err(SqlError::FunctionArityMismatch {
func: "setweight",
expected: "2",
got: args.len(),
});
}
if b.d.name() != "postgres" {
return Err(SqlError::OpNotSupportedInDialect {
op: "setweight (FTS) is Postgres-only",
dialect: b.d.name(),
});
}
let weight = match &args[1] {
Expr::Literal(SqlValue::String(w)) => w.as_str(),
_ => {
return Err(SqlError::OpNotSupportedInDialect {
op: "setweight weight argument must be a string literal — build via fts::SearchVector::weighted",
dialect: b.d.name(),
});
}
};
if !matches!(weight, "A" | "B" | "C" | "D") {
return Err(SqlError::OpNotSupportedInDialect {
op: "setweight weight must be one of 'A' | 'B' | 'C' | 'D'",
dialect: b.d.name(),
});
}
b.sql.push_str("setweight(");
write_expr(b, &args[0], None)?;
b.sql.push_str(", '");
b.sql.push_str(weight);
b.sql.push_str("')");
Ok(())
}
fn write_ts_concat(b: &mut Sql<'_>, args: &[crate::core::Expr]) -> Result<(), SqlError> {
if args.len() < 2 {
return Err(SqlError::FunctionArityMismatch {
func: "ts_concat (||)",
expected: ">= 2",
got: args.len(),
});
}
if b.d.name() != "postgres" {
return Err(SqlError::OpNotSupportedInDialect {
op: "tsvector `||` concatenation (FTS) is Postgres-only",
dialect: b.d.name(),
});
}
b.sql.push('(');
for (i, a) in args.iter().enumerate() {
if i > 0 {
b.sql.push_str(" || ");
}
write_expr(b, a, None)?;
}
b.sql.push(')');
Ok(())
}
fn write_extract_quarter_sqlite(b: &mut Sql<'_>, expr: &crate::core::Expr) -> Result<(), SqlError> {
b.sql.push_str("((CAST(strftime('%m', ");
write_expr(b, expr, None)?;
b.sql.push_str(") AS INTEGER) + 2) / 3)");
Ok(())
}
fn write_extract_int(
b: &mut Sql<'_>,
kind: crate::core::ScalarFn,
args: &[crate::core::Expr],
) -> Result<(), SqlError> {
use crate::core::ScalarFn as F;
if args.len() != 1 {
return Err(SqlError::FunctionArityMismatch {
func: "EXTRACT",
expected: "1",
got: args.len(),
});
}
let field = match kind {
F::ExtractYear => "YEAR",
F::ExtractMonth => "MONTH",
F::ExtractDay => "DAY",
F::ExtractHour => "HOUR",
F::ExtractMinute => "MINUTE",
F::ExtractSecond => "SECOND",
F::ExtractWeek => "WEEK",
F::ExtractQuarter => "QUARTER",
_ => unreachable!("write_extract_int called with non-extract kind: {kind:?}"),
};
let dialect = b.d.name();
if dialect == "postgres" {
let floor = field == "SECOND";
b.sql.push_str(if floor {
"CAST(FLOOR(EXTRACT("
} else {
"CAST(EXTRACT("
});
b.sql.push_str(field);
b.sql.push_str(" FROM ");
write_pg_utc_arg(b, &args[0])?;
b.sql.push_str(if floor {
")) AS INTEGER)"
} else {
") AS INTEGER)"
});
} else if dialect == "mysql" {
b.sql.push_str(field);
b.sql.push('(');
write_expr(b, &args[0], None)?;
b.sql.push_str(if field == "WEEK" { ", 3)" } else { ")" });
} else if field == "WEEK" {
b.sql.push_str("((CAST(strftime('%j', date(");
write_expr(b, &args[0], None)?;
b.sql
.push_str(", '-3 days', 'weekday 4')) AS INTEGER) - 1) / 7 + 1)");
} else {
let token = match field {
"YEAR" => "%Y",
"MONTH" => "%m",
"DAY" => "%d",
"HOUR" => "%H",
"MINUTE" => "%M",
"SECOND" => "%S",
_ => unreachable!("sqlite extract: {field}"),
};
b.sql.push_str("CAST(strftime('");
b.sql.push_str(token);
b.sql.push_str("', ");
write_expr(b, &args[0], None)?;
b.sql.push_str(") AS INTEGER)");
}
Ok(())
}
fn write_extract_weekday(b: &mut Sql<'_>, args: &[crate::core::Expr]) -> Result<(), SqlError> {
if args.len() != 1 {
return Err(SqlError::FunctionArityMismatch {
func: "EXTRACT(WEEKDAY)",
expected: "1",
got: args.len(),
});
}
let dialect = b.d.name();
if dialect == "postgres" {
b.sql.push_str("CAST(EXTRACT(DOW FROM ");
write_pg_utc_arg(b, &args[0])?;
b.sql.push_str(") AS INTEGER)");
} else if dialect == "mysql" {
b.sql.push_str("(DAYOFWEEK(");
write_expr(b, &args[0], None)?;
b.sql.push_str(") - 1)");
} else {
b.sql.push_str("CAST(strftime('%w', ");
write_expr(b, &args[0], None)?;
b.sql.push_str(") AS INTEGER)");
}
Ok(())
}
fn write_trunc(
b: &mut Sql<'_>,
kind: crate::core::ScalarFn,
args: &[crate::core::Expr],
) -> Result<(), SqlError> {
use crate::core::ScalarFn as F;
if args.len() != 1 {
return Err(SqlError::FunctionArityMismatch {
func: "DATE_TRUNC",
expected: "1",
got: args.len(),
});
}
let dialect = b.d.name();
let pg_unit = match kind {
F::TruncYear => "year",
F::TruncMonth => "month",
F::TruncDay => "day",
_ => unreachable!("write_trunc: non-trunc kind: {kind:?}"),
};
let format_str = match kind {
F::TruncYear => "%Y-01-01",
F::TruncMonth => "%Y-%m-01",
F::TruncDay => "%Y-%m-%d",
_ => unreachable!(),
};
if dialect == "postgres" {
b.sql.push_str("DATE_TRUNC('");
b.sql.push_str(pg_unit);
b.sql.push_str("', ");
write_expr(b, &args[0], None)?;
if expr_type(b, &args[0]) == Some(crate::core::FieldType::DateTime) {
b.sql.push_str(", 'UTC'");
}
b.sql.push(')');
} else if dialect == "mysql" {
if matches!(kind, F::TruncDay) {
b.sql.push_str("DATE(");
write_expr(b, &args[0], None)?;
b.sql.push(')');
} else {
b.sql.push_str("DATE_FORMAT(");
write_expr(b, &args[0], None)?;
b.sql.push_str(", '");
b.sql.push_str(format_str);
b.sql.push_str("')");
}
} else {
if matches!(kind, F::TruncDay) {
b.sql.push_str("date(");
write_expr(b, &args[0], None)?;
b.sql.push(')');
} else {
b.sql.push_str("strftime('");
b.sql.push_str(format_str);
b.sql.push_str("', ");
write_expr(b, &args[0], None)?;
b.sql.push(')');
}
}
Ok(())
}
fn write_call(b: &mut Sql<'_>, name: &str, args: &[crate::core::Expr]) -> Result<(), SqlError> {
b.sql.push_str(name);
b.sql.push('(');
let mut first = true;
for a in args {
if !first {
b.sql.push_str(", ");
}
first = false;
write_expr(b, a, None)?;
}
b.sql.push(')');
Ok(())
}
fn write_call_unary(
b: &mut Sql<'_>,
name: &'static str,
args: &[crate::core::Expr],
) -> Result<(), SqlError> {
if args.len() != 1 {
return Err(SqlError::FunctionArityMismatch {
func: name,
expected: "1",
got: args.len(),
});
}
write_call(b, name, args)
}
pub(super) fn write_delete(b: &mut Sql<'_>, query: &DeleteQuery) -> Result<(), SqlError> {
b.scope_stack.push(Scope::plain(query.model));
b.dml_target = Some(query.model.table);
let r = (|| {
b.sql.push_str("DELETE FROM ");
b.write_ident(query.model.table);
write_where(b, &query.where_clause, Some(query.model))?;
Ok(())
})();
b.scope_stack.pop();
r
}
#[doc(hidden)]
pub struct DeletePlan {
pub(crate) links: Vec<CompiledStatement>,
pub(crate) owner: CompiledStatement,
pub(crate) cascade: Option<DeleteQuery>,
}
impl DeletePlan {
pub(crate) fn is_one_statement(&self) -> bool {
self.links.is_empty() && self.cascade.is_none()
}
}
pub(crate) fn compile_delete_plan(
d: &dyn Dialect,
query: &DeleteQuery,
) -> Result<DeletePlan, SqlError> {
let links = super::m2m::generic_m2m_links(query);
let cascade = crate::audit::cascade::reaches_audited(query.model).then(|| query.clone());
if links.is_empty() || !d.supports_dml_in_cte() {
return Ok(DeletePlan {
links: links
.iter()
.map(|l| d.compile_delete(l))
.collect::<Result<_, _>>()?,
owner: d.compile_delete(query)?,
cascade,
});
}
let mut b = Sql::new(d);
b.sql.push_str("WITH ");
for (i, link) in links.iter().enumerate() {
if i > 0 {
b.sql.push_str(", ");
}
b.write_ident(&format!("_rustango_link{i}"));
b.sql.push_str(" AS (");
write_delete(&mut b, link)?;
b.sql.push(')');
}
b.sql.push(' ');
write_delete(&mut b, query)?;
Ok(DeletePlan {
links: Vec::new(),
owner: b.finish()?,
cascade,
})
}
pub(super) fn write_bulk_update_sqlite(
b: &mut Sql<'_>,
query: &BulkUpdateQuery,
) -> Result<(), SqlError> {
if query.rows.is_empty() {
return Err(SqlError::EmptyBulkInsert);
}
if query.update_columns.is_empty() {
return Err(SqlError::EmptyUpdateSet);
}
let pk_field = query
.model
.primary_key()
.ok_or(SqlError::MissingPrimaryKey)?;
b.sql.push_str("WITH __data(");
b.write_ident(pk_field.column);
for col in &query.update_columns {
b.sql.push_str(", ");
b.write_ident(col);
}
b.sql.push_str(") AS (VALUES ");
let mut first_row = true;
for row in &query.rows {
if !first_row {
b.sql.push_str(", ");
}
first_row = false;
b.sql.push('(');
for (i, val) in row.iter().enumerate() {
if i > 0 {
b.sql.push_str(", ");
}
b.push_param(val.clone());
}
b.sql.push(')');
}
b.sql.push_str(") UPDATE ");
b.write_ident(query.model.table);
b.sql.push_str(" SET ");
let mut first_col = true;
for col in &query.update_columns {
if !first_col {
b.sql.push_str(", ");
}
first_col = false;
b.write_ident(col);
b.sql.push_str(" = (SELECT ");
b.write_ident(col);
b.sql.push_str(" FROM __data WHERE __data.");
b.write_ident(pk_field.column);
b.sql.push_str(" = ");
b.write_ident(query.model.table);
b.sql.push('.');
b.write_ident(pk_field.column);
b.sql.push(')');
}
b.sql.push_str(" WHERE ");
b.write_ident(pk_field.column);
b.sql.push_str(" IN (SELECT ");
b.write_ident(pk_field.column);
b.sql.push_str(" FROM __data)");
Ok(())
}
pub(super) fn write_bulk_update_pg(
b: &mut Sql<'_>,
query: &BulkUpdateQuery,
) -> Result<(), SqlError> {
if query.rows.is_empty() {
return Err(SqlError::EmptyBulkInsert);
}
if query.update_columns.is_empty() {
return Err(SqlError::EmptyUpdateSet);
}
let pk_field = query
.model
.primary_key()
.ok_or(SqlError::MissingPrimaryKey)?;
b.sql.push_str("UPDATE ");
b.write_ident(query.model.table);
b.sql.push_str(" SET ");
let mut first = true;
for col in &query.update_columns {
if !first {
b.sql.push_str(", ");
}
first = false;
b.write_ident(col);
b.sql.push_str(" = __data.");
b.write_ident(col);
}
b.sql.push_str(" FROM (VALUES ");
let casts: Vec<Option<BindCast>> = std::iter::once(pk_field.column)
.chain(query.update_columns.iter().copied())
.map(|c| {
let vector = query
.model
.field_by_column(c)
.is_some_and(|f| matches!(f.ty, crate::core::FieldType::Vector(_)));
null_cast_for(b.d, query.model, c).or(vector.then_some(BindCast::Untyped("vector")))
})
.collect();
let mut first_row = true;
for row in &query.rows {
if !first_row {
b.sql.push_str(", ");
}
first_row = false;
b.sql.push('(');
for (i, val) in row.iter().enumerate() {
if i > 0 {
b.sql.push_str(", ");
}
b.push_param_typed(val.clone(), casts.get(i).copied().flatten());
}
b.sql.push(')');
}
b.sql.push_str(") AS __data(");
b.write_ident(pk_field.column);
for col in &query.update_columns {
b.sql.push_str(", ");
b.write_ident(col);
}
b.sql.push_str(") WHERE ");
b.write_ident(query.model.table);
b.sql.push('.');
b.write_ident(pk_field.column);
b.sql.push_str(" = __data.");
b.write_ident(pk_field.column);
Ok(())
}
pub(super) fn write_where(
b: &mut Sql<'_>,
where_clause: &WhereExpr,
model: Option<&'static ModelSchema>,
) -> Result<(), SqlError> {
if where_clause.is_empty() {
return Ok(());
}
b.sql.push_str(" WHERE ");
write_where_expr(b, where_clause, model)
}
pub(super) fn write_where_with_search(
b: &mut Sql<'_>,
where_clause: &WhereExpr,
search: Option<&SearchClause>,
model: Option<&'static ModelSchema>,
) -> Result<(), SqlError> {
let search = search.filter(|s| !s.query.is_empty());
let has_search = search.is_some();
let has_where = !where_clause.is_empty();
if !has_where && !has_search {
return Ok(());
}
b.sql.push_str(" WHERE ");
let wrap = has_where && has_search;
if has_where {
if wrap {
b.sql.push('(');
}
write_where_expr(b, where_clause, model)?;
if wrap {
b.sql.push(')');
}
}
if let Some(s) = search {
if has_where {
b.sql.push_str(" AND ");
}
if s.columns.is_empty() {
b.sql.push_str("1 = 0");
return Ok(());
}
let pattern = format!("%{}%", crate::core::escape_like(&s.query));
b.sql.push('(');
for (i, col) in s.columns.iter().enumerate() {
if i > 0 {
b.sql.push_str(" OR ");
}
b.params.push(SqlValue::String(pattern.clone()));
let placeholder = b.d.placeholder(b.params.len());
let mut qualified = String::new();
if let Some(table) = b.qualify_alias(col) {
qualified.push_str(&b.d.quote_ident(&table));
qualified.push('.');
}
qualified.push_str(&b.d.quote_ident(col));
let ty = model.and_then(|m| m.field_by_column(col)).map(|f| f.ty);
b.d.write_ilike_typed(&mut b.sql, &qualified, ty, &placeholder, false);
b.sql.push_str(LIKE_ESCAPE_CLAUSE);
}
b.sql.push(')');
}
Ok(())
}
fn write_like(
b: &mut Sql<'_>,
col: &str,
ty: Option<crate::core::FieldType>,
value: SqlValue,
negated: bool,
escaped: bool,
) {
if b.d.name() == "sqlite" {
if let SqlValue::String(pattern) = &value {
let p = b.bind(SqlValue::String(like_to_glob(pattern, escaped)));
b.sql.push_str(&b.d.like_operand(col, ty));
b.sql
.push_str(if negated { " NOT GLOB " } else { " GLOB " });
b.sql.push_str(&p);
return;
}
}
let p = b.bind(value);
b.d.write_like_typed(&mut b.sql, col, ty, &p, negated);
if escaped {
b.sql.push_str(LIKE_ESCAPE_CLAUSE);
}
}
fn like_to_glob(pattern: &str, escaped: bool) -> String {
let literal = |out: &mut String, c: char| match c {
'*' | '?' | '[' => {
out.push('[');
out.push(c);
out.push(']');
}
c => out.push(c),
};
let mut out = String::with_capacity(pattern.len());
let mut chars = pattern.chars();
while let Some(c) = chars.next() {
match c {
'!' if escaped => {
if let Some(next) = chars.next() {
literal(&mut out, next);
}
}
'%' => out.push('*'),
'_' => out.push('?'),
c => literal(&mut out, c),
}
}
out
}
pub(super) fn write_where_expr(
b: &mut Sql<'_>,
expr: &WhereExpr,
model: Option<&'static ModelSchema>,
) -> Result<(), SqlError> {
match expr {
WhereExpr::Predicate(filter) => write_filter(b, filter, model),
WhereExpr::ColumnCompare(cf) => write_column_compare(b, cf, model),
WhereExpr::And(items) if items.is_empty() => {
b.sql.push_str("1 = 1");
Ok(())
}
WhereExpr::And(items) => write_joined(b, items, " AND ", model),
WhereExpr::Or(items) => {
if items.is_empty() {
return Err(SqlError::EmptyOrBranch);
}
write_joined(b, items, " OR ", model)
}
WhereExpr::Xor(items) => write_xor(b, items, model),
WhereExpr::Not(child) => {
b.sql.push_str("NOT (");
write_where_expr(b, child, model)?;
b.sql.push(')');
Ok(())
}
WhereExpr::Exists(subq) => {
b.sql.push_str("EXISTS (");
b.with_derived_dml_target(2, |b| write_select(b, subq))?;
b.sql.push(')');
Ok(())
}
WhereExpr::NotExists(subq) => {
b.sql.push_str("NOT EXISTS (");
b.with_derived_dml_target(2, |b| write_select(b, subq))?;
b.sql.push(')');
Ok(())
}
WhereExpr::InSubquery {
column,
negated,
subquery,
} => {
let qualified = render_qualified_col(b, column);
b.sql.push_str(&qualified);
b.sql.push_str(if *negated { " NOT IN (" } else { " IN (" });
let limited = subquery.limit.is_some()
|| subquery.offset.is_some()
|| subquery.compound_limit.is_some()
|| subquery.compound_offset.is_some();
let outer_read = std::mem::take(&mut b.read_dml_target);
let outer_reach = b.outer_reach.take();
let start = b.sql.len();
write_select(b, subquery)?;
let reads_target = b.read_dml_target;
let correlated = b.outer_reach.is_some_and(|d| d <= b.scope_stack.len());
b.outer_reach = match (outer_reach, b.outer_reach) {
(Some(a), Some(c)) => Some(a.min(c)),
(a, c) => a.or(c),
};
let wrap = (limited || reads_target)
&& !(reads_target && correlated)
&& b.d.in_subquery_limit_needs_derived_table();
b.read_dml_target = outer_read || (reads_target && !wrap);
if wrap {
b.sql.insert_str(start, "SELECT * FROM (");
b.sql.push_str(") AS ");
b.write_ident("_rustango_in");
}
b.sql.push(')');
Ok(())
}
WhereExpr::ExprCompare { lhs, op, rhs } => write_expr_compare(b, lhs, *op, rhs),
WhereExpr::RelExists {
table,
correlation,
negated,
} => {
b.sql
.push_str(if *negated { "NOT EXISTS (" } else { "EXISTS (" });
b.sql.push_str("SELECT 1 FROM ");
b.with_derived_dml_target(1, |b| write_rel_correlation(b, table, correlation))?;
b.sql.push(')');
Ok(())
}
}
}
fn write_rel_correlation(
b: &mut Sql<'_>,
table: &str,
correlation: &crate::core::RelCorrelation,
) -> Result<(), SqlError> {
use crate::core::RelCorrelation;
let outer_table = match b.scope_stack.last() {
Some(s) => s.name,
None => {
return Err(SqlError::OuterRefOutsideSubquery {
column: "<relation>",
})
}
};
b.read_outer(b.scope_stack.len());
let alias = (table == outer_table).then_some(SELF_ALIAS);
b.write_table(table, alias);
let table = alias.unwrap_or(table);
b.sql.push_str(" WHERE ");
match correlation {
RelCorrelation::Fk {
fk_column,
outer_column,
ct,
} => {
b.write_ident(table);
b.sql.push('.');
b.write_ident(fk_column);
b.sql.push_str(" = ");
b.write_ident(outer_table);
b.sql.push('.');
b.write_ident(outer_column);
if let Some(ct) = ct {
b.sql.push_str(" AND ");
b.write_ident(table);
b.sql.push('.');
b.write_ident(ct.ct_column);
b.sql.push_str(" = (SELECT ");
b.write_ident(ct.ct_pk);
b.sql.push_str(" FROM ");
b.write_table(ct.ct_table, None);
b.sql.push_str(" WHERE ");
b.write_ident(ct.ct_table_col);
b.sql.push_str(" = ");
b.push_param(crate::core::SqlValue::String(ct.parent_table.to_owned()));
b.sql.push(')');
}
Ok(())
}
RelCorrelation::Membership {
target_pk,
through,
dst_col,
src_col,
outer_column,
} => {
b.write_ident(table);
b.sql.push('.');
b.write_ident(target_pk);
b.sql.push_str(" IN (SELECT ");
b.write_ident(dst_col);
b.sql.push_str(" FROM ");
let derive = b.derive_dml_target.take();
b.write_table(through, None);
b.derive_dml_target = derive;
b.sql.push_str(" WHERE ");
b.write_ident(through);
b.sql.push('.');
b.write_ident(src_col);
b.sql.push_str(" = ");
b.write_ident(outer_table);
b.sql.push('.');
b.write_ident(outer_column);
b.sql.push(')');
Ok(())
}
}
}
fn null_test(op: Op, value: &SqlValue) -> Option<&'static str> {
match (op, value) {
(Op::Eq, SqlValue::Null) => Some(" IS NULL"),
(Op::Ne, SqlValue::Null) => Some(" IS NOT NULL"),
_ => None,
}
}
fn write_expr_compare(
b: &mut Sql<'_>,
lhs: &crate::core::Expr,
op: crate::core::Op,
rhs: &crate::core::Expr,
) -> Result<(), SqlError> {
use crate::core::{Expr, Op};
if let Expr::Literal(v) = rhs {
if let Some(test) = null_test(op, v) {
write_expr(b, lhs, None)?;
b.sql.push_str(test);
return Ok(());
}
}
let binary_op_str = match op {
Op::Eq => Some(" = "),
Op::Ne => Some(" <> "),
Op::Lt => Some(" < "),
Op::Lte => Some(" <= "),
Op::Gt => Some(" > "),
Op::Gte => Some(" >= "),
_ => None,
};
if write_sqlite_json_value_eq(b, lhs, op, rhs)? {
return Ok(());
}
if let Some(kw) = binary_op_str {
let start = b.sql.len();
write_expr(b, lhs, None)?;
let sqlite_path = b.d.name() == "sqlite" && matches!(lhs, Expr::JsonPath { .. });
if let (Expr::Literal(v @ SqlValue::Json(_)), false) = (rhs, sqlite_path) {
let lhs_sql = b.sql.split_off(start);
if try_json_eq(b, &lhs_sql, op, v) {
return Ok(());
}
b.sql.push_str(&lhs_sql);
}
b.sql.push_str(kw);
return write_compare_rhs(b, lhs, rhs);
}
if matches!(op, Op::Like | Op::NotLike | Op::LikeEscaped) {
let lhs_start = b.sql.len();
write_expr(b, lhs, None)?;
let lhs_str = b.sql.split_off(lhs_start);
let ty = expr_type(b, lhs);
let (negated, escaped) = (matches!(op, Op::NotLike), matches!(op, Op::LikeEscaped));
if let Expr::Literal(v) = rhs {
write_like(b, &lhs_str, ty, v.clone(), negated, escaped);
return Ok(());
}
let p_start = b.sql.len();
write_expr(b, rhs, None)?;
let p = b.sql.split_off(p_start);
b.d.write_like_typed(&mut b.sql, &lhs_str, ty, &p, negated);
if escaped {
b.sql.push_str(LIKE_ESCAPE_CLAUSE);
}
return Ok(());
}
match op {
Op::Search => {
require_op(b.d, op)?;
write_expr(b, lhs, None)?;
b.sql.push_str(" @@ ");
write_expr(b, rhs, None)?;
Ok(())
}
Op::ILike | Op::NotILike | Op::ILikeEscaped => {
require_op(
b.d,
if matches!(op, Op::ILikeEscaped) {
Op::ILike
} else {
op
},
)?;
let lhs_start = b.sql.len();
write_expr(b, lhs, None)?;
let lhs_str = b.sql.split_off(lhs_start);
let Expr::Literal(v) = rhs else {
return Err(SqlError::OpNotSupportedInDialect {
op: "ILIKE with non-literal RHS in ExprCompare",
dialect: b.d.name(),
});
};
b.params.push(v.clone());
let p = b.d.placeholder(b.params.len());
let ty = expr_type(b, lhs);
b.d.write_ilike_typed(&mut b.sql, &lhs_str, ty, &p, matches!(op, Op::NotILike));
if matches!(op, Op::ILikeEscaped) {
b.sql.push_str(LIKE_ESCAPE_CLAUSE);
}
Ok(())
}
Op::In | Op::NotIn => {
let Expr::Literal(SqlValue::List(elements)) = rhs else {
return Err(SqlError::InRequiresList);
};
if elements.is_empty() {
return Err(SqlError::EmptyInList);
}
write_expr(b, lhs, None)?;
write_in_list(b, op, elements, None);
Ok(())
}
Op::Between | Op::NotBetween => {
let Expr::Literal(SqlValue::List(bounds)) = rhs else {
return Err(SqlError::BetweenRequiresTwoElementList);
};
if bounds.len() != 2 {
return Err(SqlError::BetweenRequiresTwoElementList);
}
write_expr(b, lhs, None)?;
b.sql.push_str(if matches!(op, Op::NotBetween) {
" NOT BETWEEN "
} else {
" BETWEEN "
});
b.push_param_typed(bounds[0].clone(), None);
b.sql.push_str(" AND ");
b.push_param_typed(bounds[1].clone(), None);
Ok(())
}
Op::IsNull => {
let Expr::Literal(SqlValue::Bool(is_null)) = rhs else {
return Err(SqlError::IsNullRequiresBool);
};
write_expr(b, lhs, None)?;
b.sql
.push_str(if *is_null { " IS NULL" } else { " IS NOT NULL" });
Ok(())
}
_ => Err(SqlError::OpNotSupportedInDialect {
op: "non-binary comparison in ExprCompare",
dialect: b.d.name(),
}),
}
}
fn write_compare_rhs(
b: &mut Sql<'_>,
lhs: &crate::core::Expr,
rhs: &crate::core::Expr,
) -> Result<(), SqlError> {
use crate::core::Expr;
let (Expr::JsonPath { as_text, .. }, Expr::Literal(v)) = (lhs, rhs) else {
let cast = match lhs {
Expr::Column(c) if !matches!(b.qualifier(), Some(Qualifier::Join(_))) => {
b.scope_model().and_then(|m| null_cast_for(b.d, m, c))
}
Expr::AliasedColumn { alias, column } => {
alias_model(b, alias).and_then(|m| null_cast_for(b.d, m, column))
}
_ => None,
};
return write_expr(b, rhs, cast);
};
let (open, close) = match (as_text, v) {
_ if b.d.name() != "sqlite" => return write_expr(b, rhs, None),
(false, SqlValue::Json(_)) => ("json_extract(", ", '$')"),
(
true,
SqlValue::I16(_)
| SqlValue::I32(_)
| SqlValue::I64(_)
| SqlValue::F32(_)
| SqlValue::F64(_),
) => ("CAST(", " AS NUMERIC)"),
_ => return write_expr(b, rhs, None),
};
b.sql.push_str(open);
b.push_param(v.clone());
b.sql.push_str(close);
Ok(())
}
fn write_column_compare(
b: &mut Sql<'_>,
cf: &crate::core::ColumnFilter,
_model: Option<&'static ModelSchema>,
) -> Result<(), SqlError> {
let qualified = render_qualified_col(b, cf.column);
b.sql.push_str(&qualified);
if let crate::core::Expr::Literal(v) = &cf.rhs {
if let Some(test) = null_test(cf.op, v) {
b.sql.push_str(test);
return Ok(());
}
}
let op_str = match cf.op {
crate::core::Op::Eq => " = ",
crate::core::Op::Ne => " <> ",
crate::core::Op::Lt => " < ",
crate::core::Op::Lte => " <= ",
crate::core::Op::Gt => " > ",
crate::core::Op::Gte => " >= ",
_ => {
return Err(SqlError::OpNotSupportedInDialect {
op: "non-binary comparison in ColumnCompare",
dialect: b.d.name(),
});
}
};
b.sql.push_str(op_str);
write_expr(b, &cf.rhs, None)?;
Ok(())
}
fn write_joined(
b: &mut Sql<'_>,
items: &[WhereExpr],
sep: &str,
model: Option<&'static ModelSchema>,
) -> Result<(), SqlError> {
let mut first = true;
for child in items {
if !first {
b.sql.push_str(sep);
}
first = false;
write_child(b, child, model)?;
}
Ok(())
}
fn write_child(
b: &mut Sql<'_>,
expr: &WhereExpr,
model: Option<&'static ModelSchema>,
) -> Result<(), SqlError> {
match expr {
WhereExpr::Predicate(filter) => write_filter(b, filter, model),
WhereExpr::ColumnCompare(cf) => write_column_compare(b, cf, model),
WhereExpr::Exists(_)
| WhereExpr::NotExists(_)
| WhereExpr::InSubquery { .. }
| WhereExpr::ExprCompare { .. }
| WhereExpr::RelExists { .. } => write_where_expr(b, expr, model),
WhereExpr::And(_) | WhereExpr::Or(_) | WhereExpr::Xor(_) | WhereExpr::Not(_) => {
b.sql.push('(');
write_where_expr(b, expr, model)?;
b.sql.push(')');
Ok(())
}
}
}
fn write_xor(
b: &mut Sql<'_>,
items: &[WhereExpr],
model: Option<&'static ModelSchema>,
) -> Result<(), SqlError> {
match items.len() {
0 => Err(SqlError::EmptyXorBranch),
1 => write_where_expr(b, &items[0], model),
_ => {
b.sql.push('(');
let mut first = true;
for child in items {
if !first {
b.sql.push_str(" + ");
}
first = false;
b.sql.push_str("(CASE WHEN ");
write_child(b, child, model)?;
b.sql.push_str(" THEN 1 ELSE 0 END)");
}
b.sql.push_str(") % 2 = 1");
Ok(())
}
}
}
#[allow(clippy::too_many_lines)] fn write_filter(
b: &mut Sql<'_>,
filter: &Filter,
model: Option<&'static ModelSchema>,
) -> Result<(), SqlError> {
let qualified_col = render_qualified_col(b, filter.column);
if let Some(test) = null_test(filter.op, &filter.value) {
b.sql.push_str(&qualified_col);
b.sql.push_str(test);
return Ok(());
}
if try_json_eq(b, &qualified_col, filter.op, &filter.value) {
return Ok(());
}
let cast = model.and_then(|m| null_cast_for(b.d, m, filter.column));
match filter.op {
Op::Eq => simple_op(b, &qualified_col, " = ", filter.value.clone(), cast),
Op::Ne => simple_op(b, &qualified_col, " <> ", filter.value.clone(), cast),
Op::Lt => simple_op(b, &qualified_col, " < ", filter.value.clone(), cast),
Op::Lte => simple_op(b, &qualified_col, " <= ", filter.value.clone(), cast),
Op::Gt => simple_op(b, &qualified_col, " > ", filter.value.clone(), cast),
Op::Gte => simple_op(b, &qualified_col, " >= ", filter.value.clone(), cast),
Op::Like | Op::NotLike | Op::LikeEscaped => {
let ty = model
.and_then(|m| m.field_by_column(filter.column))
.map(|f| f.ty);
write_like(
b,
&qualified_col,
ty,
filter.value.clone(),
matches!(filter.op, Op::NotLike),
matches!(filter.op, Op::LikeEscaped),
);
}
Op::ILike | Op::NotILike | Op::ILikeEscaped => {
require_op(
b.d,
if matches!(filter.op, Op::ILikeEscaped) {
Op::ILike
} else {
filter.op
},
)?;
b.params.push(filter.value.clone());
let p = b.d.placeholder(b.params.len());
let ty = model
.and_then(|m| m.field_by_column(filter.column))
.map(|f| f.ty);
b.d.write_ilike_typed(
&mut b.sql,
&qualified_col,
ty,
&p,
matches!(filter.op, Op::NotILike),
);
if matches!(filter.op, Op::ILikeEscaped) {
b.sql.push_str(LIKE_ESCAPE_CLAUSE);
}
}
Op::Regex | Op::NotRegex | Op::IRegex | Op::NotIRegex => {
require_op(b.d, filter.op)?;
b.params.push(filter.value.clone());
let p = b.d.placeholder(b.params.len());
b.d.write_regex(
&mut b.sql,
&qualified_col,
&p,
matches!(filter.op, Op::Regex | Op::NotRegex),
matches!(filter.op, Op::NotRegex | Op::NotIRegex),
);
}
Op::TrigramSimilar | Op::TrigramWordSimilar => {
require_op(b.d, filter.op)?;
b.params.push(filter.value.clone());
let p = b.d.placeholder(b.params.len());
b.d.write_trigram_similar(
&mut b.sql,
&qualified_col,
&p,
matches!(filter.op, Op::TrigramWordSimilar),
)?;
}
Op::Search => {
require_op(b.d, filter.op)?;
b.params.push(filter.value.clone());
let p = b.d.placeholder(b.params.len());
b.d.write_search(&mut b.sql, &qualified_col, &p)?;
}
Op::ArrayContains | Op::ArrayContainedBy | Op::ArrayOverlap => {
if !matches!(filter.value, SqlValue::Array(_)) {
return Err(SqlError::ArrayOpRequiresArray);
}
require_op(b.d, filter.op)?;
b.params.push(filter.value.clone());
let p = b.d.placeholder(b.params.len());
let op_str: &'static str = match filter.op {
Op::ArrayContains => "@>",
Op::ArrayContainedBy => "<@",
Op::ArrayOverlap => "&&",
_ => unreachable!(),
};
b.d.write_array_op(&mut b.sql, &qualified_col, &p, op_str)?;
}
Op::RangeContains
| Op::RangeContainedBy
| Op::RangeOverlap
| Op::RangeStrictlyLeft
| Op::RangeStrictlyRight
| Op::RangeAdjacent => {
require_op(b.d, filter.op)?;
let range_cast = if matches!(filter.value, SqlValue::RangeLiteral(_)) {
model
.and_then(|m| m.field_by_column(filter.column))
.filter(|f| matches!(f.ty, crate::core::FieldType::Range(_)))
.and_then(|f| b.d.cast_type(f.ty))
} else {
None
};
b.params.push(filter.value.clone());
let mut p = b.d.placeholder(b.params.len());
if let Some(ty) = range_cast {
p.push_str("::");
p.push_str(ty);
}
let op_str: &'static str = match filter.op {
Op::RangeContains => "@>",
Op::RangeContainedBy => "<@",
Op::RangeOverlap => "&&",
Op::RangeStrictlyLeft => "<<",
Op::RangeStrictlyRight => ">>",
Op::RangeAdjacent => "-|-",
_ => unreachable!(),
};
b.d.write_range_op(&mut b.sql, &qualified_col, &p, op_str)?;
}
Op::In | Op::NotIn => {
let SqlValue::List(elements) = &filter.value else {
return Err(SqlError::InRequiresList);
};
if elements.is_empty() {
return Err(SqlError::EmptyInList);
}
b.sql.push_str(&qualified_col);
write_in_list(b, filter.op, elements, cast);
}
Op::Between | Op::NotBetween => {
let SqlValue::List(bounds) = &filter.value else {
return Err(SqlError::BetweenRequiresTwoElementList);
};
if bounds.len() != 2 {
return Err(SqlError::BetweenRequiresTwoElementList);
}
b.sql.push_str(&qualified_col);
b.sql.push_str(if matches!(filter.op, Op::NotBetween) {
" NOT BETWEEN "
} else {
" BETWEEN "
});
b.push_param_typed(bounds[0].clone(), cast);
b.sql.push_str(" AND ");
b.push_param_typed(bounds[1].clone(), cast);
}
Op::IsNull => {
let SqlValue::Bool(is_null) = filter.value else {
return Err(SqlError::IsNullRequiresBool);
};
b.sql.push_str(&qualified_col);
b.sql
.push_str(if is_null { " IS NULL" } else { " IS NOT NULL" });
}
Op::IsDistinctFrom | Op::IsNotDistinctFrom => {
require_op(b.d, filter.op)?;
let p = b.bind(filter.value.clone());
b.d.write_null_safe_eq(
&mut b.sql,
&qualified_col,
&p,
matches!(filter.op, Op::IsDistinctFrom),
);
}
Op::JsonContains => {
require_op(b.d, filter.op)?;
let SqlValue::Json(_) = &filter.value else {
return Err(SqlError::JsonOpRequiresJson);
};
b.params.push(filter.value.clone());
let p = b.d.placeholder(b.params.len());
b.d.write_json_contains(&mut b.sql, &qualified_col, &p);
}
Op::JsonContainedBy => {
require_op(b.d, filter.op)?;
let SqlValue::Json(_) = &filter.value else {
return Err(SqlError::JsonOpRequiresJson);
};
b.params.push(filter.value.clone());
let p = b.d.placeholder(b.params.len());
b.d.write_json_contained_by(&mut b.sql, &qualified_col, &p);
}
Op::JsonHasKey => {
require_op(b.d, filter.op)?;
let SqlValue::String(_) = &filter.value else {
return Err(SqlError::JsonKeyRequiresString);
};
b.params.push(filter.value.clone());
let p = b.d.placeholder(b.params.len());
b.d.write_json_has_key(&mut b.sql, &qualified_col, &p);
}
Op::JsonHasAnyKey | Op::JsonHasAllKeys => {
require_op(b.d, filter.op)?;
let SqlValue::List(keys) = &filter.value else {
return Err(SqlError::JsonKeysRequiresList);
};
let placeholders = bind_param_list(b, keys);
if matches!(filter.op, Op::JsonHasAnyKey) {
b.d.write_json_has_any_keys(&mut b.sql, &qualified_col, &placeholders);
} else {
b.d.write_json_has_all_keys(&mut b.sql, &qualified_col, &placeholders);
}
}
}
Ok(())
}
fn try_json_eq(b: &mut Sql<'_>, lhs: &str, op: Op, value: &SqlValue) -> bool {
let (Op::Eq | Op::Ne, SqlValue::Json(_)) = (op, value) else {
return false;
};
let p = b.d.placeholder(b.params.len() + 1);
let mut out = String::new();
if !b.d.write_json_eq(&mut out, lhs, &p, matches!(op, Op::Ne)) {
return false;
}
b.params.push(value.clone());
b.sql.push_str(&out);
true
}
fn simple_op(
b: &mut Sql<'_>,
qualified_col: &str,
kw: &str,
value: SqlValue,
cast: Option<BindCast>,
) {
b.sql.push_str(qualified_col);
b.sql.push_str(kw);
b.push_param_typed(value, cast);
}
fn render_qualified_col(b: &Sql<'_>, column: &str) -> String {
let mut s = String::new();
if let Some(table) = b.qualify_alias(column) {
s.push_str(&b.d.quote_ident(&table));
s.push('.');
}
s.push_str(&b.d.quote_ident(column));
s
}
fn bind_param_list(b: &mut Sql<'_>, values: &[SqlValue]) -> Vec<String> {
let mut out = Vec::with_capacity(values.len());
for v in values {
b.params.push(v.clone());
out.push(b.d.placeholder(b.params.len()));
}
out
}
fn require_op(d: &dyn Dialect, op: Op) -> Result<(), SqlError> {
if d.supports_op(op) {
Ok(())
} else {
Err(SqlError::OperatorNotSupportedInDialect {
op: op_label(op),
dialect: d.name(),
})
}
}
fn op_label(op: Op) -> &'static str {
match op {
Op::Eq => "=",
Op::Ne => "<>",
Op::Lt => "<",
Op::Lte => "<=",
Op::Gt => ">",
Op::Gte => ">=",
Op::In => "IN",
Op::NotIn => "NOT IN",
Op::Like => "LIKE",
Op::NotLike => "NOT LIKE",
Op::ILike => "ILIKE",
Op::NotILike => "NOT ILIKE",
Op::LikeEscaped => "LIKE ... ESCAPE",
Op::ILikeEscaped => "ILIKE ... ESCAPE",
Op::Between => "BETWEEN",
Op::NotBetween => "NOT BETWEEN",
Op::IsNull => "IS NULL",
Op::IsDistinctFrom => "IS DISTINCT FROM",
Op::IsNotDistinctFrom => "IS NOT DISTINCT FROM",
Op::JsonContains => "@>",
Op::JsonContainedBy => "<@",
Op::JsonHasKey => "? (json)",
Op::JsonHasAnyKey => "?| (json)",
Op::JsonHasAllKeys => "?& (json)",
Op::Regex => "~ (regex)",
Op::NotRegex => "!~ (regex)",
Op::TrigramSimilar => "% (trigram_similar)",
Op::TrigramWordSimilar => "%> (trigram_word_similar)",
Op::Search => "@@ (search)",
Op::ArrayContains => "@> (array_contains)",
Op::ArrayContainedBy => "<@ (array_contained_by)",
Op::ArrayOverlap => "&& (array_overlap)",
Op::RangeContains => "@> (range_contains)",
Op::RangeContainedBy => "<@ (range_contained_by)",
Op::RangeOverlap => "&& (range_overlap)",
Op::RangeStrictlyLeft => "<< (range_strictly_left)",
Op::RangeStrictlyRight => ">> (range_strictly_right)",
Op::RangeAdjacent => "-|- (range_adjacent)",
Op::IRegex => "~* (iregex)",
Op::NotIRegex => "!~* (iregex)",
}
}
fn write_order_limit_offset(
b: &mut Sql<'_>,
order_by: &[crate::core::OrderItem],
limit: Option<i64>,
offset: Option<i64>,
) -> Result<(), SqlError> {
write_order_limit_offset_with(b, order_by, limit, offset, |b, _, item| {
write_order_target(b, item)
})
}
fn write_order_limit_offset_with(
b: &mut Sql<'_>,
order_by: &[crate::core::OrderItem],
limit: Option<i64>,
offset: Option<i64>,
target: impl Fn(&mut Sql<'_>, usize, &crate::core::OrderItem) -> Result<(), SqlError>,
) -> Result<(), SqlError> {
use crate::core::{NullsOrder, OrderItem};
if !order_by.is_empty() {
b.sql.push_str(" ORDER BY ");
let supports_nulls = b.d.supports_nulls_order();
let mut first = true;
for (i, item) in order_by.iter().enumerate() {
let (desc, nulls) = match item {
OrderItem::Column { desc, nulls, .. } => (*desc, *nulls),
OrderItem::Expr { desc, nulls, .. } => (*desc, *nulls),
OrderItem::Random => (false, NullsOrder::Default),
};
if !supports_nulls && !matches!(nulls, NullsOrder::Default) {
if !first {
b.sql.push_str(", ");
}
first = false;
target(b, i, item)?;
b.sql.push_str(" IS NULL");
match nulls {
NullsOrder::First => b.sql.push_str(" DESC"),
NullsOrder::Last => b.sql.push_str(" ASC"),
NullsOrder::Default => unreachable!(),
}
}
if !first {
b.sql.push_str(", ");
}
first = false;
target(b, i, item)?;
if desc {
b.sql.push_str(" DESC");
}
if supports_nulls {
match nulls {
NullsOrder::First => b.sql.push_str(" NULLS FIRST"),
NullsOrder::Last => b.sql.push_str(" NULLS LAST"),
NullsOrder::Default => {}
}
}
}
}
if let Some(n) = limit {
let _ = write!(b.sql, " LIMIT {n}");
} else if offset.is_some() {
if let Some(clause) = b.d.offset_without_limit_clause() {
b.sql.push_str(clause);
}
}
if let Some(n) = offset {
let _ = write!(b.sql, " OFFSET {n}");
}
Ok(())
}
fn write_order_target(b: &mut Sql<'_>, item: &crate::core::OrderItem) -> Result<(), SqlError> {
use crate::core::OrderItem;
match item {
OrderItem::Column { column, .. } => {
let qualified = render_qualified_col(b, column);
b.sql.push_str(&qualified);
}
OrderItem::Expr { expr, .. } => {
write_expr(b, expr, None)?;
}
OrderItem::Random => {
b.sql.push_str(b.d.random_fn());
b.sql.push_str("()");
}
}
Ok(())
}
fn write_returning(b: &mut Sql<'_>, returning: &[&'static str]) -> Result<(), SqlError> {
if returning.is_empty() {
return Ok(());
}
if !b.d.supports_returning() {
return Err(SqlError::OperatorNotSupportedInDialect {
op: "RETURNING",
dialect: b.d.name(),
});
}
b.sql.push_str(" RETURNING ");
let mut first = true;
for col in returning {
if !first {
b.sql.push_str(", ");
}
first = false;
b.write_ident(col);
}
Ok(())
}
pub(crate) fn compile_select_with_child_count(
d: &dyn Dialect,
select: &SelectQuery,
child: ChildCount,
) -> Result<CompiledStatement, SqlError> {
let mut b = Sql::new(d);
if select.compound.is_empty() {
b.child_count = Some(child);
write_select(&mut b, select)?;
return b.finish();
}
const PARENT: &str = "__rustango_parent";
let merged = SelectQuery {
compound_order_by: Vec::new(),
compound_limit: None,
compound_offset: None,
lock_mode: None,
..select.clone()
};
b.sql.push_str("SELECT ");
b.write_ident(PARENT);
b.sql.push_str(".*");
write_child_count(&mut b, PARENT, child);
b.sql.push_str(" FROM (");
write_select(&mut b, &merged)?;
b.sql.push_str(") AS ");
b.write_ident(PARENT);
write_order_limit_offset(
&mut b,
&select.compound_order_by,
select.compound_limit,
select.compound_offset,
)?;
b.finish()
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ChildCount {
pub child_table: &'static str,
pub fk_column: &'static str,
pub parent_pk: &'static str,
}
fn write_child_count(b: &mut Sql<'_>, parent: &str, child: ChildCount) {
const CHILD: &str = "__rustango_child";
b.sql.push_str(", (SELECT COUNT(*) FROM ");
b.write_ident(child.child_table);
b.sql.push_str(" AS ");
b.write_ident(CHILD);
b.sql.push_str(" WHERE ");
b.write_ident(CHILD);
b.sql.push('.');
b.write_ident(child.fk_column);
b.sql.push_str(" = ");
b.write_ident(parent);
b.sql.push('.');
b.write_ident(child.parent_pk);
b.sql.push_str(") AS ");
b.write_ident("__annotated_count");
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum DateBucket {
Date(crate::query::DateKind),
DateTime(crate::query::DateTimeKind),
}
pub(crate) fn compile_date_buckets(
d: &dyn Dialect,
select: &SelectQuery,
column: &'static str,
bucket: DateBucket,
descending: bool,
) -> Result<CompiledStatement, SqlError> {
let mut b = Sql::new(d);
b.sql.push_str("SELECT DISTINCT ");
let col = d.quote_ident(column);
let pg_utc = d.name() == "postgres"
&& select.model.field_by_column(column).map(|f| f.ty)
== Some(crate::core::FieldType::DateTime);
b.sql.push_str(&match bucket {
DateBucket::Date(kind) if pg_utc => {
date_trunc_sql(kind, d.name(), &format!("({col} AT TIME ZONE 'UTC')"))
}
DateBucket::Date(kind) => date_trunc_sql(kind, d.name(), &col),
DateBucket::DateTime(kind) => datetime_trunc_sql(kind, d.name(), &col, pg_utc),
});
b.sql.push_str(" AS ");
b.write_ident("rs_bucket");
b.sql.push_str(" FROM (");
write_select(&mut b, select)?;
b.sql.push_str(") AS ");
b.write_ident("rs_bucket_sub");
b.sql.push_str(" WHERE ");
b.sql.push_str(&col);
b.sql.push_str(" IS NOT NULL");
b.sql.push_str(" ORDER BY ");
b.write_ident("rs_bucket");
b.sql.push_str(if descending { " DESC" } else { " ASC" });
b.finish()
}
fn date_trunc_sql(kind: crate::query::DateKind, dialect_name: &str, col_quoted: &str) -> String {
use crate::query::DateKind;
match (dialect_name, kind) {
("postgres", DateKind::Year) => format!("DATE_TRUNC('year', {col_quoted})::date"),
("postgres", DateKind::Month) => format!("DATE_TRUNC('month', {col_quoted})::date"),
("mysql", DateKind::Year) => format!("DATE(DATE_FORMAT({col_quoted}, '%Y-01-01'))"),
("mysql", DateKind::Month) => format!("DATE(DATE_FORMAT({col_quoted}, '%Y-%m-01'))"),
("postgres" | "mysql", DateKind::Day) => format!("DATE({col_quoted})"),
(_, DateKind::Year) => format!("date(strftime('%Y-01-01', {col_quoted}))"),
(_, DateKind::Month) => format!("date(strftime('%Y-%m-01', {col_quoted}))"),
(_, DateKind::Day) => format!("date({col_quoted})"),
}
}
fn datetime_trunc_sql(
kind: crate::query::DateTimeKind,
dialect_name: &str,
col_quoted: &str,
pg_utc: bool,
) -> String {
use crate::query::DateTimeKind as K;
if dialect_name == "postgres" {
let unit = match kind {
K::Year => "year",
K::Month => "month",
K::Day => "day",
K::Hour => "hour",
K::Minute => "minute",
K::Second => "second",
};
let zone = if pg_utc { ", 'UTC'" } else { "" };
return format!("DATE_TRUNC('{unit}', {col_quoted}{zone})");
}
let (min, sec) = if dialect_name == "mysql" {
("%i", "%s")
} else {
("%M", "%S")
};
let fmt = match kind {
K::Year => "%Y-01-01 00:00:00".to_owned(),
K::Month => "%Y-%m-01 00:00:00".to_owned(),
K::Day => "%Y-%m-%d 00:00:00".to_owned(),
K::Hour => "%Y-%m-%d %H:00:00".to_owned(),
K::Minute => format!("%Y-%m-%d %H:{min}:00"),
K::Second => format!("%Y-%m-%d %H:{min}:{sec}"),
};
if dialect_name == "mysql" {
format!("CAST(DATE_FORMAT({col_quoted}, '{fmt}') AS DATETIME)")
} else {
format!("strftime('{fmt}', {col_quoted})")
}
}