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}