use std::marker::PhantomData;
use sqlx::{
Executor,
FromRow,
Postgres,
};
use sqlxo_traits::{
JoinNavigationModel,
QueryContext,
};
use crate::{
and,
blocks::{
BuildableFilter,
DeleteHead,
Expression,
SqlWriter,
},
select::{
self,
SelectionList,
},
Buildable,
Deletable,
ExecutablePlan,
FetchablePlan,
Planable,
Result,
};
#[allow(dead_code)]
pub trait BuildableDeleteQuery<C, Row = <C as QueryContext>::Model>:
Buildable<C, Row = Row, Plan: Planable<C, Row>> + BuildableFilter<C>
where
C: QueryContext,
Row: Send + Sync + Unpin + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>,
{
}
pub struct DeleteQueryPlan<
'a,
C: QueryContext,
Row = <C as QueryContext>::Model,
> {
pub(crate) where_expr: Option<Expression<C::Query>>,
pub(crate) table: &'a str,
pub(crate) is_soft: bool,
pub(crate) delete_marker_field: Option<&'a str>,
pub(crate) auto_joins: bool,
pub(crate) include_lazy_relations: bool,
pub(crate) selection: Option<SelectionList<Row, select::SelectionColumn>>,
row: PhantomData<Row>,
}
impl<'a, C, Row> DeleteQueryPlan<'a, C, Row>
where
C: QueryContext,
{
fn to_query_builder(&self) -> sqlx::QueryBuilder<'static, Postgres> {
let head =
DeleteHead::new(self.table, self.is_soft, self.delete_marker_field);
let mut w = SqlWriter::new(head);
if let Some(e) = &self.where_expr {
w.push_where(e);
}
w.into_builder()
}
fn push_returning(&self, qb: &mut sqlx::QueryBuilder<'static, Postgres>) {
select::push_returning(qb, self.table, self.selection.as_ref());
}
#[cfg(any(test, feature = "test-utils"))]
pub fn sql(&self) -> String {
use sqlx::Execute;
self.to_query_builder().build().sql().to_string()
}
}
#[async_trait::async_trait]
impl<'a, C, Row> ExecutablePlan<C> for DeleteQueryPlan<'a, C, Row>
where
C: QueryContext,
C::Model: crate::Deletable,
Row: Send + Sync + Unpin + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>,
{
async fn execute<'e, E>(&self, exec: E) -> Result<u64>
where
E: Executor<'e, Database = Postgres>,
{
let rows = self
.to_query_builder()
.build()
.execute(exec)
.await?
.rows_affected();
Ok(rows)
}
}
#[async_trait::async_trait]
impl<'a, C, Row> FetchablePlan<C, Row> for DeleteQueryPlan<'a, C, Row>
where
C: QueryContext,
C::Model: crate::Deletable,
Row: Send
+ Sync
+ Unpin
+ for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>
+ DeleteFetchRow<C>,
{
async fn fetch_one<'e, E>(&self, exec: E) -> Result<Row>
where
E: Executor<'e, Database = Postgres>,
{
Ok(<Row as DeleteFetchRow<C>>::fetch_one(self, exec).await?)
}
async fn fetch_all<'e, E>(&self, exec: E) -> Result<Vec<Row>>
where
E: Executor<'e, Database = Postgres>,
{
Ok(<Row as DeleteFetchRow<C>>::fetch_all(self, exec).await?)
}
async fn fetch_optional<'e, E>(&self, exec: E) -> Result<Option<Row>>
where
E: Executor<'e, Database = Postgres>,
{
Ok(<Row as DeleteFetchRow<C>>::fetch_optional(self, exec).await?)
}
}
#[async_trait::async_trait]
trait DeleteFetchRow<C: QueryContext>: Sized
where
C::Model: Deletable,
{
async fn fetch_one<'a, 'e, E>(
plan: &DeleteQueryPlan<'a, C, Self>,
exec: E,
) -> Result<Self>
where
E: Executor<'e, Database = Postgres>;
async fn fetch_all<'a, 'e, E>(
plan: &DeleteQueryPlan<'a, C, Self>,
exec: E,
) -> Result<Vec<Self>>
where
E: Executor<'e, Database = Postgres>;
async fn fetch_optional<'a, 'e, E>(
plan: &DeleteQueryPlan<'a, C, Self>,
exec: E,
) -> Result<Option<Self>>
where
E: Executor<'e, Database = Postgres>;
}
struct QbWrite<'a> {
qb: &'a mut sqlx::QueryBuilder<'static, Postgres>,
}
impl sqlxo_traits::SqlWrite for QbWrite<'_> {
fn push(&mut self, s: &str) {
self.qb.push(s);
}
fn bind<T>(&mut self, value: T)
where
T: sqlx::Encode<'static, Postgres> + Send + 'static,
T: sqlx::Type<Postgres>,
{
self.qb.push_bind(value);
}
}
fn push_join_path_inline(
qb: &mut sqlx::QueryBuilder<'static, Postgres>,
path: &sqlxo_traits::JoinPath,
base_table: &str,
) {
if path.is_empty() {
return;
}
let mut left_alias = base_table.to_string();
let mut alias_prefix = String::new();
for segment in path.segments() {
let join_word = match segment.kind {
sqlxo_traits::JoinKind::Inner => " INNER JOIN ",
sqlxo_traits::JoinKind::Left => " LEFT JOIN ",
};
if let Some(through) = segment.descriptor.through {
let mut through_alias = alias_prefix.clone();
through_alias.push_str(through.alias_segment);
let clause = format!(
r#"{join}{table} AS "{alias}" ON "{left}"."{left_field}" = "{alias}"."{right_field}""#,
join = join_word,
table = through.table,
alias = &through_alias,
left = &left_alias,
left_field = through.left_field,
right_field = through.right_field,
);
qb.push(clause);
left_alias = through_alias;
}
alias_prefix.push_str(segment.descriptor.alias_segment);
let right_alias = alias_prefix.clone();
let clause = format!(
r#"{join}{table} AS "{alias}" ON "{left}"."{left_field}" = "{alias}"."{right_field}""#,
join = join_word,
table = segment.descriptor.right_table,
alias = &right_alias,
left = &left_alias,
left_field = segment.descriptor.left_field,
right_field = segment.descriptor.right_field,
);
qb.push(clause);
left_alias = right_alias;
}
}
impl<'a, C> DeleteQueryPlan<'a, C, C::Model>
where
C: QueryContext,
C::Model: Deletable + JoinNavigationModel,
{
fn auto_join_paths(&self) -> Vec<sqlxo_traits::JoinPath> {
if !self.auto_joins {
return Vec::new();
}
C::Model::default_join_paths(self.include_lazy_relations).into_vec()
}
fn to_graph_query_builder(
&self,
joins: &[sqlxo_traits::JoinPath],
) -> sqlx::QueryBuilder<'static, Postgres> {
let mut qb = sqlx::QueryBuilder::<Postgres>::new("WITH affected AS (");
if self.is_soft {
qb.push("UPDATE ");
qb.push(self.table);
qb.push(" SET ");
let marker = self.delete_marker_field.unwrap_or_else(|| {
panic!("soft delete marker field missing for {}", self.table)
});
qb.push(marker);
qb.push(" = NOW()");
} else {
qb.push("DELETE FROM ");
qb.push(self.table);
}
if let Some(expr) = &self.where_expr {
qb.push(" WHERE ");
expr.write(&mut QbWrite { qb: &mut qb });
}
qb.push(" RETURNING *) SELECT \"affected\".*");
for col in C::Model::collect_join_columns(Some(joins), "") {
qb.push(", ");
qb.push(format!(
r#""{}"."{}" AS "{}""#,
col.table_alias, col.column, col.alias
));
}
qb.push(" FROM affected");
for path in joins {
push_join_path_inline(&mut qb, path, "affected");
}
qb
}
fn hydrate_graph_rows(
&self,
rows: Vec<sqlx::postgres::PgRow>,
joins: &[sqlxo_traits::JoinPath],
) -> Result<Vec<C::Model>> {
let joins_ref = if joins.is_empty() { None } else { Some(joins) };
let mut models = Vec::with_capacity(rows.len());
for row in rows {
let mut model = C::Model::from_row(&row)?;
model.hydrate_navigations(joins_ref, &row, "")?;
models.push(model);
}
if C::Model::has_collection_joins(joins_ref) {
return Ok(C::Model::merge_collection_rows(models, joins_ref));
}
Ok(models)
}
}
#[async_trait::async_trait]
impl<C, Row> DeleteFetchRow<C> for Row
where
C: QueryContext,
C::Model: Deletable,
Row: Send + Sync + Unpin + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>,
{
default async fn fetch_one<'a, 'e, E>(
plan: &DeleteQueryPlan<'a, C, Self>,
exec: E,
) -> Result<Self>
where
E: Executor<'e, Database = Postgres>,
{
let mut qb = plan.to_query_builder();
plan.push_returning(&mut qb);
Ok(qb.build_query_as::<Self>().fetch_one(exec).await?)
}
default async fn fetch_all<'a, 'e, E>(
plan: &DeleteQueryPlan<'a, C, Self>,
exec: E,
) -> Result<Vec<Self>>
where
E: Executor<'e, Database = Postgres>,
{
let mut qb = plan.to_query_builder();
plan.push_returning(&mut qb);
Ok(qb.build_query_as::<Self>().fetch_all(exec).await?)
}
default async fn fetch_optional<'a, 'e, E>(
plan: &DeleteQueryPlan<'a, C, Self>,
exec: E,
) -> Result<Option<Self>>
where
E: Executor<'e, Database = Postgres>,
{
let mut qb = plan.to_query_builder();
plan.push_returning(&mut qb);
Ok(qb.build_query_as::<Self>().fetch_optional(exec).await?)
}
}
#[async_trait::async_trait]
impl<C> DeleteFetchRow<C> for C::Model
where
C: QueryContext,
C::Model: Deletable + JoinNavigationModel + Clone,
{
async fn fetch_one<'a, 'e, E>(
plan: &DeleteQueryPlan<'a, C, Self>,
exec: E,
) -> Result<Self>
where
E: Executor<'e, Database = Postgres>,
{
if !plan.auto_joins || plan.selection.is_some() {
let mut qb = plan.to_query_builder();
plan.push_returning(&mut qb);
return Ok(qb.build_query_as::<Self>().fetch_one(exec).await?);
}
let joins = plan.auto_join_paths();
let rows = plan
.to_graph_query_builder(&joins)
.build()
.fetch_all(exec)
.await?;
let models = plan.hydrate_graph_rows(rows, &joins)?;
models
.into_iter()
.next()
.ok_or_else(|| sqlx::Error::RowNotFound.into())
}
async fn fetch_all<'a, 'e, E>(
plan: &DeleteQueryPlan<'a, C, Self>,
exec: E,
) -> Result<Vec<Self>>
where
E: Executor<'e, Database = Postgres>,
{
if !plan.auto_joins || plan.selection.is_some() {
let mut qb = plan.to_query_builder();
plan.push_returning(&mut qb);
return Ok(qb.build_query_as::<Self>().fetch_all(exec).await?);
}
let joins = plan.auto_join_paths();
let rows = plan
.to_graph_query_builder(&joins)
.build()
.fetch_all(exec)
.await?;
plan.hydrate_graph_rows(rows, &joins)
}
async fn fetch_optional<'a, 'e, E>(
plan: &DeleteQueryPlan<'a, C, Self>,
exec: E,
) -> Result<Option<Self>>
where
E: Executor<'e, Database = Postgres>,
{
if !plan.auto_joins || plan.selection.is_some() {
let mut qb = plan.to_query_builder();
plan.push_returning(&mut qb);
return Ok(qb
.build_query_as::<Self>()
.fetch_optional(exec)
.await?);
}
let joins = plan.auto_join_paths();
let rows = plan
.to_graph_query_builder(&joins)
.build()
.fetch_all(exec)
.await?;
let models = plan.hydrate_graph_rows(rows, &joins)?;
Ok(models.into_iter().next())
}
}
impl<'a, C, Row> Planable<C, Row> for DeleteQueryPlan<'a, C, Row>
where
C: QueryContext,
C::Model: crate::Deletable,
Row: Send + Sync + Unpin + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>,
{
}
pub struct DeleteQueryBuilder<
'a,
C: QueryContext,
Row = <C as QueryContext>::Model,
> {
pub(crate) table: &'a str,
pub(crate) where_expr: Option<Expression<C::Query>>,
pub(crate) is_soft: bool,
pub(crate) delete_marker_field: Option<&'a str>,
pub(crate) auto_joins: bool,
pub(crate) include_lazy_relations: bool,
pub(crate) selection: Option<SelectionList<Row, select::SelectionColumn>>,
row: PhantomData<Row>,
}
impl<'a, C, Row> DeleteQueryBuilder<'a, C, Row>
where
C: QueryContext,
{
pub fn new_soft(table: &'a str, delete_marker_field: &'a str) -> Self {
Self {
table,
where_expr: None,
is_soft: true,
delete_marker_field: Some(delete_marker_field),
auto_joins: true,
include_lazy_relations: false,
selection: None,
row: PhantomData,
}
}
pub fn new_hard(table: &'a str) -> Self {
Self {
table,
where_expr: None,
is_soft: false,
delete_marker_field: None,
auto_joins: true,
include_lazy_relations: false,
selection: None,
row: PhantomData,
}
}
pub fn without_auto_joins(mut self) -> Self {
self.auto_joins = false;
self
}
pub fn include_lazy_relations(mut self) -> Self {
self.include_lazy_relations = true;
self
}
}
impl<'a, C, Row> Buildable<C> for DeleteQueryBuilder<'a, C, Row>
where
C: QueryContext,
C::Model: crate::Deletable,
Row: Send + Sync + Unpin + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>,
{
type Row = Row;
type Plan = DeleteQueryPlan<'a, C, Row>;
fn from_ctx() -> Self {
Self {
table: C::TABLE,
where_expr: None,
is_soft:
<C::Model as crate::Deletable>::IS_SOFT_DELETE,
delete_marker_field:
<C::Model as crate::Deletable>::DELETE_MARKER_FIELD,
auto_joins: true,
include_lazy_relations: false,
selection: None,
row: PhantomData,
}
}
fn build(self) -> Self::Plan {
DeleteQueryPlan {
where_expr: self.where_expr,
table: self.table,
is_soft: self.is_soft,
delete_marker_field: self.delete_marker_field,
auto_joins: self.auto_joins,
include_lazy_relations: self.include_lazy_relations,
selection: self.selection,
row: PhantomData,
}
}
}
impl<'a, C, Row> DeleteQueryBuilder<'a, C, Row>
where
C: QueryContext,
C::Model: crate::Deletable,
Row: Send + Sync + Unpin + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>,
{
pub fn take<NewRow>(
self,
selection: SelectionList<NewRow, select::SelectionEntry>,
) -> DeleteQueryBuilder<'a, C, NewRow>
where
NewRow: Send
+ Sync
+ Unpin
+ for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>,
{
DeleteQueryBuilder {
table: self.table,
where_expr: self.where_expr,
is_soft: self.is_soft,
delete_marker_field: self.delete_marker_field,
auto_joins: self.auto_joins,
include_lazy_relations: self.include_lazy_relations,
selection: Some(selection.expect_columns()),
row: PhantomData,
}
}
}
impl<'a, C, Row> BuildableFilter<C> for DeleteQueryBuilder<'a, C, Row>
where
C: QueryContext,
C::Model: crate::Deletable,
{
fn r#where(mut self, e: Expression<<C as QueryContext>::Query>) -> Self {
match self.where_expr {
Some(existing) => self.where_expr = Some(and![existing, e]),
None => self.where_expr = Some(e),
};
self
}
}
impl<'a, C, Row> BuildableDeleteQuery<C, Row> for DeleteQueryBuilder<'a, C, Row>
where
C: QueryContext,
C::Model: crate::Deletable,
Row: Send + Sync + Unpin + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>,
{
}