Skip to main content

coven_database/
internal_schema_sync_status.rs

1//! What this installation's sync state is computed from that the sync loop
2//! knows (§19.1, api.md E4, E5), kept as a table so the app's live queries
3//! and local triggers can follow it (§9.4), as they follow `coven_pins`.
4//!
5//! It has one row from the store's creation on, which says no storage is set
6//! up until the first open writes over it, so a read always finds one. Each
7//! fact is written in the step that changes it: opening writes whether a
8//! location is recorded and this device holds its credentials (`storage`,
9//! with the location's `provider`) and that no pass moves; setup, joining
10//! and restoring write storage set up and their finish as the last synced
11//! time; each start of sync adds one to `starts`, so even a start that ends
12//! the way the last one did writes the row; a store-wide worker's panic
13//! halts sync until the store reopens (`halted`), after which no pass writes
14//! the row; the loop writes that its pass moved a slot (`moving`) and how the
15//! pass ended (`last_pass`, which sync encodes as JSON and the app reads as
16//! `LastPass`), with a synced pass's finish as `last_synced`, in milliseconds
17//! since the Unix epoch by the store's clock. That time never goes backward:
18//! a write keeps the later of it and the new one, so a clock set back moves
19//! nothing.
20//!
21//! Only the handle that holds the store's writer lock reads it: a read-only
22//! handle can't tell whether that writer still runs, so app SQL there may not
23//! read the table (`authorization.rs`).
24
25use crate::DbError;
26use std::time::SystemTime;
27
28/// The sync status table, which the app's triggers may follow.
29pub(crate) const SYNC_STATUS: &str = "coven_sync_status";
30
31/// The table and its one row, as every store holds them from its creation.
32pub(super) const TABLE: &str = "CREATE TABLE coven_sync_status (
33        singleton INTEGER PRIMARY KEY NOT NULL CHECK(singleton=1),
34        storage TEXT NOT NULL CHECK(storage IN ('none','signed_out','connected')),
35        provider TEXT,
36        last_pass BLOB,
37        moving INTEGER NOT NULL CHECK(moving IN (0,1)),
38        last_synced INTEGER CHECK(last_synced>=0),
39        starts INTEGER NOT NULL CHECK(starts>=0),
40        halted INTEGER NOT NULL CHECK(halted IN (0,1)),
41        CHECK((storage='none')=(provider IS NULL))
42    ) STRICT;
43    INSERT INTO coven_sync_status(singleton,storage,provider,last_pass,moving,last_synced,starts,halted)
44        VALUES(1,'none',NULL,NULL,0,NULL,0,0);";
45
46/// Whether a storage location is recorded on this device, and whether this
47/// device holds credentials for it (§19.1), with the location's provider.
48/// The database keeps the provider as the name sync gives it.
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub enum RecordedLocation<P> {
51    /// No location is recorded.
52    None,
53    /// A location is recorded, but this device holds no credentials for it.
54    SignedOut(P),
55    /// A location is recorded, and this device holds its credentials.
56    Connected(P),
57}
58
59impl<P> RecordedLocation<P> {
60    /// A location is recorded and this device holds its credentials.
61    pub fn is_connected(&self) -> bool {
62        matches!(self, Self::Connected(_))
63    }
64
65    /// The same location, with its provider as `name` gives it.
66    pub fn map<Q>(self, name: impl FnOnce(P) -> Q) -> RecordedLocation<Q> {
67        match self {
68            Self::None => RecordedLocation::None,
69            Self::SignedOut(provider) => RecordedLocation::SignedOut(name(provider)),
70            Self::Connected(provider) => RecordedLocation::Connected(name(provider)),
71        }
72    }
73}
74
75/// `coven_sync_status`'s row as it is read.
76#[derive(Clone, Debug, PartialEq, Eq)]
77pub struct SyncStatusRow {
78    /// Whether a location is recorded and its credentials held.
79    pub location: RecordedLocation<String>,
80    /// How the last pass ended, as sync encodes it; none until a pass ends
81    /// on this device.
82    pub last_pass: Option<Vec<u8>>,
83    /// Whether the running pass has read a slot it hadn't, or claimed one.
84    pub moving: bool,
85    /// When a pass last reached the end of the sequence, or setup, joining
86    /// or restoring finished, by the store's clock.
87    pub last_synced: Option<SystemTime>,
88}
89
90/// Milliseconds since the Unix epoch, rounded down so a stored finish never
91/// comes after the pass's own.
92fn millis(time: SystemTime) -> Result<i64, DbError> {
93    crate::stored_millis::since_epoch(time, crate::stored_millis::Round::Down).ok_or(
94        DbError::Invariant {
95            what: "a sync finishes between the Unix epoch and SQLite's last millisecond",
96        },
97    )
98}
99
100/// The `storage` and `provider` columns of `location`.
101fn columns(location: &RecordedLocation<String>) -> (&'static str, Option<&str>) {
102    match location {
103        RecordedLocation::None => ("none", None),
104        RecordedLocation::SignedOut(provider) => ("signed_out", Some(provider)),
105        RecordedLocation::Connected(provider) => ("connected", Some(provider)),
106    }
107}
108
109/// The row as it stands.
110pub(crate) fn read(db: &crate::sqlite::DatabaseConnection) -> Result<SyncStatusRow, DbError> {
111    let (storage, provider, last_pass, moving, last_synced) = db.query_row(
112        "SELECT storage,provider,last_pass,moving,last_synced FROM coven_sync_status
113         WHERE singleton=1",
114        [],
115        |row| {
116            Ok((
117                row.get::<_, String>(0)?,
118                row.get::<_, Option<String>>(1)?,
119                row.get(2)?,
120                row.get(3)?,
121                row.get::<_, Option<i64>>(4)?,
122            ))
123        },
124    )?;
125    let location = match (storage.as_str(), provider) {
126        ("none", None) => RecordedLocation::None,
127        ("signed_out", Some(provider)) => RecordedLocation::SignedOut(provider),
128        ("connected", Some(provider)) => RecordedLocation::Connected(provider),
129        _ => return Err(DbError::DamagedDatabase),
130    };
131    Ok(SyncStatusRow {
132        location,
133        last_pass,
134        moving,
135        last_synced: last_synced.map(crate::stored_millis::stored_time).transpose()?,
136    })
137}
138
139/// Write whether a location is recorded and its credentials held, and, when
140/// `finished` names one, that sync finished then. Opening also writes that
141/// no pass moves and that no halt holds. A row that already says this is
142/// left alone.
143pub(crate) fn save_location(
144    db: &crate::sqlite::DatabaseConnection,
145    location: &RecordedLocation<String>,
146    finished: Option<SystemTime>,
147    opening: bool,
148) -> Result<(), DbError> {
149    let (storage, provider) = columns(location);
150    let finished = finished.map(millis).transpose()?;
151    db.internal_execute(
152        "UPDATE coven_sync_status SET storage=?1,provider=?2,
153             moving=CASE WHEN ?4 THEN 0 ELSE moving END,
154             halted=CASE WHEN ?4 THEN 0 ELSE halted END,
155             last_synced=CASE WHEN ?3 IS NULL THEN last_synced
156                 ELSE max(coalesce(last_synced,?3),?3) END
157         WHERE singleton=1 AND (storage IS NOT ?1 OR provider IS NOT ?2
158             OR (?4 AND (moving=1 OR halted=1))
159             OR (?3 IS NOT NULL AND (last_synced IS NULL OR last_synced<?3)))",
160        rusqlite::params![storage, provider, finished, opening],
161    )?;
162    Ok(())
163}
164
165/// Write that the running pass moved a slot.
166pub(crate) fn save_moving(db: &crate::sqlite::DatabaseConnection) -> Result<(), DbError> {
167    db.internal_execute(
168        "UPDATE coven_sync_status SET moving=1 WHERE singleton=1 AND moving=0 AND halted=0",
169        [],
170    )?;
171    Ok(())
172}
173
174/// Write that sync started once more.
175pub(crate) fn save_start(db: &crate::sqlite::DatabaseConnection) -> Result<(), DbError> {
176    db.internal_execute("UPDATE coven_sync_status SET starts=starts+1 WHERE singleton=1", [])?;
177    Ok(())
178}
179
180/// Write how the last pass ended, as sync encodes it, when sync failed
181/// outside a pass's end, and that nothing moves: a loop that ends in a pass
182/// that moved a slot shows its failure, never syncing. Once a halt holds,
183/// it writes nothing: the halt's failure stands.
184pub(crate) fn save_last_pass(
185    db: &crate::sqlite::DatabaseConnection,
186    last_pass: &[u8],
187) -> Result<(), DbError> {
188    db.internal_execute(
189        "UPDATE coven_sync_status SET last_pass=?1,moving=0
190         WHERE singleton=1 AND halted=0 AND (last_pass IS NOT ?1 OR moving=1)",
191        [last_pass],
192    )?;
193    Ok(())
194}
195
196/// Write that a store-wide worker's panic halted sync until the store
197/// reopens, with its failure as how the last pass ended, and that nothing
198/// moves: from here no pass writes the row.
199pub(crate) fn save_halt(
200    db: &crate::sqlite::DatabaseConnection,
201    last_pass: &[u8],
202) -> Result<(), DbError> {
203    db.internal_execute(
204        "UPDATE coven_sync_status SET last_pass=?1,moving=0,halted=1 WHERE singleton=1",
205        [last_pass],
206    )?;
207    Ok(())
208}
209
210/// Write how the running pass ended and that it moves no more, and, when
211/// `finished` names one, that it reached the end of the sequence then. A
212/// pass that ends after a halt writes nothing: the halt's failure stands.
213pub(crate) fn save_pass_end(
214    db: &crate::sqlite::DatabaseConnection,
215    last_pass: &[u8],
216    finished: Option<SystemTime>,
217) -> Result<(), DbError> {
218    let finished = finished.map(millis).transpose()?;
219    db.internal_execute(
220        "UPDATE coven_sync_status SET last_pass=?1,moving=0,
221             last_synced=CASE WHEN ?2 IS NULL THEN last_synced
222                 ELSE max(coalesce(last_synced,?2),?2) END
223         WHERE singleton=1 AND halted=0 AND (last_pass IS NOT ?1 OR moving=1
224             OR (?2 IS NOT NULL AND (last_synced IS NULL OR last_synced<?2)))",
225        rusqlite::params![last_pass, finished],
226    )?;
227    Ok(())
228}
229
230#[cfg(test)]
231#[path = "internal_schema_sync_status_tests.rs"]
232mod tests;