1use crate::DbError;
26use std::time::SystemTime;
27
28pub(crate) const SYNC_STATUS: &str = "coven_sync_status";
30
31pub(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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub enum RecordedLocation<P> {
51 None,
53 SignedOut(P),
55 Connected(P),
57}
58
59impl<P> RecordedLocation<P> {
60 pub fn is_connected(&self) -> bool {
62 matches!(self, Self::Connected(_))
63 }
64
65 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#[derive(Clone, Debug, PartialEq, Eq)]
77pub struct SyncStatusRow {
78 pub location: RecordedLocation<String>,
80 pub last_pass: Option<Vec<u8>>,
83 pub moving: bool,
85 pub last_synced: Option<SystemTime>,
88}
89
90fn 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
100fn 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
109pub(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
139pub(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
165pub(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
174pub(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
180pub(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
196pub(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
210pub(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;