1use crate::write_encoding::{audience, counter};
7use crate::{sqlite::DatabaseConnection, write_schema::WriteSchema, DbError};
8use coven_format::{admin::SnapshotId, snapshot::SnapshotHeader};
9use coven_foundation::id_source::DeviceId;
10use coven_merge::{Audience, WriteId};
11use rusqlite::params;
12
13#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct RaiseCapture {
16 pub audience: Audience,
18 pub version: u32,
20 pub through: WriteId,
22 pub covers: u64,
24}
25
26pub(crate) fn capture(
27 db: &DatabaseConnection,
28 _schema: &WriteSchema,
29 marker: WriteId,
30 version: u32,
31) -> Result<bool, DbError> {
32 let state = crate::store_state_tables::read(db)?;
33 db.internal_execute("DELETE FROM _coven_raise_captures", [])?;
34 let Some(member) = state
35 .store
36 .as_ref()
37 .and_then(|_| state.devices.get(&marker.device))
38 .map(|device| device.member)
39 else {
40 return Ok(false);
41 };
42 for selected in state.readable_audiences(&member) {
43 db.internal_execute("INSERT INTO _coven_raise_captures(audience,version,through,covers) VALUES(?1,?2,?3,?4)",params![&selected,version,marker.number.to_be_bytes(),crate::sequence::applied(db)?.to_be_bytes()])?;
44 }
45 crate::pending::end_captured_raise_waits(db, marker.number)?;
46 Ok(true)
47}
48
49pub(crate) fn capture_created(
55 db: &DatabaseConnection,
56 schema: &WriteSchema,
57) -> Result<(), DbError> {
58 if breaking_version(db)? == 0 || !provisional(db)?.is_empty() {
59 return Ok(());
60 }
61 let device = db
62 .local_device()
63 .ok_or(DbError::Invariant { what: "a writer connection observes this device" })?;
64 let queued = db.query(
65 "SELECT number FROM _coven_uploads WHERE device=?1 ORDER BY number DESC",
66 [device],
67 |r| counter(r.get::<_, Vec<u8>>(0)?),
68 )?;
69 for number in queued {
70 let marker = WriteId { device, number };
71 let header = crate::upload::queued(db, marker)?
72 .ok_or(DbError::Invariant { what: "a queued write has its header" })?
73 .header;
74 if header.disposition == coven_format::write::WriteDisposition::Migration {
75 let version = db.schema_version()?;
76 if capture(db, schema, marker, version)? {
77 publish_at(db, version)?;
78 }
79 return Ok(());
80 }
81 }
82 Ok(())
83}
84
85pub(crate) fn capture_made(
94 db: &DatabaseConnection,
95 circle: coven_foundation::id_source::CircleId,
96 state: &crate::StoreState,
97 operation: Option<&crate::migration::MigrationOperation>,
98) -> Result<(), DbError> {
99 let audience = Audience::Circle(circle);
100 let device = db
101 .local_device()
102 .ok_or(DbError::Invariant { what: "a writer connection observes this device" })?;
103 let made = state
104 .devices
105 .get(&device)
106 .is_some_and(|local| local.active() && state.reads_circle(circle, &local.member));
107 if !made || state.reached(&audience) >= breaking_version(db)? {
108 return Ok(());
109 }
110 let through = match captured_through(db)? {
113 Some(through) => through,
114 None => crate::write_metadata::newest_number(db, device)?.unwrap_or(0),
115 };
116 db.internal_execute(
117 "INSERT INTO _coven_raise_captures(audience,version,through,covers) VALUES(?1,?2,?3,?4)",
118 params![
119 &audience,
120 db.schema_version()?,
121 through.to_be_bytes(),
122 crate::sequence::applied(db)?.to_be_bytes()
123 ],
124 )?;
125 crate::pending::end_captured_raise_waits(db, through)?;
126 if let Some(operation) = operation {
129 if crate::operation::of_kind(db, crate::operation_kind::SCHEMA_RAISE)?.is_empty() {
130 journal(db, operation, db.schema_version()?)?;
131 }
132 }
133 Ok(())
134}
135
136pub(crate) fn journal(
139 db: &DatabaseConnection,
140 operation: &crate::migration::MigrationOperation,
141 version: u32,
142) -> Result<(), DbError> {
143 let new = operation(version)?;
144 let id = crate::operation::insert(db, &new)?;
145 crate::operation::advance(
146 db,
147 &crate::OperationUpdate { id, previous: 0, last_step: 1, data: new.data },
148 )
149}
150
151pub(crate) fn raise_to(
152 db: &DatabaseConnection,
153 _tables: &[crate::SyncedTable],
154 device: DeviceId,
155 version: u32,
156) -> Result<bool, DbError> {
157 if captures(db, device)?.is_empty() {
158 return Ok(false);
159 }
160 let newest = crate::write_metadata::newest_number(db, device)?
162 .ok_or(DbError::Invariant { what: "a raise capture's device has a write" })?;
163 db.internal_execute(
164 "UPDATE _coven_raise_captures SET version=?1,through=?2,covers=?3",
165 params![version, newest.to_be_bytes(), crate::sequence::applied(db)?.to_be_bytes()],
166 )?;
167 crate::pending::end_captured_raise_waits(db, newest)?;
168 Ok(true)
169}
170
171pub(crate) fn prepare(
172 db: &DatabaseConnection,
173 _schema: &WriteSchema,
174 device: DeviceId,
175 selected: &Audience,
176 version: u32,
177) -> Result<Option<RaiseCapture>, DbError> {
178 db.internal_execute(
179 "UPDATE _coven_raise_captures SET covers=?1 WHERE audience=?2 AND version=?3",
180 params![crate::sequence::applied(db)?.to_be_bytes(), selected, version],
181 )?;
182 Ok(captures(db, device)?
183 .into_iter()
184 .find(|capture| capture.audience == *selected && capture.version == version))
185}
186
187pub(crate) fn first_ahead(
195 db: &DatabaseConnection,
196 device: DeviceId,
197) -> Result<Option<(u64, Audience)>, DbError> {
198 let breaking = breaking_version(db)?;
199 if breaking == 0 || captured_through(db)?.is_some() {
200 return Ok(None);
201 }
202 let state = crate::store_state_tables::read(db)?;
203 let below = |audience: &Audience| state.awaits_raise(audience, breaking);
204 if !below(&Audience::Store) && !state.circles.keys().any(|c| below(&Audience::Circle(*c))) {
205 return Ok(None);
206 }
207 let queued = db.query(
208 "SELECT number FROM _coven_uploads WHERE device=?1 ORDER BY number",
209 [device],
210 |r| counter(r.get::<_, Vec<u8>>(0)?),
211 )?;
212 for number in queued {
213 let write = crate::upload::queued(db, WriteId { device, number })?
214 .ok_or(DbError::Invariant { what: "a queued write has its header" })?;
215 if write.header.schema_version >= breaking {
216 if let Some(part) = write.parts.iter().find(|part| below(&part.audience)) {
217 return Ok(Some((number, part.audience.clone())));
218 }
219 }
220 }
221 Ok(None)
222}
223
224pub(crate) fn record_ahead<P: crate::PendingValue>(
227 db: &DatabaseConnection,
228 device: DeviceId,
229) -> Result<(), DbError> {
230 if let Some((number, audience)) = first_ahead(db, device)? {
231 let write = WriteId { device, number };
232 crate::pending::save(db, &P::raise_wait(write, audience, breaking_version(db)?))?;
233 }
234 Ok(())
235}
236
237pub(crate) fn end_raise_waits<P: crate::PendingValue>(
241 db: &DatabaseConnection,
242) -> Result<(), DbError> {
243 let state = crate::store_state_tables::read(db)?;
244 for record in crate::pending::read::<P>(db)? {
245 if let Some((write, audience, version)) = record.raise_awaited() {
246 if !state.awaits_raise(audience, version) {
247 crate::pending::clear(db, record.subject())?;
248 record_ahead::<P>(db, write.device)?;
249 }
250 }
251 }
252 Ok(())
253}
254
255pub(crate) fn captured_through(db: &DatabaseConnection) -> Result<Option<u64>, DbError> {
256 db.query_row("SELECT max(through) FROM _coven_raise_captures", [], |r| {
257 r.get::<_, Option<Vec<u8>>>(0)?.map(counter).transpose()
258 })
259}
260
261pub(crate) fn provisional(db: &DatabaseConnection) -> Result<Vec<(Audience, u32, u64)>, DbError> {
262 db.query(
263 "SELECT audience,version,covers FROM _coven_raise_captures ORDER BY audience",
264 [],
265 |r| Ok((audience(r, 0)?, r.get(1)?, counter(r.get(2)?)?)),
266 )
267}
268
269pub(crate) fn captures(
270 db: &DatabaseConnection,
271 device: DeviceId,
272) -> Result<Vec<RaiseCapture>, DbError> {
273 db.query(
274 "SELECT audience,version,through,covers FROM _coven_raise_captures ORDER BY audience",
275 [],
276 |r| {
277 Ok(RaiseCapture {
278 audience: audience(r, 0)?,
279 version: r.get(1)?,
280 through: WriteId { device, number: counter(r.get(2)?)? },
281 covers: counter(r.get(3)?)?,
282 })
283 },
284 )
285}
286
287pub(crate) fn write<E>(
288 db: &DatabaseConnection,
289 schema: &WriteSchema,
290 id: SnapshotId,
291 version: u32,
292 begin: impl FnOnce(&SnapshotHeader) -> Result<(), E>,
293 emit: impl FnMut(Vec<u8>) -> Result<(), E>,
294) -> Result<u32, crate::SnapshotWriteError<E>> {
295 let capture = captures(db, id.device)?
296 .into_iter()
297 .find(|capture| capture.audience == id.audience && capture.version == version)
298 .ok_or(DbError::DamagedDatabase)?;
299 if crate::upload::unconfirmed(db, capture.through)?.is_some() {
300 return Err(DbError::ClaimUnsettled.into());
301 }
302 if capture.covers != crate::sequence::applied(db)? {
303 return Err(DbError::CaptureChanged.into());
304 }
305 crate::snapshot_write::write(db, schema, id, begin, emit)
306}
307
308pub(crate) fn breaking_version(db: &DatabaseConnection) -> Result<u32, DbError> {
312 db.query_row("SELECT minimum FROM _coven_snapshot_schema WHERE singleton=1", [], |r| r.get(0))
313}
314
315pub(crate) fn require(db: &DatabaseConnection, minimum: u32) -> Result<(), DbError> {
318 db.internal_execute(
319 "UPDATE _coven_snapshot_schema SET minimum=?1 WHERE singleton=1",
320 [minimum],
321 )?;
322 Ok(())
323}
324
325pub(crate) fn publish_at(db: &DatabaseConnection, version: u32) -> Result<(), DbError> {
328 db.internal_execute(
329 "UPDATE _coven_snapshot_schema SET publication=?1 WHERE singleton=1",
330 [version],
331 )?;
332 Ok(())
333}
334
335pub(crate) fn loaded(
338 db: &DatabaseConnection,
339 audience: &Audience,
340 version: u32,
341) -> Result<(), DbError> {
342 db.internal_execute(
343 "DELETE FROM _coven_raise_captures WHERE audience=?1 AND version<=?2",
344 params![audience, version],
345 )?;
346 Ok(())
347}
348
349pub(crate) fn discard(
350 db: &DatabaseConnection,
351 audience: &Audience,
352 version: u32,
353) -> Result<(), DbError> {
354 db.internal_execute(
355 "DELETE FROM _coven_raise_captures WHERE audience=?1 AND version=?2",
356 params![audience, version],
357 )?;
358 Ok(())
359}
360
361#[cfg(test)]
362#[path = "raise_capture_tests.rs"]
363pub(crate) mod tests;