Skip to main content

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}