cratestack_sqlx/query/batch/
delete.rs1use std::collections::HashMap;
6use std::hash::Hash;
7
8use cratestack_core::{AuditOperation, BatchResponse, CoolContext, CoolError, ModelEventKind};
9
10use crate::audit::{
11 build_audit_event, dispatch_audit_sink, enqueue_audit_event, ensure_audit_table,
12};
13use crate::descriptor::{enqueue_event_outbox, ensure_event_outbox_table};
14use crate::query::support::push_action_policy_query;
15use crate::{ModelDescriptor, ModelPrimaryKey, SqlxRuntime, cool_error_from_sqlx, sqlx};
16
17use super::validate::{reject_duplicate_pks, validate_batch_size};
18
19#[derive(Debug, Clone)]
20pub struct BatchDelete<'a, M: 'static, PK: 'static> {
21 pub(crate) runtime: &'a SqlxRuntime,
22 pub(crate) descriptor: &'static ModelDescriptor<M, PK>,
23 pub(crate) ids: Vec<PK>,
24}
25
26impl<'a, M: 'static, PK: 'static> BatchDelete<'a, M, PK> {
27 pub async fn run(self, ctx: &CoolContext) -> Result<BatchResponse<M>, CoolError>
28 where
29 for<'r> M: Send
30 + Unpin
31 + sqlx::FromRow<'r, sqlx::postgres::PgRow>
32 + ModelPrimaryKey<PK>
33 + serde::Serialize,
34 PK: Clone
35 + Eq
36 + Hash
37 + Send
38 + sqlx::Type<sqlx::Postgres>
39 + for<'q> sqlx::Encode<'q, sqlx::Postgres>,
40 {
41 validate_batch_size(self.ids.len())?;
42 reject_duplicate_pks(&self.ids)?;
43 if self.ids.is_empty() {
44 return Ok(BatchResponse::from_results(vec![]));
45 }
46
47 let emits_event = self.descriptor.emits(ModelEventKind::Deleted);
48 let audit_enabled = self.descriptor.audit_enabled;
49
50 let mut tx = self
51 .runtime
52 .pool()
53 .begin()
54 .await
55 .map_err(cool_error_from_sqlx)?;
56 if emits_event {
57 ensure_event_outbox_table(&mut *tx).await?;
58 }
59 if audit_enabled {
60 ensure_audit_table(self.runtime).await?;
61 }
62
63 let mut query = sqlx::QueryBuilder::<sqlx::Postgres>::new("");
64 match self.descriptor.soft_delete_column {
65 Some(col) => {
66 query.push("UPDATE ").push(self.descriptor.table_name);
67 query.push(" SET ").push(col).push(" = NOW()");
68 if let Some(version_col) = self.descriptor.version_column {
69 query
70 .push(", ")
71 .push(version_col)
72 .push(" = ")
73 .push(version_col)
74 .push(" + 1");
75 }
76 query.push(" WHERE ").push(col).push(" IS NULL AND ");
77 }
78 None => {
79 query.push("DELETE FROM ").push(self.descriptor.table_name);
80 query.push(" WHERE ");
81 }
82 }
83 query.push(self.descriptor.primary_key).push(" IN (");
84 for (index, id) in self.ids.iter().enumerate() {
85 if index > 0 {
86 query.push(", ");
87 }
88 query.push_bind(id.clone());
89 }
90 query.push(") AND ");
91 push_action_policy_query(
92 &mut query,
93 self.descriptor.delete_allow_policies,
94 self.descriptor.delete_deny_policies,
95 ctx,
96 );
97 query
98 .push(" RETURNING ")
99 .push(self.descriptor.select_projection());
100
101 let deleted: Vec<M> = query
102 .build_query_as::<M>()
103 .fetch_all(&mut *tx)
104 .await
105 .map_err(cool_error_from_sqlx)?;
106
107 let mut audit_events = Vec::new();
110 for record in &deleted {
111 if emits_event {
112 enqueue_event_outbox(
113 &mut *tx,
114 self.descriptor.schema_name,
115 ModelEventKind::Deleted,
116 record,
117 )
118 .await?;
119 }
120 if audit_enabled {
121 let before = serde_json::to_value(record).ok();
122 let event =
123 build_audit_event(self.descriptor, AuditOperation::Delete, before, None, ctx);
124 enqueue_audit_event(&mut *tx, &event).await?;
125 audit_events.push(event);
126 }
127 }
128
129 tx.commit().await.map_err(cool_error_from_sqlx)?;
130
131 if emits_event {
132 let _ = self.runtime.drain_event_outbox().await;
133 }
134 dispatch_audit_sink(self.runtime, &audit_events).await;
135
136 let mut by_pk: HashMap<PK, M> = deleted.into_iter().map(|m| (m.primary_key(), m)).collect();
140 let per_item: Vec<Result<M, CoolError>> = self
141 .ids
142 .into_iter()
143 .map(|id| {
144 by_pk
145 .remove(&id)
146 .ok_or_else(|| CoolError::NotFound("no row matched".to_owned()))
147 })
148 .collect();
149
150 Ok(BatchResponse::from_results(per_item))
151 }
152}