Skip to main content

dovecote_sqlx_sqlite/
scope.rs

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