dovecote_sqlx_postgres/scope.rs
1//! Explicit tenant-scoped and administrative `PostgreSQL` handles.
2
3use dovecote::{
4 ClaimedEvent, EnqueueOutcome, FinalizeOutcome, ImportOutcome, ImportedDeliveryState, NewEvent,
5 TenantId,
6};
7use sqlx::{Postgres, Transaction};
8use time::OffsetDateTime;
9
10use crate::{
11 ClaimError, EnqueueError, FinalizeError, ImportError, MutationError, PageError, SnapshotPager,
12 enqueue, finalize, import, lifecycle, page, rls,
13};
14
15/// `PostgreSQL` Dovecote operations restricted to one validated tenant.
16#[derive(Clone)]
17pub struct TenantDovecote {
18 pool: sqlx::PgPool,
19 tenant_id: TenantId,
20}
21
22impl TenantDovecote {
23 pub(crate) const fn new(pool: sqlx::PgPool, tenant_id: TenantId) -> Self {
24 Self { pool, tenant_id }
25 }
26
27 /// Returns this handle's validated tenant identifier.
28 #[must_use]
29 pub const fn tenant_id(&self) -> &TenantId {
30 &self.tenant_id
31 }
32
33 /// Borrows the pool used by this handle.
34 #[must_use]
35 pub const fn pool(&self) -> &sqlx::PgPool {
36 &self.pool
37 }
38
39 /// Binds this tenant to a transaction for the optional `PostgreSQL` RLS
40 /// profile. Adapter predicates remain active even without RLS.
41 ///
42 /// # Errors
43 /// Returns the database error if binding transaction-local tenant context fails.
44 /// The caller must roll back the transaction before retrying.
45 pub async fn bind_tenant<'c>(
46 &self,
47 transaction: &mut Transaction<'c, Postgres>,
48 ) -> Result<(), sqlx::Error> {
49 rls::bind_tenant(transaction, &self.tenant_id).await
50 }
51
52 /// Enqueues an event for this tenant in the caller-owned transaction.
53 ///
54 /// # Errors
55 /// Returns an identity conflict for different immutable content, a schema or
56 /// backend incompatibility, invalid stored data, or a database error. The caller
57 /// must roll back its transaction on failure; this method never commits it.
58 pub async fn enqueue<'c>(
59 &self,
60 transaction: &mut Transaction<'c, Postgres>,
61 event: NewEvent,
62 ) -> Result<EnqueueOutcome, EnqueueError> {
63 self.bind_tenant(transaction)
64 .await
65 .map_err(|source| EnqueueError::sql("bind tenant", source))?;
66 enqueue::enqueue_for_scope(transaction, &self.tenant_id, event).await
67 }
68
69 /// Imports one event and legacy state for this tenant.
70 ///
71 /// # Errors
72 /// Returns a conflict if existing immutable content or delivery history differs,
73 /// or an error for unsupported history, incompatible schema, invalid stored data,
74 /// or database failure. Roll back the caller-owned transaction on failure.
75 pub async fn import_for_migration<'c>(
76 &self,
77 transaction: &mut Transaction<'c, Postgres>,
78 event: NewEvent,
79 state: ImportedDeliveryState,
80 ) -> Result<ImportOutcome, ImportError> {
81 self.bind_tenant(transaction)
82 .await
83 .map_err(|source| ImportError::sql("bind tenant", source))?;
84 import::import_for_scope(transaction, &self.tenant_id, event, state).await
85 }
86
87 /// Finalizes one canonical pending migration row for this tenant.
88 ///
89 /// # Errors
90 /// Returns an error for invalid occurrence time, conflicting or non-pending
91 /// delivery history, incompatible schema, or database failure. The caller owns
92 /// rollback and commit; a failed operation must not be committed.
93 pub async fn finalize_pending_delivery_for_migration<'c>(
94 &self,
95 transaction: &mut Transaction<'c, Postgres>,
96 row_id: dovecote::RowId,
97 delivered_at: OffsetDateTime,
98 ) -> Result<FinalizeOutcome, FinalizeError> {
99 self.bind_tenant(transaction)
100 .await
101 .map_err(|source| FinalizeError::sql("bind tenant", source))?;
102 finalize::finalize_for_scope(transaction, &self.tenant_id, row_id, delivered_at).await
103 }
104
105 /// Reads a live page restricted to this tenant.
106 ///
107 /// # Errors
108 /// Returns an error for incompatible schema, invalid stored event or delivery
109 /// state, or a database failure. No delivery state is changed.
110 pub async fn page(
111 &self,
112 after_row_id: Option<dovecote::RowId>,
113 limit: dovecote::Limit,
114 ) -> Result<Vec<dovecote::PagedEvent>, PageError> {
115 page::page_for_scope(&self.pool, Some(&self.tenant_id), after_row_id, limit).await
116 }
117
118 /// Begins a finite snapshot pager restricted to this tenant.
119 ///
120 /// # Errors
121 /// Returns an error if the backend cannot establish the required snapshot,
122 /// the schema is incompatible, or a database operation fails.
123 pub async fn begin_snapshot(&self) -> Result<SnapshotPager, PageError> {
124 page::begin_snapshot_for_scope(&self.pool, Some(&self.tenant_id)).await
125 }
126
127 /// Claims pending and expired deliveries for this tenant.
128 ///
129 /// # Errors
130 /// Returns an error for incompatible backend or schema, invalid stored state,
131 /// attempt-counter overflow, unavailable entropy, or database failure. The owned
132 /// transaction is rolled back on pre-commit failure; an unknown commit requires
133 /// recovery from durable state rather than assuming no claim occurred.
134 pub async fn claim(
135 &self,
136 worker: dovecote::WorkerId,
137 lease_for: dovecote::Lease,
138 limit: dovecote::Limit,
139 ) -> Result<Vec<ClaimedEvent>, ClaimError> {
140 lifecycle::claim_for_scope(&self.pool, Some(&self.tenant_id), worker, lease_for, limit)
141 .await
142 }
143
144 /// Renews one current claim for this tenant.
145 ///
146 /// # Errors
147 /// Returns `LostClaim` if the token no longer owns an unexpired claim, or an
148 /// error for invalid stored state, duration overflow, or database failure.
149 pub async fn renew(
150 &self,
151 row_id: dovecote::RowId,
152 claim_token: &dovecote::ClaimToken,
153 lease_for: dovecote::Lease,
154 ) -> Result<(), MutationError> {
155 lifecycle::renew_for_scope(
156 &self.pool,
157 Some(&self.tenant_id),
158 row_id,
159 claim_token,
160 lease_for,
161 )
162 .await
163 }
164
165 /// Acknowledges one current claim for this tenant.
166 ///
167 /// # Errors
168 /// Returns `LostClaim` if the token no longer owns an unexpired claim, or an
169 /// error for invalid stored state or database failure. A lost commit response
170 /// requires durable-state recovery; delivery remains at least once.
171 pub async fn ack(
172 &self,
173 row_id: dovecote::RowId,
174 claim_token: &dovecote::ClaimToken,
175 ) -> Result<(), MutationError> {
176 lifecycle::ack_for_scope(&self.pool, Some(&self.tenant_id), row_id, claim_token).await
177 }
178
179 /// Returns one current claim to pending for this tenant.
180 ///
181 /// # Errors
182 /// Returns `LostClaim` if the token no longer owns an unexpired claim, or an
183 /// error for invalid stored state, delay overflow, or database failure.
184 pub async fn retry(
185 &self,
186 row_id: dovecote::RowId,
187 claim_token: &dovecote::ClaimToken,
188 failure: &dovecote::Failure,
189 backoff: dovecote::Delay,
190 ) -> Result<(), MutationError> {
191 lifecycle::retry_for_scope(
192 &self.pool,
193 Some(&self.tenant_id),
194 row_id,
195 claim_token,
196 failure,
197 backoff,
198 )
199 .await
200 }
201
202 /// Releases one current claim for this tenant.
203 ///
204 /// # Errors
205 /// Returns `LostClaim` if the token no longer owns an unexpired claim, or an
206 /// error for invalid stored state, delay overflow, or database failure.
207 pub async fn release(
208 &self,
209 row_id: dovecote::RowId,
210 claim_token: &dovecote::ClaimToken,
211 delay: dovecote::Delay,
212 ) -> Result<(), MutationError> {
213 lifecycle::release_for_scope(
214 &self.pool,
215 Some(&self.tenant_id),
216 row_id,
217 claim_token,
218 delay,
219 )
220 .await
221 }
222
223 /// Quarantines one current claim for this tenant.
224 ///
225 /// # Errors
226 /// Returns `LostClaim` if the token no longer owns an unexpired claim, or an
227 /// error for invalid stored state or database failure.
228 pub async fn quarantine(
229 &self,
230 row_id: dovecote::RowId,
231 claim_token: &dovecote::ClaimToken,
232 reason: &dovecote::QuarantineReason,
233 ) -> Result<(), MutationError> {
234 lifecycle::quarantine_for_scope(
235 &self.pool,
236 Some(&self.tenant_id),
237 row_id,
238 claim_token,
239 reason,
240 )
241 .await
242 }
243}
244
245/// Explicit all-tenant `PostgreSQL` Dovecote operations.
246#[derive(Clone)]
247pub struct AdminDovecote {
248 pool: sqlx::PgPool,
249}
250
251impl AdminDovecote {
252 pub(crate) const fn new(pool: sqlx::PgPool) -> Self {
253 Self { pool }
254 }
255
256 /// Borrows the pool used by this handle.
257 #[must_use]
258 pub const fn pool(&self) -> &sqlx::PgPool {
259 &self.pool
260 }
261
262 /// Enqueues an event for an explicitly named tenant.
263 ///
264 /// # Errors
265 /// Returns an identity conflict for different immutable content, a schema or
266 /// backend incompatibility, invalid stored data, or a database error. The caller
267 /// must roll back its transaction on failure; this method never commits it.
268 pub async fn enqueue<'c>(
269 &self,
270 transaction: &mut Transaction<'c, Postgres>,
271 tenant_id: TenantId,
272 event: NewEvent,
273 ) -> Result<EnqueueOutcome, EnqueueError> {
274 enqueue::enqueue_for_scope(transaction, &tenant_id, event).await
275 }
276
277 /// Imports one event and legacy state for an explicitly named tenant.
278 ///
279 /// # Errors
280 /// Returns a conflict if existing immutable content or delivery history differs,
281 /// or an error for unsupported history, incompatible schema, invalid stored data,
282 /// or database failure. Roll back the caller-owned transaction on failure.
283 pub async fn import_for_migration<'c>(
284 &self,
285 transaction: &mut Transaction<'c, Postgres>,
286 tenant_id: TenantId,
287 event: NewEvent,
288 state: ImportedDeliveryState,
289 ) -> Result<ImportOutcome, ImportError> {
290 import::import_for_scope(transaction, &tenant_id, event, state).await
291 }
292
293 /// Finalizes one migration row for an explicitly named tenant.
294 ///
295 /// # Errors
296 /// Returns an error for invalid occurrence time, conflicting or non-pending
297 /// delivery history, incompatible schema, or database failure. The caller owns
298 /// rollback and commit; a failed operation must not be committed.
299 pub async fn finalize_pending_delivery_for_migration<'c>(
300 &self,
301 transaction: &mut Transaction<'c, Postgres>,
302 tenant_id: TenantId,
303 row_id: dovecote::RowId,
304 delivered_at: OffsetDateTime,
305 ) -> Result<FinalizeOutcome, FinalizeError> {
306 finalize::finalize_for_scope(transaction, &tenant_id, row_id, delivered_at).await
307 }
308
309 /// Reads a live page across all tenants.
310 ///
311 /// # Errors
312 /// Returns an error for incompatible schema, invalid stored event or delivery
313 /// state, or a database failure. No delivery state is changed.
314 pub async fn page(
315 &self,
316 after_row_id: Option<dovecote::RowId>,
317 limit: dovecote::Limit,
318 ) -> Result<Vec<dovecote::PagedEvent>, PageError> {
319 page::page_for_scope(&self.pool, None, after_row_id, limit).await
320 }
321
322 /// Begins a finite snapshot pager across all tenants.
323 ///
324 /// # Errors
325 /// Returns an error if the backend cannot establish the required snapshot,
326 /// the schema is incompatible, or a database operation fails.
327 pub async fn begin_snapshot(&self) -> Result<SnapshotPager, PageError> {
328 page::begin_snapshot_for_scope(&self.pool, None).await
329 }
330
331 /// Claims pending and expired deliveries across all tenants.
332 ///
333 /// # Errors
334 /// Returns an error for incompatible backend or schema, invalid stored state,
335 /// attempt-counter overflow, unavailable entropy, or database failure. The owned
336 /// transaction is rolled back on pre-commit failure; an unknown commit requires
337 /// recovery from durable state rather than assuming no claim occurred.
338 pub async fn claim(
339 &self,
340 worker: dovecote::WorkerId,
341 lease_for: dovecote::Lease,
342 limit: dovecote::Limit,
343 ) -> Result<Vec<ClaimedEvent>, ClaimError> {
344 lifecycle::claim_for_scope(&self.pool, None, worker, lease_for, limit).await
345 }
346
347 /// Renews one claim for an explicitly named tenant.
348 ///
349 /// # Errors
350 /// Returns `LostClaim` if the token no longer owns an unexpired claim, or an
351 /// error for invalid stored state, duration overflow, or database failure.
352 pub async fn renew(
353 &self,
354 tenant_id: TenantId,
355 row_id: dovecote::RowId,
356 claim_token: &dovecote::ClaimToken,
357 lease_for: dovecote::Lease,
358 ) -> Result<(), MutationError> {
359 lifecycle::renew_for_scope(&self.pool, Some(&tenant_id), row_id, claim_token, lease_for)
360 .await
361 }
362
363 /// Acknowledges one claim for an explicitly named tenant.
364 ///
365 /// # Errors
366 /// Returns `LostClaim` if the token no longer owns an unexpired claim, or an
367 /// error for invalid stored state or database failure. A lost commit response
368 /// requires durable-state recovery; delivery remains at least once.
369 pub async fn ack(
370 &self,
371 tenant_id: TenantId,
372 row_id: dovecote::RowId,
373 claim_token: &dovecote::ClaimToken,
374 ) -> Result<(), MutationError> {
375 lifecycle::ack_for_scope(&self.pool, Some(&tenant_id), row_id, claim_token).await
376 }
377
378 /// Retries one claim for an explicitly named tenant.
379 ///
380 /// # Errors
381 /// Returns `LostClaim` if the token no longer owns an unexpired claim, or an
382 /// error for invalid stored state, delay overflow, or database failure.
383 pub async fn retry(
384 &self,
385 tenant_id: TenantId,
386 row_id: dovecote::RowId,
387 claim_token: &dovecote::ClaimToken,
388 failure: &dovecote::Failure,
389 backoff: dovecote::Delay,
390 ) -> Result<(), MutationError> {
391 lifecycle::retry_for_scope(
392 &self.pool,
393 Some(&tenant_id),
394 row_id,
395 claim_token,
396 failure,
397 backoff,
398 )
399 .await
400 }
401
402 /// Releases one claim for an explicitly named tenant.
403 ///
404 /// # Errors
405 /// Returns `LostClaim` if the token no longer owns an unexpired claim, or an
406 /// error for invalid stored state, delay overflow, or database failure.
407 pub async fn release(
408 &self,
409 tenant_id: TenantId,
410 row_id: dovecote::RowId,
411 claim_token: &dovecote::ClaimToken,
412 delay: dovecote::Delay,
413 ) -> Result<(), MutationError> {
414 lifecycle::release_for_scope(&self.pool, Some(&tenant_id), row_id, claim_token, delay).await
415 }
416
417 /// Quarantines one claim for an explicitly named tenant.
418 ///
419 /// # Errors
420 /// Returns `LostClaim` if the token no longer owns an unexpired claim, or an
421 /// error for invalid stored state or database failure.
422 pub async fn quarantine(
423 &self,
424 tenant_id: TenantId,
425 row_id: dovecote::RowId,
426 claim_token: &dovecote::ClaimToken,
427 reason: &dovecote::QuarantineReason,
428 ) -> Result<(), MutationError> {
429 lifecycle::quarantine_for_scope(&self.pool, Some(&tenant_id), row_id, claim_token, reason)
430 .await
431 }
432}