use core::fmt::Write;
use crate::SQL;
use crate::SQLParam;
use crate::dialect::Dialect;
use crate::prelude::*;
use crate::relation::{CardWrap, JunctionMeta, RelationDef};
use crate::sql::{SQLChunk, write_quoted_ident};
use super::builder::{AllColumns, PartialColumns, QueryTable};
use super::handle::RelationHandle;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RelCardinality {
Many,
One,
OptionalOne,
}
pub struct RenderedRelation<'a, V: SQLParam> {
pub table_name: &'static str,
pub column_names: Vec<&'static str>,
pub blob_columns: &'static [&'static str],
pub fk_columns: &'static [(&'static str, &'static str)],
pub cardinality: RelCardinality,
pub rel_name: &'static str,
pub where_sql: SQL<'a, V>,
pub order_by_sql: SQL<'a, V>,
pub limit: Option<SQL<'a, V>>,
pub offset: Option<SQL<'a, V>>,
pub nested: Vec<Self>,
pub junction: Option<JunctionMeta>,
}
pub trait RenderRelations<'a, V: SQLParam> {
fn render_into(self, out: &mut Vec<RenderedRelation<'a, V>>);
}
impl<'a, V: SQLParam> RenderRelations<'a, V> for () {
#[inline]
fn render_into(self, _out: &mut Vec<RenderedRelation<'a, V>>) {}
}
impl<'a, V, R, Nested, Rest, Cl> RenderRelations<'a, V>
for (RelationHandle<'a, V, R, Nested, AllColumns, Cl>, Rest)
where
V: SQLParam,
R: RelationDef,
Nested: RenderRelations<'a, V>,
Rest: RenderRelations<'a, V>,
{
fn render_into(self, out: &mut Vec<RenderedRelation<'a, V>>) {
let (handle, rest) = self;
let mut nested = Vec::new();
handle.nested.render_into(&mut nested);
out.push(RenderedRelation {
table_name: <R::Target as QueryTable>::TABLE_NAME,
column_names: <R::Target as QueryTable>::COLUMN_NAMES.to_vec(),
blob_columns: <R::Target as QueryTable>::BLOB_COLUMNS,
fk_columns: R::fk_columns(),
cardinality: <R::Card as CardWrap>::CARDINALITY,
rel_name: R::NAME,
where_sql: handle.where_sql,
order_by_sql: handle.order_by_sql,
limit: handle.limit,
offset: handle.offset,
nested,
junction: R::junction(),
});
rest.render_into(out);
}
}
impl<'a, V, R, Nested, Rest, Cl> RenderRelations<'a, V>
for (RelationHandle<'a, V, R, Nested, PartialColumns, Cl>, Rest)
where
V: SQLParam,
R: RelationDef,
Nested: RenderRelations<'a, V>,
Rest: RenderRelations<'a, V>,
{
fn render_into(self, out: &mut Vec<RenderedRelation<'a, V>>) {
let (handle, rest) = self;
let mut nested = Vec::new();
handle.nested.render_into(&mut nested);
out.push(RenderedRelation {
table_name: <R::Target as QueryTable>::TABLE_NAME,
column_names: handle.cols.columns,
blob_columns: <R::Target as QueryTable>::BLOB_COLUMNS,
fk_columns: R::fk_columns(),
cardinality: <R::Card as CardWrap>::CARDINALITY,
rel_name: R::NAME,
where_sql: handle.where_sql,
order_by_sql: handle.order_by_sql,
limit: handle.limit,
offset: handle.offset,
nested,
junction: R::junction(),
});
rest.render_into(out);
}
}
#[allow(clippy::too_many_arguments)]
pub fn build_query_sql<'a, V: SQLParam>(
table_name: &str,
column_names: &[&str],
blob_columns: &[&str],
relations: Vec<RenderedRelation<'a, V>>,
where_sql: SQL<'a, V>,
order_by_sql: SQL<'a, V>,
limit: Option<SQL<'a, V>>,
offset: Option<SQL<'a, V>>,
wrap_base_json: bool,
) -> SQL<'a, V> {
let mut sql = QuerySql::new();
let alias = "t0";
let dialect = V::DIALECT;
sql.push_str("SELECT ");
if wrap_base_json {
write_json_object_open(dialect, sql.buf_mut());
for (i, c) in column_names.iter().enumerate() {
if i > 0 {
sql.push_str(", ");
}
sql.push('\'');
sql.push_str(c);
sql.push_str("', ");
write_json_column(alias, c, blob_columns, dialect, sql.buf_mut());
}
sql.push(')');
if dialect == Dialect::PostgreSQL {
sql.push_str("::text");
}
sql.push_str(" AS \"__base\"");
} else {
for (i, c) in column_names.iter().enumerate() {
if i > 0 {
sql.push_str(", ");
}
write_qualified_column(alias, c, sql.buf_mut());
}
}
let mut alias_counter = 1usize;
for rel in relations {
let rel_name = rel.rel_name;
sql.push_str(", ");
write_relation_subquery::<V>(rel, alias, &mut alias_counter, &mut sql);
if dialect == Dialect::PostgreSQL {
sql.push_str("::text");
}
sql.push_str(" AS \"__rel_");
sql.push_str(rel_name);
sql.push('"');
}
sql.push_str(" FROM \"");
sql.push_str(table_name);
sql.push_str("\" AS \"");
sql.push_str(alias);
sql.push('"');
if !where_sql.chunks.is_empty() {
sql.push_str(" WHERE ");
sql.push_fragment(where_sql, table_name, alias);
}
if !order_by_sql.chunks.is_empty() {
sql.push_str(" ORDER BY ");
sql.push_fragment(order_by_sql, table_name, alias);
}
if let Some(limit_sql) = limit {
sql.push_str(" LIMIT ");
sql.push_fragment(limit_sql, table_name, alias);
}
if let Some(offset_sql) = offset {
sql.push_str(" OFFSET ");
sql.push_fragment(offset_sql, table_name, alias);
}
sql.finish()
}
struct QuerySql<'a, V: SQLParam> {
sql: SQL<'a, V>,
buf: String,
}
impl<'a, V: SQLParam> QuerySql<'a, V> {
fn new() -> Self {
Self {
sql: SQL::empty(),
buf: String::with_capacity(256),
}
}
fn buf_mut(&mut self) -> &mut String {
&mut self.buf
}
fn push(&mut self, ch: char) {
self.buf.push(ch);
}
fn push_str(&mut self, text: &str) {
self.buf.push_str(text);
}
fn push_fragment(&mut self, fragment: SQL<'a, V>, target_table: &str, alias: &str) {
for chunk in fragment.chunks {
match chunk {
SQLChunk::Column(column) if column.table == target_table => {
write_quoted_ident(&mut self.buf, alias);
self.buf.push('.');
write_quoted_ident(&mut self.buf, column.name);
}
SQLChunk::Table(table) if table.name == target_table => {
write_quoted_ident(&mut self.buf, alias);
}
other => {
self.flush();
self.sql.push_mut(other);
}
}
}
}
fn flush(&mut self) {
if !self.buf.is_empty() {
self.sql
.push_mut(SQLChunk::Raw(Cow::Owned(core::mem::take(&mut self.buf))));
}
}
fn finish(mut self) -> SQL<'a, V> {
self.flush();
self.sql
}
}
fn write_inner_subquery_prelude(
target_table: &str,
alias: &str,
target_columns: &[&'static str],
extra_cols: &[&str],
dialect: Dialect,
sql: &mut String,
) {
if dialect == Dialect::PostgreSQL {
sql.push_str("LATERAL ");
}
sql.push_str("(SELECT ");
for (i, c) in target_columns.iter().enumerate() {
if i > 0 {
sql.push_str(", ");
}
write_qualified_column(alias, c, sql);
}
for c in extra_cols {
sql.push_str(", ");
write_qualified_column(alias, c, sql);
}
sql.push_str(" FROM \"");
sql.push_str(target_table);
}
fn collect_nested_extra_cols<V: SQLParam>(
nested: &[RenderedRelation<'_, V>],
target_columns: &[&'static str],
) -> Vec<&'static str> {
let mut extra_cols = Vec::new();
for nested_rel in nested {
if let Some(junction) = &nested_rel.junction {
for (_, src_col) in junction.source_fk {
if !target_columns.contains(src_col) && !extra_cols.contains(src_col) {
extra_cols.push(*src_col);
}
}
} else {
for (_, tgt_col) in nested_rel.fk_columns {
if !target_columns.contains(tgt_col) && !extra_cols.contains(tgt_col) {
extra_cols.push(*tgt_col);
}
}
}
}
extra_cols
}
fn write_json_object_body<'a, V: SQLParam>(
blob_columns: &[&str],
nested: Vec<RenderedRelation<'a, V>>,
alias: &str,
target_columns: &[&'static str],
dialect: Dialect,
ctx: &mut SubqueryCtx<'_, 'a, V>,
) {
write_json_object_open(dialect, ctx.sql.buf_mut());
let mut first_arg = true;
for c in target_columns {
if !first_arg {
ctx.sql.push_str(", ");
}
first_arg = false;
ctx.sql.push('\'');
ctx.sql.push_str(c);
ctx.sql.push_str("', ");
write_json_column(alias, c, blob_columns, dialect, ctx.sql.buf_mut());
}
for nested_rel in nested {
if !first_arg {
ctx.sql.push_str(", ");
}
first_arg = false;
ctx.sql.push('\'');
ctx.sql.push_str(nested_rel.rel_name);
ctx.sql.push_str("', ");
write_relation_subquery::<V>(nested_rel, alias, ctx.alias_counter, ctx.sql);
}
ctx.sql.push(')'); }
fn alloc_alias(counter: &mut usize) -> String {
let num = *counter;
*counter += 1;
let mut buf = String::with_capacity(4);
buf.push('t');
let _ = write!(buf, "{num}");
buf
}
struct SubqueryCtx<'s, 'a, V: SQLParam> {
alias_counter: &'s mut usize,
sql: &'s mut QuerySql<'a, V>,
}
struct RelationClauseSql<'a, V: SQLParam> {
where_sql: SQL<'a, V>,
order_by_sql: Option<SQL<'a, V>>,
limit: Option<SQL<'a, V>>,
offset: Option<SQL<'a, V>>,
}
fn write_where_order_limit_offset<'a, V: SQLParam>(
target_table: &str,
alias: &str,
pg_order_in_agg: bool,
cardinality: RelCardinality,
clauses: RelationClauseSql<'a, V>,
ctx: &mut SubqueryCtx<'_, 'a, V>,
) {
let RelationClauseSql {
where_sql,
order_by_sql,
limit,
offset,
} = clauses;
if !where_sql.chunks.is_empty() {
ctx.sql.push_str(" AND ");
ctx.sql.push_fragment(where_sql, target_table, alias);
}
if !pg_order_in_agg
&& let Some(order_by_sql) = order_by_sql
&& !order_by_sql.chunks.is_empty()
{
ctx.sql.push_str(" ORDER BY ");
ctx.sql.push_fragment(order_by_sql, target_table, alias);
}
match cardinality {
RelCardinality::One | RelCardinality::OptionalOne => {
ctx.sql.push_str(" LIMIT 1");
}
RelCardinality::Many => {
if let Some(limit_sql) = limit {
ctx.sql.push_str(" LIMIT ");
ctx.sql.push_fragment(limit_sql, target_table, alias);
}
}
}
if let Some(offset_sql) = offset {
ctx.sql.push_str(" OFFSET ");
ctx.sql.push_fragment(offset_sql, target_table, alias);
}
}
fn write_fk_join_conditions(
junction: Option<&JunctionMeta>,
alias: &str,
parent_alias: &str,
junction_alias: Option<&str>,
fk_columns: &[(&str, &str)],
sql: &mut String,
) {
let push_pair = |a: &str, b: &str, ca: &str, cb: &str, sql: &mut String| {
sql.push('"');
sql.push_str(a);
sql.push_str("\".\"");
sql.push_str(ca);
sql.push_str("\" = \"");
sql.push_str(b);
sql.push_str("\".\"");
sql.push_str(cb);
sql.push('"');
};
if let (Some(junction), Some(junc_alias)) = (junction, junction_alias) {
for (i, (junc_col, src_col)) in junction.source_fk.iter().enumerate() {
if i > 0 {
sql.push_str(" AND ");
}
push_pair(junc_alias, parent_alias, junc_col, src_col, sql);
}
} else {
for (i, (src_col, tgt_col)) in fk_columns.iter().enumerate() {
if i > 0 {
sql.push_str(" AND ");
}
push_pair(alias, parent_alias, src_col, tgt_col, sql);
}
}
}
fn write_relation_subquery<'a, V: SQLParam>(
rel: RenderedRelation<'a, V>,
parent_alias: &str,
alias_counter: &mut usize,
sql: &mut QuerySql<'a, V>,
) {
let RenderedRelation {
table_name: target_table,
column_names: target_columns,
blob_columns,
fk_columns,
cardinality,
where_sql,
order_by_sql,
nested,
junction,
limit,
offset,
..
} = rel;
let alias_buf = alloc_alias(alias_counter);
let alias = &alias_buf;
let junction_alias = junction.as_ref().map(|_| alloc_alias(alias_counter));
let dialect = V::DIALECT;
let has_order_by = !order_by_sql.chunks.is_empty();
let extra_cols = collect_nested_extra_cols(&nested, &target_columns);
let pg_order_in_agg = cardinality == RelCardinality::Many
&& dialect == Dialect::PostgreSQL
&& has_order_by
&& limit.is_none()
&& offset.is_none();
let needs_inner_subquery = cardinality == RelCardinality::Many
&& (limit.is_some() || offset.is_some() || (!pg_order_in_agg && has_order_by));
let mut order_by_sql = Some(order_by_sql);
sql.push_str("(SELECT ");
if cardinality == RelCardinality::Many {
write_json_array_agg_open(dialect, sql.buf_mut());
}
write_json_object_body::<V>(
blob_columns,
nested,
alias,
&target_columns,
dialect,
&mut SubqueryCtx { alias_counter, sql },
);
if pg_order_in_agg {
sql.push_str(" ORDER BY ");
if let Some(order_by_sql) = order_by_sql.take() {
sql.push_fragment(order_by_sql, target_table, alias);
}
}
if cardinality == RelCardinality::Many {
write_json_array_agg_close(dialect, sql.buf_mut());
}
sql.push_str(" FROM ");
if needs_inner_subquery {
write_inner_subquery_prelude(
target_table,
alias,
&target_columns,
&extra_cols,
dialect,
sql.buf_mut(),
);
} else {
sql.push('"');
sql.push_str(target_table);
}
sql.push_str("\" AS \"");
sql.push_str(alias);
sql.push('"');
if let (Some(junction), Some(junc_alias)) = (&junction, &junction_alias) {
write_junction_join(junction, alias, junc_alias, sql.buf_mut());
}
sql.push_str(" WHERE ");
write_fk_join_conditions(
junction.as_ref(),
alias,
parent_alias,
junction_alias.as_deref(),
fk_columns,
sql.buf_mut(),
);
write_where_order_limit_offset(
target_table,
alias,
pg_order_in_agg,
cardinality,
RelationClauseSql {
where_sql,
order_by_sql,
limit,
offset,
},
&mut SubqueryCtx { alias_counter, sql },
);
if needs_inner_subquery {
sql.push_str(") AS \"");
sql.push_str(alias);
sql.push('"');
}
sql.push(')'); }
fn write_qualified_column(alias: &str, column: &str, sql: &mut String) {
sql.push('"');
sql.push_str(alias);
sql.push_str("\".\"");
sql.push_str(column);
sql.push('"');
}
fn write_junction_join(
junction: &JunctionMeta,
target_alias: &str,
junc_alias: &str,
sql: &mut String,
) {
sql.push_str(" INNER JOIN \"");
sql.push_str(junction.table_name);
sql.push_str("\" AS \"");
sql.push_str(junc_alias);
sql.push_str("\" ON ");
for (i, (junc_col, target_col)) in junction.target_fk.iter().enumerate() {
if i > 0 {
sql.push_str(" AND ");
}
sql.push('"');
sql.push_str(junc_alias);
sql.push_str("\".\"");
sql.push_str(junc_col);
sql.push_str("\" = \"");
sql.push_str(target_alias);
sql.push_str("\".\"");
sql.push_str(target_col);
sql.push('"');
}
}
fn write_json_column(
alias: &str,
column: &str,
blob_columns: &[&str],
dialect: Dialect,
sql: &mut String,
) {
let is_blob = dialect == Dialect::SQLite && blob_columns.contains(&column);
if is_blob {
sql.push_str("CASE WHEN ");
write_qualified_column(alias, column, sql);
sql.push_str(" IS NULL THEN NULL ELSE hex(");
write_qualified_column(alias, column, sql);
sql.push_str(") END");
} else {
write_qualified_column(alias, column, sql);
}
}
fn write_json_object_open(dialect: Dialect, sql: &mut String) {
match dialect {
Dialect::SQLite | Dialect::MySQL => sql.push_str("json_object("),
Dialect::PostgreSQL => sql.push_str("json_build_object("),
}
}
fn write_json_array_agg_open(dialect: Dialect, sql: &mut String) {
match dialect {
Dialect::SQLite | Dialect::MySQL => sql.push_str("json_group_array("),
Dialect::PostgreSQL => sql.push_str("COALESCE(json_agg("),
}
}
fn write_json_array_agg_close(dialect: Dialect, sql: &mut String) {
match dialect {
Dialect::SQLite | Dialect::MySQL => sql.push(')'),
Dialect::PostgreSQL => sql.push_str("), '[]'::json)"),
}
}