1use crate::{sqlite::DatabaseConnection, DbError, OperationId};
4use coven_foundation::id_source::{DeviceId, FileId, ObjectId};
5use serde::{de::DeserializeOwned, Serialize};
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum FileWork {
10 Upload,
12 Download,
16}
17
18pub trait PendingValue: Serialize + DeserializeOwned + Send + 'static {
21 fn writes_refused(
23 slot: coven_merge::Slot,
24 cutoff: coven_merge::Slot,
25 cause: crate::WriteCutoff,
26 ) -> Self;
27 fn reload(audience: coven_merge::Audience) -> Self;
29 fn raise_wait(
32 write: coven_merge::WriteId,
33 audience: coven_merge::Audience,
34 version: u32,
35 ) -> Self;
36 fn raise_awaited(&self) -> Option<(coven_merge::WriteId, &coven_merge::Audience, u32)>;
40 fn provider_access(operation: OperationId, account: String, grant: bool) -> Self;
42 type Subject: Serialize + Ord;
44 fn subject(&self) -> &Self::Subject;
46 fn operation(&self) -> Option<OperationId>;
48 fn object(&self) -> Option<(FileWork, DeviceId, ObjectId)>;
52 fn move_of(&self) -> Option<crate::MoveId>;
54 fn pin_of(&self) -> Option<FileId>;
57 fn slot(&self) -> Option<coven_merge::Slot>;
59 fn ends_when_covered(&self) -> bool;
63 const BUILD: u128;
67 fn can_retry(&self, recorded_build: u128, schema_version: u32) -> bool;
70 fn cause(&self) -> Option<&str>;
74 fn with_cause(self, cause: Option<String>) -> Self;
76}
77
78#[derive(Clone, Debug, PartialEq, Eq)]
81pub(crate) struct SubjectKey(Vec<u8>);
82
83impl SubjectKey {
84 pub(crate) fn of<S: Serialize>(subject: &S) -> Result<Self, DbError> {
85 serde_json::to_vec(subject).map(Self).map_err(DbError::StateEncoding)
86 }
87}
88
89impl rusqlite::ToSql for SubjectKey {
90 fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
91 self.0.to_sql()
92 }
93}
94
95impl rusqlite::types::FromSql for SubjectKey {
96 fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
97 Vec::column_result(value).map(Self)
98 }
99}
100
101const SELECT: &str =
102 "SELECT subject,value,operation,slot_key,work,device,object,build,move,pin,cause,id,raise_write FROM _coven_pending";
103
104struct StoredRecord {
105 position: i64,
107 subject: SubjectKey,
108 value: Vec<u8>,
109 operation: Option<i64>,
110 slot_key: Option<Vec<u8>>,
111 object: Option<(String, DeviceId, ObjectId)>,
112 build: u128,
114 moved: Option<i64>,
115 pin: Option<String>,
116 raise_write: Option<Vec<u8>>,
117 cause: Option<String>,
118}
119impl StoredRecord {
120 fn read(row: &rusqlite::Row<'_>) -> rusqlite::Result<Self> {
121 Ok(Self {
122 subject: row.get(0)?,
123 value: row.get(1)?,
124 operation: row.get(2)?,
125 slot_key: row.get(3)?,
126 object: match (
127 row.get::<_, Option<String>>(4)?,
128 row.get::<_, Option<DeviceId>>(5)?,
129 row.get::<_, Option<ObjectId>>(6)?,
130 ) {
131 (Some(work), Some(device), Some(object)) => Some((work, device, object)),
132 (None, None, None) => None,
133 _ => return Err(crate::write_encoding::damaged(4, rusqlite::types::Type::Text)),
134 },
135 build: u128::from_be_bytes(row.get(7)?),
136 moved: row.get(8)?,
137 pin: row.get(9)?,
138 cause: row.get(10)?,
139 position: row.get(11)?,
140 raise_write: row.get(12)?,
141 })
142 }
143 fn decode<P: PendingValue>(&self) -> Result<P, DbError> {
144 let record: P = serde_json::from_slice(&self.value).map_err(DbError::StateEncoding)?;
145 if SubjectKey::of(record.subject())? != self.subject
146 || record.operation().map(|id| id.0) != self.operation
147 || record.move_of().map(|id| id.0) != self.moved
148 || record.pin_of().map(|file| file.to_string()) != self.pin
149 || stored_raise_write(&record) != self.raise_write
150 || stored_slot_key(&record) != self.slot_key
151 || stored_object(&record)
152 != self
153 .object
154 .as_ref()
155 .map(|(work, device, object)| (work.as_str(), *device, *object))
156 {
157 return Err(DbError::DamagedDatabase);
158 }
159 Ok(record.with_cause(self.cause.clone()))
160 }
161}
162
163pub(crate) fn read<P: PendingValue>(db: &DatabaseConnection) -> Result<Vec<P>, DbError> {
166 Ok(positioned(db)?.into_iter().map(|(_, record)| record).collect())
167}
168
169pub(crate) fn positioned<P: PendingValue>(
172 db: &DatabaseConnection,
173) -> Result<Vec<(i64, P)>, DbError> {
174 db.query(&format!("{SELECT} ORDER BY id"), [], StoredRecord::read)?
175 .iter()
176 .map(|stored| Ok((stored.position, stored.decode::<P>()?)))
177 .collect()
178}
179
180pub(crate) fn save<P: PendingValue>(db: &DatabaseConnection, record: &P) -> Result<(), DbError> {
181 db.pending_change(|| {
182 let subject = SubjectKey::of(record.subject())?;
183 let value = serde_json::to_vec(record).map_err(DbError::StateEncoding)?;
184 let (work, device, object) = match stored_object(record) {
185 Some((work, device, object)) => (Some(work), Some(device), Some(object)),
186 None => (None, None, None),
187 };
188 db.internal_execute(
192 "INSERT INTO _coven_pending(subject,value,operation,slot_key,work,device,object,build,move,pin,cause,raise_write)
193 SELECT ?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12
194 WHERE ?5 IS NOT 'download' OR EXISTS(SELECT 1 FROM _coven_stored_files AS s WHERE s.device=?6 AND s.object=?7)
195 ON CONFLICT(subject) DO UPDATE SET value=excluded.value,operation=excluded.operation,slot_key=excluded.slot_key,work=excluded.work,device=excluded.device,object=excluded.object,build=excluded.build,move=excluded.move,pin=excluded.pin,cause=excluded.cause,raise_write=excluded.raise_write
196 WHERE value!=excluded.value OR build!=excluded.build OR cause IS NOT excluded.cause",
197 rusqlite::params![subject, value, record.operation().map(|id| id.0), stored_slot_key(record), work, device, object, P::BUILD.to_be_bytes(), record.move_of(), record.pin_of(), record.cause(), stored_raise_write(record)],
198 )?;
199 Ok(())
200 })
201}
202
203pub(crate) fn forget_causes(db: &DatabaseConnection) -> Result<(), DbError> {
207 db.internal_execute("UPDATE _coven_pending SET cause=NULL WHERE cause IS NOT NULL", [])?;
208 Ok(())
209}
210
211pub(crate) fn clear<S: Serialize>(db: &DatabaseConnection, subject: &S) -> Result<(), DbError> {
212 clear_key(db, &SubjectKey::of(subject)?)
213}
214
215pub(crate) fn clear_key(db: &DatabaseConnection, subject: &SubjectKey) -> Result<(), DbError> {
217 db.pending_change(|| {
218 db.internal_execute("DELETE FROM _coven_pending WHERE subject=?1", [subject])?;
219 Ok(())
220 })
221}
222
223pub(crate) fn clear_move(db: &DatabaseConnection, id: crate::MoveId) -> Result<(), DbError> {
225 db.pending_change(|| {
226 db.internal_execute("DELETE FROM _coven_pending WHERE move=?1", [id])?;
227 Ok(())
228 })
229}
230
231pub(crate) fn clear_operation(db: &DatabaseConnection, id: OperationId) -> Result<(), DbError> {
234 crate::retry::clear(db, crate::RetrySubject::Operation(id))?;
235 db.pending_change(|| {
236 db.internal_execute("DELETE FROM _coven_pending WHERE operation=?1", [id.0])?;
237 Ok(())
238 })
239}
240
241pub(crate) fn operation<P: PendingValue>(
242 db: &DatabaseConnection,
243 id: OperationId,
244) -> Result<Option<P>, DbError> {
245 db.query(&format!("{SELECT} WHERE operation=?1"), [id.0], StoredRecord::read)?
246 .pop()
247 .map(|r| r.decode())
248 .transpose()
249}
250
251pub(crate) fn recorded<S: Serialize>(
253 db: &DatabaseConnection,
254 subject: &S,
255) -> Result<bool, DbError> {
256 db.query_row(
257 "SELECT EXISTS(SELECT 1 FROM _coven_pending WHERE subject=?1)",
258 [SubjectKey::of(subject)?],
259 |r| r.get(0),
260 )
261}
262
263fn stored<S: Serialize>(
265 db: &DatabaseConnection,
266 subject: &S,
267) -> Result<Option<StoredRecord>, DbError> {
268 Ok(db
269 .query(
270 &format!("{SELECT} WHERE subject=?1"),
271 [SubjectKey::of(subject)?],
272 StoredRecord::read,
273 )?
274 .pop())
275}
276
277pub(crate) fn find<P: PendingValue>(
279 db: &DatabaseConnection,
280 subject: &P::Subject,
281) -> Result<Option<P>, DbError> {
282 stored(db, subject)?.map(|stored| stored.decode()).transpose()
283}
284
285pub(crate) fn retry<P: PendingValue>(
286 db: &DatabaseConnection,
287 subject: &P::Subject,
288) -> Result<bool, DbError> {
289 match stored(db, subject)? {
290 None => Ok(true),
291 Some(stored) => Ok(stored.decode::<P>()?.can_retry(stored.build, db.schema_version()?)),
292 }
293}
294
295#[cfg(any(test, feature = "test-utils"))]
298pub(crate) fn set_build(db: &DatabaseConnection, build: u128) -> Result<(), DbError> {
299 db.pending_change(|| {
300 db.internal_execute("UPDATE _coven_pending SET build=?1", [build.to_be_bytes()])?;
301 Ok(())
302 })
303}
304
305fn slot_key(slot: coven_merge::Slot, covered: bool) -> Vec<u8> {
306 let mut key = vec![u8::from(covered)];
307 key.extend(slot.get().to_be_bytes());
308 key
309}
310const COVERED_SLOT: u8 = 1;
311
312fn stored_object<P: PendingValue>(record: &P) -> Option<(&'static str, DeviceId, ObjectId)> {
314 record.object().map(|(work, device, object)| {
315 let work = match work {
316 FileWork::Upload => "upload",
317 FileWork::Download => "download",
318 };
319 (work, device, object)
320 })
321}
322
323fn stored_slot_key<P: PendingValue>(record: &P) -> Option<Vec<u8>> {
324 Some(slot_key(record.slot()?, record.ends_when_covered()))
325}
326
327fn stored_raise_write<P: PendingValue>(record: &P) -> Option<Vec<u8>> {
330 record.raise_awaited().map(|(write, _, _)| write.number.to_be_bytes().to_vec())
331}
332
333pub(crate) fn end_captured_raise_waits(
337 db: &DatabaseConnection,
338 through: u64,
339) -> Result<(), DbError> {
340 db.pending_change(|| {
341 db.internal_execute(
342 "DELETE FROM _coven_pending WHERE raise_write IS NOT NULL AND raise_write<=?1",
343 [through.to_be_bytes().as_slice()],
344 )?;
345 Ok(())
346 })
347}
348
349pub(crate) fn reset(db: &DatabaseConnection) -> Result<(), DbError> {
351 db.pending_change(|| {
352 db.internal_execute("DELETE FROM _coven_pending AS p WHERE NOT EXISTS(SELECT 1 FROM _coven_provider_access a WHERE a.operation=p.operation)",[])?;
353 Ok(())
354 })
355}
356
357pub(crate) fn clear_slot(db: &DatabaseConnection, slot: coven_merge::Slot) -> Result<(), DbError> {
359 db.pending_change(|| {
360 for covered in [false, true] {
361 db.internal_execute(
362 "DELETE FROM _coven_pending WHERE slot_key=?1",
363 [slot_key(slot, covered)],
364 )?;
365 }
366 Ok(())
367 })
368}
369
370pub(crate) fn clear_covered_slots(db: &DatabaseConnection) -> Result<(), DbError> {
373 let waiting = db.query(
374 "SELECT slot_key FROM _coven_pending WHERE slot_key>=?1 AND slot_key<?2",
375 ([COVERED_SLOT].as_slice(), [COVERED_SLOT + 1].as_slice()),
376 |row| row.get::<_, Vec<u8>>(0),
377 )?;
378 if waiting.is_empty() {
379 return Ok(());
380 }
381 let applied = crate::sequence::applied(db)?;
382 db.pending_change(|| {
383 for key in waiting {
384 let slot = u64::from_be_bytes(key[1..].try_into().expect("eight-byte slot"));
385 if slot <= applied {
386 db.internal_execute("DELETE FROM _coven_pending WHERE slot_key=?1", [key])?;
387 }
388 }
389 Ok(())
390 })
391}
392
393#[cfg(test)]
394#[path = "pending_tests.rs"]
395pub(crate) mod tests;