Skip to main content

coven_database/
database_sync.rs

1//! Database operations used by synchronization, on the retained connections.
2
3use super::{Database, Mode};
4use crate::internal_schema::retry_effect::{self, RetryState};
5use crate::DbError;
6use coven_foundation::task::CarriesBug;
7
8impl Database {
9    /// Transfer metadata of the current durable claim attempt.
10    pub async fn claim_upload(&self) -> Result<crate::ClaimUpload, DbError> {
11        self.call(|inner| inner.access.with_committed(crate::claim::upload)).await
12    }
13    /// A bounded range of the current attempt, used by a recorded upload.
14    pub async fn claim_range(&self, offset: u64, length: usize) -> Result<Vec<u8>, DbError> {
15        self.call(move |inner| {
16            inner.access.with_committed(|db| crate::claim::range(db, offset, length))
17        })
18        .await
19    }
20    /// Save provider-confirmed progress before the next part is sent.
21    pub async fn save_claim_upload(&self, session: Vec<u8>) -> Result<(), DbError> {
22        self.call(move |inner| {
23            inner.access.with_writer(|writer| {
24                writer.stream_transaction(|db| crate::claim::save_upload(db, session))
25            })
26        })
27        .await
28    }
29    /// Fix first-attempt keys for this captured queue prefix. Previously
30    /// attempted originals retain their exact format and keys.
31    pub async fn prepare_uploads<E, F>(
32        &self,
33        through: u64,
34        select: F,
35    ) -> Result<crate::PreparedUploads, E>
36    where
37        E: From<DbError> + From<coven_format::Error> + CarriesBug + Send + 'static,
38        F: FnMut(
39                &coven_format::write_stream::WriteHeaderFrame,
40            ) -> Result<coven_format::sealed_write::WriteObjectPrefix, E>
41            + Send
42            + 'static,
43    {
44        self.call(move |inner| {
45            inner.access.with_writer(|writer| {
46                crate::upload::prepare_batch(writer, inner.access.device, through, select)
47            })
48        })
49        .await
50    }
51
52    /// Stream the prepared originals through one committed reader. Concurrent
53    /// app commits append after the prepared prefix and remain for the next pass.
54    pub async fn read_uploads<E, F>(
55        &self,
56        prepared: crate::PreparedUploads,
57        consume: F,
58    ) -> Result<(), E>
59    where
60        E: From<DbError> + CarriesBug + Send + 'static,
61        F: FnMut(crate::WaitingUpload<'_>) -> Result<(), E> + Send + 'static,
62    {
63        self.call(move |inner| {
64            inner
65                .access
66                .with_committed(|reader| crate::upload::read_batch(reader, prepared, consume))
67        })
68        .await
69    }
70
71    /// Authenticate and retain original source records covered by an applied slot
72    /// or snapshot, without applying their data again or changing source verdicts.
73    #[cfg(test)]
74    pub async fn retain_write_sources<R, F>(
75        &self,
76        write: crate::DownloadedWriteStream<R>,
77        authenticate: F,
78    ) -> Result<(), DbError>
79    where
80        R: std::io::Read + Send + 'static,
81        F: FnOnce() -> Result<(), DbError> + Send + 'static,
82    {
83        self.call(move |inner| {
84            inner.inputs.prepare(|inputs| {
85                let write = write.prepare(inputs)?;
86                authenticate()?;
87                inner.access.with_writer(|writer| {
88                    writer.stream_transaction(|db| {
89                        crate::download_stream::retain_sources(
90                            db,
91                            &inner.access.write_schema,
92                            write,
93                        )
94                    })
95                })
96            })
97        })
98        .await
99    }
100
101    /// Commit one complete slot, its claim number and every readable effect.
102    /// Authentication failure rolls back the slot and leaves its predecessor
103    /// applied. `writers` are the other devices that can still write once
104    /// the slot is applied, which kept records wait for (§16.7).
105    pub async fn apply_slot<P, R, F>(
106        &self,
107        incoming: crate::IncomingSlot<crate::DownloadedWriteStream<R>>,
108        writers: crate::Writers,
109        authenticate: F,
110    ) -> Result<crate::ApplyOutcome, DbError>
111    where
112        P: crate::PendingValue,
113        R: std::io::Read + Send + 'static,
114        F: FnOnce() -> Result<(), DbError> + Send + 'static,
115    {
116        self.call(move |inner| {
117            if let crate::SlotContent::Admin { effect: Some(effect), .. } = &incoming.content {
118                if let Some(store) = &effect.state.store {
119                    crate::store_state::check_creation(store.id, inner.access.directory.id())?;
120                }
121            }
122            let started = inner.clock.elapsed();
123            inner.inputs.prepare(|inputs| {
124                let slot = incoming.slot;
125                let incoming = incoming.prepare(inputs)?;
126                let prepared = inner.clock.elapsed();
127                let reader = inner.access.readers.acquire_reader();
128                let applied = inner.with_files("apply a slot", |writer, files| {
129                    reader.with_reader(|reader| {
130                        reader.read_transaction(|| {
131                            crate::write_schema::prepare_evaluators(
132                                reader,
133                                &inner.access.write_schema,
134                            )?;
135                            reader.schema_version()?;
136                            crate::sequence_apply::apply::<P>(
137                                writer,
138                                reader,
139                                &inner.access.write_schema,
140                                incoming,
141                                &writers,
142                                files,
143                                inner.raise_operation.as_deref(),
144                                authenticate,
145                            )
146                        })
147                    })
148                });
149                tracing::debug!(
150                    slot = slot.get(),
151                    preparing = ?(prepared - started),
152                    applying = ?(inner.clock.elapsed() - prepared),
153                    "applied slot"
154                );
155                applied
156            })
157        })
158        .await
159    }
160    /// Fix the keys of the oldest original before constructing its batch.
161    #[cfg(test)]
162    pub async fn prepare_upload<F>(
163        &self,
164        select: F,
165    ) -> Result<Option<coven_merge::WriteId>, DbError>
166    where
167        F: FnOnce(
168                &crate::StoreState,
169                u32,
170                &coven_format::write_stream::WriteHeaderFrame,
171            ) -> Result<coven_format::sealed_write::WriteObjectPrefix, DbError>
172            + Send
173            + 'static,
174    {
175        self.call(move |inner| {
176            inner.access.with_writer(|db| {
177                let state = crate::store_state_tables::read(db)?;
178                crate::upload::prepare(db, &state, select)
179            })
180        })
181        .await
182    }
183    /// The newest waiting write this device may send now, read in one
184    /// committed state with the queue (§6.3): none past an unpublished
185    /// breaking raise's capture or from a write ahead of an audience's
186    /// version, whose wait it records (§18.5), and none from a write a bound
187    /// upload holds on (§17.4). `None` with no write waiting.
188    pub async fn sendable_through<P: crate::PendingValue>(&self) -> Result<Option<u64>, DbError> {
189        self.call(|inner| {
190            let device = inner.access.device;
191            let ahead =
192                inner.access.with_committed(|db| crate::raise_capture::first_ahead(db, device))?;
193            if ahead.is_none() {
194                return inner
195                    .access
196                    .with_committed(|db| crate::upload::sendable_through(db, device));
197            }
198            inner.access.with_writer(|writer| {
199                writer.state_transaction(|db| {
200                    crate::raise_capture::record_ahead::<P>(db, device)?;
201                    crate::upload::sendable_through(db, device)
202                })
203            })
204        })
205        .await
206    }
207
208    /// Original plaintext of an authenticated received or confirmed published header.
209    /// This establishes header facts, not the validity of a subsequently read body.
210    #[cfg(test)]
211    pub async fn checked_write_header(
212        &self,
213        id: coven_merge::WriteId,
214    ) -> Result<Option<coven_format::write_stream::WriteHeaderFrame>, DbError> {
215        self.call(move |inner| {
216            inner
217                .access
218                .readers
219                .acquire_reader()
220                .with_reader(|db| crate::write_headers::read(db, id))
221        })
222        .await
223    }
224
225    /// Audiences whose counted administrative effects require replacement data.
226    /// Loading their replacement rows removes the requirement atomically.
227    pub async fn reloading_audiences(
228        &self,
229    ) -> Result<std::collections::BTreeSet<coven_merge::Audience>, DbError> {
230        self.call(move |inner| {
231            inner
232                .access
233                .readers
234                .acquire_reader()
235                .with_reader(crate::reload_gate::reloading_audiences)
236        })
237        .await
238    }
239
240    /// Current pending records, decoded as the sync owner's typed values.
241    pub async fn pending<P: crate::PendingValue>(&self) -> Result<Vec<P>, DbError> {
242        self.read(|sql| sql.pending()).await
243    }
244
245    /// Writes and files this device committed and has not uploaded, from one
246    /// committed state.
247    pub async fn unsent(&self) -> Result<crate::UnsentWork, DbError> {
248        self.read(|sql| sql.unsent()).await
249    }
250
251    /// The same pre-built unsent-work query, observing committed changes.
252    pub fn subscribe_unsent<E>(&self) -> crate::LiveQuery<crate::UnsentWork, E>
253    where
254        E: From<DbError> + coven_foundation::task::CarriesBug + Send + 'static,
255    {
256        self.subscribe(|sql| sql.unsent().map_err(E::from))
257    }
258
259    /// What the sync state is computed from, in one committed snapshot
260    /// (§19.1): `coven_sync_status`'s row, `coven_retry_effect`'s, the send
261    /// and upload queues, the files moving and the pending records, decoded
262    /// as the sync owner's typed values, given to `compute` in the read.
263    pub async fn sync_facts<P, T, E>(
264        &self,
265        compute: impl FnOnce(crate::SyncFacts<P>) -> Result<T, E> + Send + 'static,
266    ) -> Result<T, E>
267    where
268        P: crate::PendingValue + Clone,
269        T: Send + 'static,
270        E: From<DbError> + coven_foundation::task::CarriesBug + Send + 'static,
271    {
272        self.read(move |sql| compute(sql.sync_facts()?)).await
273    }
274
275    /// The same pre-built query, live: it runs again on each committed
276    /// change of what it reads, and as a file starts or ends a transfer.
277    pub fn subscribe_sync_facts<P, T, E>(
278        &self,
279        compute: impl Fn(crate::SyncFacts<P>) -> Result<T, E> + Send + Sync + 'static,
280    ) -> crate::LiveQuery<T, E>
281    where
282        P: crate::PendingValue + Clone,
283        T: Send + 'static,
284        E: From<DbError> + coven_foundation::task::CarriesBug + Send + 'static,
285    {
286        self.subscribe(move |sql| compute(sql.sync_facts()?))
287    }
288
289    /// The upload queue, its pause setting and each file's recorded provider
290    /// session in one committed snapshot, available while disconnected (E7).
291    pub async fn upload_queue(&self) -> Result<crate::UploadQueueState, DbError> {
292        self.read(|sql| sql.upload_queue()).await
293    }
294
295    /// Record, as the store opens, whether a storage location is recorded
296    /// here and its credentials held, and that no pass moves, keeping how the
297    /// last pass ended and when sync last finished; `finished`, for a store
298    /// joining or restoring read to its end, is when (api.md E5). The
299    /// provider is as sync names it. A failure fails this handle with its
300    /// cause, as losing a pending record does.
301    pub async fn record_opened(
302        &self,
303        location: crate::RecordedLocation<String>,
304        finished: Option<std::time::SystemTime>,
305    ) -> Result<(), DbError> {
306        self.save_sync_status(move |db| {
307            crate::internal_schema::sync_status::save_location(db, &location, finished, true)
308        })
309        .await
310    }
311
312    /// Record whether a storage location is recorded here and its
313    /// credentials held, as setup, a change of credentials or releasing a
314    /// connection finds it; `finished`, for setup, is when it read the store
315    /// to its end. A failure fails this handle, as `record_opened`'s does.
316    pub async fn record_location(
317        &self,
318        location: crate::RecordedLocation<String>,
319        finished: Option<std::time::SystemTime>,
320    ) -> Result<(), DbError> {
321        self.save_sync_status(move |db| {
322            crate::internal_schema::sync_status::save_location(db, &location, finished, false)
323        })
324        .await
325    }
326
327    /// Record that the running pass read a slot it hadn't, or claimed one.
328    /// A failure fails this handle, as `record_opened`'s does.
329    pub async fn record_moving(&self) -> Result<(), DbError> {
330        self.save_sync_status(crate::internal_schema::sync_status::save_moving).await
331    }
332
333    /// Record how the last pass ended, as sync encodes it, when sync failed
334    /// outside a pass's end: starting, stopping, or a local failure or panic
335    /// of the loop's, in a pass or outside one; and that nothing moves. A
336    /// failure fails this handle, as `record_opened`'s does.
337    pub async fn record_last_pass(&self, last_pass: Vec<u8>) -> Result<(), DbError> {
338        self.save_sync_status(move |db| {
339            crate::internal_schema::sync_status::save_last_pass(db, &last_pass)
340        })
341        .await
342    }
343
344    /// Record that a store-wide worker's panic halted sync until the store
345    /// reopens, with its failure, as sync encodes it, as how the last pass
346    /// ended, and that nothing moves: from here no pass writes the row. A
347    /// failure fails this handle, as `record_opened`'s does.
348    pub async fn record_halt(&self, last_pass: Vec<u8>) -> Result<(), DbError> {
349        self.save_sync_status(move |db| {
350            crate::internal_schema::sync_status::save_halt(db, &last_pass)
351        })
352        .await
353    }
354
355    /// Record how the running pass ended, as sync encodes it, and that it
356    /// moves no more; `finished`, for a pass that reached the end of the
357    /// sequence, is when, which becomes the last synced time unless that is
358    /// later. A failure fails this handle, as `record_opened`'s does.
359    pub async fn record_pass_end(
360        &self,
361        last_pass: Vec<u8>,
362        finished: Option<std::time::SystemTime>,
363    ) -> Result<(), DbError> {
364        self.save_sync_status(move |db| {
365            crate::internal_schema::sync_status::save_pass_end(db, &last_pass, finished)
366        })
367        .await
368    }
369
370    /// Write `coven_sync_status` in the step that changes it, on the writer.
371    async fn save_sync_status(
372        &self,
373        save: impl FnOnce(&crate::sqlite::DatabaseConnection) -> Result<(), DbError> + Send + 'static,
374    ) -> Result<(), DbError> {
375        self.call(move |inner| inner.access.with_writer(|writer| writer.state_transaction(save)))
376            .await
377    }
378
379    /// Change what decides the person's retry, `coven_retry_effect`'s row,
380    /// then run `then` with the state as committed, in one step on the
381    /// writer: no other change to the state, and no other `then`, comes
382    /// between them, so a decision `then` makes from the state is the one
383    /// the row shows. `change` gets the state as the row holds it; `then`
384    /// runs only once the change commits. A failed write fails this handle
385    /// with its cause, as losing a pending record does.
386    pub async fn change_retry_state<T: Send + 'static>(
387        &self,
388        change: impl FnOnce(RetryState) -> RetryState + Send + 'static,
389        then: impl FnOnce(RetryState) -> T + Send + 'static,
390    ) -> Result<T, DbError> {
391        self.retry_step(change, then, |_| Ok(())).await
392    }
393
394    /// Change the retry state as sync starts, as `change_retry_state` does,
395    /// and count the start in `coven_sync_status.starts` in the same
396    /// transaction: every start writes the row, even one that holds for the
397    /// reason the last one did.
398    pub async fn start_retry_state<T: Send + 'static>(
399        &self,
400        change: impl FnOnce(RetryState) -> RetryState + Send + 'static,
401        then: impl FnOnce(RetryState) -> T + Send + 'static,
402    ) -> Result<T, DbError> {
403        self.retry_step(change, then, crate::internal_schema::sync_status::save_start).await
404    }
405
406    async fn retry_step<T: Send + 'static>(
407        &self,
408        change: impl FnOnce(RetryState) -> RetryState + Send + 'static,
409        then: impl FnOnce(RetryState) -> T + Send + 'static,
410        also: fn(&crate::sqlite::DatabaseConnection) -> Result<(), DbError>,
411    ) -> Result<T, DbError> {
412        self.call(move |inner| {
413            inner.access.with_writer(|writer| {
414                let state = writer.state_transaction(|db| {
415                    let state = retry_effect::stored(change(retry_effect::read(db)?))?;
416                    retry_effect::save(db, state)?;
417                    also(db)?;
418                    Ok(state)
419                })?;
420                Ok(then(state))
421            })
422        })
423        .await
424    }
425
426    /// Record the provider cooldown's wait, and change the retry state as
427    /// `change_retry_state` does, in one transaction: the cooldown that
428    /// holds every pass and the effect it gives a retry commit together.
429    pub async fn record_cooldown(
430        &self,
431        wait: crate::RetryWait,
432        change: impl FnOnce(RetryState) -> RetryState + Send + 'static,
433    ) -> Result<(), DbError> {
434        self.call(move |inner| {
435            inner.access.with_writer(|writer| {
436                writer.state_transaction(|db| {
437                    crate::retry::save(db, crate::RetrySubject::Provider, &wait)?;
438                    let state = retry_effect::stored(change(retry_effect::read(db)?))?;
439                    retry_effect::save(db, state)
440                })
441            })
442        })
443        .await
444    }
445
446    /// Record one local first unmet condition, replacing its previous reason.
447    pub async fn record_pending<P: crate::PendingValue>(&self, record: P) -> Result<(), DbError> {
448        self.call(move |inner| {
449            inner.access.with_writer(|writer| {
450                if let Some(id) = record.operation() {
451                    if crate::operation::find(writer, id)?.is_none() {
452                        return Err(DbError::OperationChanged(id));
453                    }
454                }
455                writer.state_transaction(|db| crate::pending::save(db, &record))
456            })
457        })
458        .await
459    }
460
461    /// Clear this device's observation when its subject advances. The usual
462    /// case, with nothing recorded, reads without taking the writer.
463    pub async fn clear_pending<S: serde::Serialize + Send + 'static>(
464        &self,
465        subject: S,
466    ) -> Result<(), DbError> {
467        self.clear_pending_checked(subject, crate::pending::recorded, |_| Ok(())).await.map(drop)
468    }
469
470    /// Settle only the condition the consumer actually checked, using the
471    /// subject's indexed lookup and the same pending transaction as deletion.
472    pub(crate) async fn clear_checked<P: crate::PendingValue>(
473        &self,
474        subject: P::Subject,
475        checked: impl Fn(&P) -> bool + Send + 'static,
476    ) -> Result<(), DbError>
477    where
478        P::Subject: Send + 'static,
479    {
480        self.clear_pending_checked(
481            subject,
482            move |db, subject| {
483                Ok(crate::pending::find::<P>(db, subject)?.is_some_and(|record| checked(&record)))
484            },
485            |_| Ok(()),
486        )
487        .await
488        .map(drop)
489    }
490
491    /// Settle only the condition the consumer actually checked, as
492    /// `clear_checked` does, ending the wait of `object`'s queued upload
493    /// with it.
494    pub(crate) async fn clear_pending_if<P: crate::PendingValue>(
495        &self,
496        object: (coven_foundation::id_source::DeviceId, coven_foundation::id_source::ObjectId),
497        subject: P::Subject,
498        checked: impl Fn(&P) -> bool + Send + 'static,
499    ) -> Result<Option<crate::RetrySubject>, DbError>
500    where
501        P::Subject: Send + 'static,
502    {
503        self.clear_pending_checked(
504            subject,
505            move |db, subject| {
506                Ok(crate::pending::find::<P>(db, subject)?.is_some_and(|record| checked(&record)))
507            },
508            move |db| {
509                let retry = crate::file_queue::upload_id(db, object.0, object.1)?
510                    .map(crate::RetrySubject::Upload);
511                if let Some(retry) = retry {
512                    crate::retry::clear(db, retry)?;
513                }
514                Ok(retry)
515            },
516        )
517        .await
518        .map(Option::flatten)
519    }
520
521    async fn clear_pending_checked<S, F, T>(
522        &self,
523        subject: S,
524        checked: F,
525        settled: impl FnOnce(&crate::sqlite::DatabaseConnection) -> Result<T, DbError> + Send + 'static,
526    ) -> Result<Option<T>, DbError>
527    where
528        S: serde::Serialize + Send + 'static,
529        T: Send + 'static,
530        F: Fn(&crate::sqlite::DatabaseConnection, &S) -> Result<bool, DbError> + Send + 'static,
531    {
532        self.call(move |inner| {
533            let should_clear =
534                inner.access.readers.acquire_reader().with_reader(|db| checked(db, &subject))?;
535            if !should_clear {
536                return Ok(None);
537            }
538            inner.access.with_writer(|writer| {
539                writer.state_transaction(|db| {
540                    // Another writer may have replaced this condition since
541                    // the committed read; deletion checks that current record.
542                    if checked(db, &subject)? {
543                        crate::pending::clear(db, &subject)?;
544                        return settled(db).map(Some);
545                    }
546                    Ok(None)
547                })
548            })
549        })
550        .await
551    }
552
553    /// Replace this device's observations of one completed scan atomically:
554    /// its records matching `scanned` become exactly `records`, so a subject
555    /// that now checks, or is no longer listed, loses its record in the same
556    /// transaction. An unchanged set writes nothing.
557    #[cfg(test)]
558    pub async fn replace_local_pending<P: crate::PendingValue>(
559        &self,
560        scanned: impl Fn(&P) -> bool + Send + 'static,
561        records: Vec<P>,
562    ) -> Result<(), DbError> {
563        use std::collections::BTreeSet;
564        self.call(move |inner| {
565            let mut identities = BTreeSet::new();
566            for record in &records {
567                if record.operation().is_some()
568                    || !scanned(record)
569                    || !identities.insert(record.subject())
570                {
571                    return Err(DbError::Invariant {
572                        what: "a scan replaces distinct records it covers",
573                    });
574                }
575            }
576            let encode = |r: &P| serde_json::to_vec(r).map_err(DbError::StateEncoding);
577            let next = records.iter().map(encode).collect::<Result<BTreeSet<_>, _>>()?;
578            // The usual unchanged scan reads its class without taking the writer.
579            let unchanged = inner.access.readers.acquire_reader().with_reader(|reader| {
580                let current = crate::pending::read::<P>(reader)?
581                    .into_iter()
582                    .filter(|r| scanned(r))
583                    .map(|r| encode(&r))
584                    .collect::<Result<BTreeSet<_>, _>>()?;
585                Ok::<_, DbError>(current == next)
586            })?;
587            if unchanged {
588                return Ok(());
589            }
590            inner.access.with_writer(|writer| {
591                let previous: Vec<_> =
592                    crate::pending::read::<P>(writer)?.into_iter().filter(|r| scanned(r)).collect();
593                if previous.iter().map(encode).collect::<Result<BTreeSet<_>, _>>()? == next {
594                    return Ok(());
595                }
596                writer.state_transaction(|db| {
597                    for record in &previous {
598                        if !identities.contains(record.subject()) {
599                            crate::pending::clear(db, record.subject())?;
600                        }
601                    }
602                    for record in &records {
603                        crate::pending::save(db, record)?;
604                    }
605                    Ok(())
606                })
607            })
608        })
609        .await
610    }
611
612    /// Apply the persisted condition's retry policy to this installed version.
613    pub async fn pending_can_retry<P: crate::PendingValue>(
614        &self,
615        subject: P::Subject,
616    ) -> Result<bool, DbError>
617    where
618        P::Subject: Send + 'static,
619    {
620        self.call(move |inner| {
621            inner.access.with_writer(|writer| crate::pending::retry::<P>(writer, &subject))
622        })
623        .await
624    }
625
626    /// Make every pending record one `build` recorded, as a store opened by
627    /// another build of coven finds the records an earlier one wrote (§19.1).
628    #[cfg(any(test, feature = "test-utils"))]
629    pub async fn set_pending_build(&self, build: u128) -> Result<(), DbError> {
630        self.call(move |inner| {
631            inner.access.with_writer(|writer| {
632                writer.state_transaction(|db| crate::pending::set_build(db, build))
633            })
634        })
635        .await
636    }
637
638    /// Read the oldest waiting plaintext.
639    /// The callback runs in a committed reader transaction; its result is `None`
640    /// when the queue is empty.
641    #[cfg(any(test, feature = "test-utils"))]
642    pub async fn read_oldest_upload<F, R, E>(
643        &self,
644        consume: F,
645    ) -> Result<Option<R>, crate::UploadReadError<E>>
646    where
647        F: FnOnce(crate::UploadPlaintext<'_>) -> Result<R, E> + Send + 'static,
648        R: Send + 'static,
649        E: CarriesBug + Send + 'static,
650    {
651        self.call(move |inner| {
652            let reader = inner.access.readers.acquire_reader();
653            reader.with_reader(|reader| {
654                reader.read_transaction(|| crate::upload::read(reader, consume))
655            })
656        })
657        .await
658    }
659
660    /// The oldest waiting write and the keys its first attempt fixed, if
661    /// any; `None` when the queue is empty.
662    #[cfg(test)]
663    pub(crate) async fn oldest_upload_keys(
664        &self,
665    ) -> Result<
666        Option<(coven_merge::WriteId, Option<coven_format::sealed_write::WriteObjectPrefix>)>,
667        DbError,
668    > {
669        self.call(move |inner| {
670            let reader = inner.access.readers.acquire_reader();
671            reader.with_reader(|reader| {
672                reader.read_transaction(|| crate::upload::oldest_keys(reader))
673            })
674        })
675        .await
676    }
677
678    /// The same state, live (E4), for a test waiting for passes to change
679    /// it, such as to apply a raise.
680    #[cfg(any(test, feature = "test-utils"))]
681    pub fn subscribe_store_state(&self) -> crate::LiveQuery<crate::StoreState, DbError> {
682        self.subscribe(|sql| sql.store_state())
683    }
684
685    /// The store prefix whose complete effects have committed.
686    pub async fn applied_slot(&self) -> Result<u64, DbError> {
687        self.call(|inner| {
688            #[cfg(any(test, feature = "test-utils"))]
689            if inner
690                .applied_slot_failures
691                .fetch_update(
692                    std::sync::atomic::Ordering::SeqCst,
693                    std::sync::atomic::Ordering::SeqCst,
694                    |left| left.checked_sub(1),
695                )
696                .is_ok()
697            {
698                return Err(DbError::Sqlite(rusqlite::Error::SqliteFailure(
699                    rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_IOERR),
700                    Some("injected applied slot read failure".into()),
701                )));
702            }
703            inner.access.with_committed(crate::sequence::applied)
704        })
705        .await
706    }
707
708    /// Fail the next `count` `applied_slot` reads with a disk error.
709    #[cfg(any(test, feature = "test-utils"))]
710    pub async fn fail_applied_slots(&self, count: usize) {
711        self.call(move |inner| {
712            inner.applied_slot_failures.store(count, std::sync::atomic::Ordering::SeqCst);
713            Ok::<_, DbError>(())
714        })
715        .await
716        .expect("an open store arms its applied slot failures");
717    }
718
719    /// Claim numbers and complete-slot digests recorded by every consumed slot, including refusals.
720    #[cfg(any(test, feature = "test-utils"))]
721    pub async fn last_claims(&self) -> Result<Vec<coven_format::snapshot::LastClaim>, DbError> {
722        self.call(|inner| inner.access.with_committed(crate::sequence::claims)).await
723    }
724
725    /// Original administrative, copy and announcement evidence retained by snapshots.
726    pub async fn original_slots(&self) -> Result<Vec<coven_format::floor::OriginalSlot>, DbError> {
727        self.call(|inner| {
728            inner.access.with_committed(|db| {
729                db.query("SELECT slot,record FROM _coven_slot_records ORDER BY slot", [], |row| {
730                    Ok(coven_format::floor::OriginalSlot {
731                        slot: crate::write_encoding::slot_counter(row.get(0)?)?,
732                        object: row.get(1)?,
733                    })
734                })
735            })
736        })
737        .await
738    }
739
740    /// Greatest locally reserved claim number, including finished journals.
741    #[cfg(any(test, feature = "test-utils"))]
742    pub async fn claim_counter(&self) -> Result<u64, DbError> {
743        self.call(|inner| {
744            inner.access.with_committed(|db| crate::claim::reserved(db, inner.access.device))
745        })
746        .await
747    }
748
749    /// Whether this copy journaled these complete bytes at this exact slot.
750    pub async fn attempted_slot(
751        &self,
752        number: u64,
753        path: coven_format::path::ObjectPath,
754        digest: [u8; 32],
755    ) -> Result<bool, DbError> {
756        self.call(move |inner| {
757            inner
758                .access
759                .with_committed(|db| crate::claim::attempted_digest(db, number, &path, &digest))
760        })
761        .await
762    }
763
764    /// The active claim and every durable attempted path. `claim_range`
765    /// reads the current attempt's exact bytes.
766    pub async fn current_claim(&self) -> Result<Option<crate::Claim>, DbError> {
767        self.call(|inner| inner.access.with_committed(crate::claim::current)).await
768    }
769
770    /// Seal and stage outside the application writer, then atomically reserve
771    /// the claim number and retain its complete attempted bytes.
772    /// Returning the attempt proves the journal transaction has committed.
773    pub async fn begin_claim<F, R, E>(
774        &self,
775        work: crate::ClaimWork,
776        seal: F,
777    ) -> Result<crate::Claim, E>
778    where
779        F: FnOnce(u64) -> Result<crate::ClaimAttempt<R>, E> + Send + 'static,
780        R: std::io::Read + Send + 'static,
781        E: From<DbError> + CarriesBug + Send + 'static,
782    {
783        self.call(move |inner| {
784            let number = inner
785                .access
786                .with_committed(|db| crate::claim::reserved(db, inner.access.device))?
787                .checked_add(1)
788                .ok_or(DbError::ClaimNumberExhausted)?;
789            let attempt = seal(number)?;
790            Ok(inner.inputs.prepare_claim(attempt, |prepared| {
791                inner.access.with_writer(|writer| {
792                    writer.stream_transaction(|db| {
793                        crate::claim::begin(db, inner.access.device, work, number, prepared)
794                    })
795                })
796            })?)
797        })
798        .await
799    }
800
801    /// Stream the current attempt's exact ciphertext from one committed reader.
802    #[cfg(any(test, feature = "test-utils"))]
803    pub async fn read_current_attempt<T, E, F>(&self, consume: F) -> Result<T, E>
804    where
805        T: Send + 'static,
806        E: From<DbError> + CarriesBug + Send + 'static,
807        F: FnOnce(crate::ClaimBytes<'_>) -> Result<T, E> + Send + 'static,
808    {
809        self.call(move |inner| {
810            inner
811                .access
812                .readers
813                .acquire_reader()
814                .with_reader(|db| db.read_transaction(|| crate::claim::read_attempt(db, consume)))
815        })
816        .await
817    }
818
819    /// Retain a new envelope only after the prior attempt was proved not landed.
820    pub async fn retry_claim<R: std::io::Read + Send + 'static>(
821        &self,
822        attempt: crate::ClaimAttempt<R>,
823    ) -> Result<(), DbError> {
824        self.call(move |inner| {
825            inner.inputs.prepare_claim(attempt, |prepared| {
826                inner.access.with_writer(|writer| {
827                    writer.stream_transaction(|db| crate::claim::retry(db, prepared))
828                })
829            })
830        })
831        .await
832    }
833
834    /// Settle from checked kept bytes or the floor's authenticated snapshot.
835    pub async fn settle_claim(
836        &self,
837        read: crate::ClaimRead,
838    ) -> Result<crate::ClaimSettlement, DbError> {
839        self.call(move |inner| {
840            inner.access.with_writer(|writer| {
841                writer.stream_transaction(|db| crate::claim::settle(db, read))
842            })
843        })
844        .await
845    }
846
847    /// Finish a settled claim and its optional operation transition atomically.
848    pub async fn finish_claim(
849        &self,
850        operation: Option<crate::OperationUpdate>,
851    ) -> Result<(), DbError> {
852        self.call(move |inner| {
853            inner.access.with_writer(|writer| {
854                writer.stream_transaction(|db| {
855                    crate::claim::finish(db)?;
856                    if let Some(update) = operation {
857                        crate::operation::advance(db, &update)?;
858                    }
859                    Ok(())
860                })
861            })
862        })
863        .await
864    }
865
866    /// Whether the open claim attempted this path. Finished journals are deleted.
867    #[cfg(any(test, feature = "test-utils"))]
868    pub async fn attempted_claim(
869        &self,
870        path: coven_format::path::ObjectPath,
871    ) -> Result<bool, DbError> {
872        self.call(move |inner| inner.access.with_committed(|db| crate::claim::attempted(db, &path)))
873            .await
874    }
875}
876
877#[path = "database_device.rs"]
878mod device;
879
880impl<M: Mode> Database<M> {
881    /// Membership and audience state of the committed sequence prefix.
882    pub async fn store_state(&self) -> Result<crate::StoreState, DbError> {
883        self.call(|open| M::access(open).with_committed(crate::store_state_tables::read)).await
884    }
885}