1use crate::write_encoding::{counter, decoded, encoded};
11use crate::{sqlite::DatabaseConnection, DbError};
12use coven_format::merge_fields;
13use coven_foundation::id_source::DeviceId;
14use coven_merge::{Audience, RowId, RowState, Slot, WriteId};
15use rusqlite::params;
16use std::collections::{BTreeMap, BTreeSet};
17
18#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct Writers(BTreeMap<DeviceId, Option<u64>>);
24
25impl Writers {
26 pub fn new(devices: BTreeMap<DeviceId, Option<u64>>) -> Self {
28 Self(devices)
29 }
30
31 pub fn devices(&self) -> impl Iterator<Item = DeviceId> + '_ {
33 self.0.keys().copied()
34 }
35
36 pub fn through(&self, reads: &BTreeMap<DeviceId, u64>) -> u64 {
42 self.0
43 .iter()
44 .map(|(device, added)| {
45 let seen = added.map(|added| added.saturating_sub(1));
49 reads.get(device).copied().max(seen).unwrap_or(0)
50 })
51 .min()
52 .unwrap_or(u64::MAX)
53 }
54}
55
56fn current(state: &RowState<coven_format::value::Value>) -> BTreeSet<WriteId> {
59 state
60 .generations()
61 .get(&state.generation())
62 .copied()
63 .into_iter()
64 .chain(state.cells().values().map(|cell| cell.write))
65 .collect()
66}
67
68pub(crate) fn stopped(
72 db: &DatabaseConnection,
73 write: WriteId,
74 before: &RowState<coven_format::value::Value>,
75 after: &RowState<coven_format::value::Value>,
76 slot: Slot,
77) -> Result<(), DbError> {
78 let after = current(after);
79 for writer in current(before) {
80 if writer != write && !after.contains(&writer) {
81 stop(db, writer, before.row(), slot)?;
82 }
83 }
84 Ok(())
85}
86
87fn stop(db: &DatabaseConnection, writer: WriteId, row: &RowId, slot: Slot) -> Result<(), DbError> {
90 let Some((_, table)) = crate::write_sources::kept_part(db, writer, row)? else {
91 tracing::debug!(?writer, ?row, "a write stopped being current with no record held");
95 return Ok(());
96 };
97 stopped_at(db, writer, &row.audience, &table, &row.key, slot)
98}
99
100pub(crate) fn applied<'a>(
103 write: WriteId,
104 mut updates: impl Iterator<Item = &'a RowState<coven_format::value::Value>>,
105) -> bool {
106 updates.any(|state| current(state).contains(&write))
107}
108
109pub(crate) fn stopped_at(
112 db: &DatabaseConnection,
113 write: WriteId,
114 audience: &Audience,
115 table: &str,
116 key: &[u8],
117 slot: Slot,
118) -> Result<(), DbError> {
119 db.internal_execute(
120 "UPDATE _coven_authored_changes SET stop=?5 WHERE write_id=?1 AND audience=?2 AND table_name=?3 AND key=?4 AND stop IS NULL",
121 params![
122 encoded(merge_fields::encode_write_id(&write))?,
123 audience,
124 table,
125 key,
126 slot.get().to_be_bytes().as_slice()
127 ],
128 )?;
129 Ok(())
130}
131
132pub(crate) fn none_current(
136 db: &DatabaseConnection,
137 write: WriteId,
138 audiences: &[Audience],
139 slot: Slot,
140) -> Result<(), DbError> {
141 let id = encoded(merge_fields::encode_write_id(&write))?;
142 for audience in audiences {
143 db.internal_execute(
144 "UPDATE _coven_authored_changes SET stop=?3 WHERE write_id=?1 AND audience=?2 AND stop IS NULL",
145 params![id, audience, slot.get().to_be_bytes().as_slice()],
146 )?;
147 }
148 Ok(())
149}
150
151pub(crate) fn forget(db: &DatabaseConnection, writers: &Writers) -> Result<(), DbError> {
159 let mut reads = BTreeMap::new();
160 for device in writers.devices() {
161 if let Some(read) = crate::write_metadata::last_landed_read(db, device)? {
162 reads.insert(device, read);
163 }
164 }
165 let own = db
166 .local_device()
167 .ok_or(DbError::Invariant { what: "a writer connection observes this device" })?;
168 let seen = writers.through(&reads).min(crate::sequence::applied(db)?);
171 let through = match crate::write_metadata::queued_read(db, own)? {
172 Some(queued) => seen.min(queued),
173 None => seen,
174 };
175 let (from, newest) = forgotten_through(db)?;
176 if through <= from {
177 return crate::forget_entries::forget(db, through, newest);
178 }
179 let candidates = db.query(
180 "SELECT write_id,audience,table_name,key FROM _coven_authored_changes WHERE stop>?1 AND stop<=?2 ORDER BY write_id,audience",
181 params![from.to_be_bytes().as_slice(), through.to_be_bytes().as_slice()],
182 |r| {
183 Ok((
184 r.get::<_, Vec<u8>>(0)?,
185 crate::write_encoding::audience(r, 1)?,
186 r.get::<_, String>(2)?,
187 r.get::<_, Vec<u8>>(3)?,
188 ))
189 },
190 )?;
191 let mut parts = BTreeMap::<(Vec<u8>, Audience), Vec<(String, Vec<u8>)>>::new();
192 for (write, audience, table, key) in candidates {
193 parts.entry((write, audience)).or_default().push((table, key));
194 }
195 for ((write, audience), records) in parts {
196 forget_in_part(db, &write, &audience, records, through)?;
197 }
198 let newest = crate::forget_entries::passed(db, from, through, newest)?;
199 db.internal_execute(
200 "INSERT INTO _coven_forgotten(singleton,through,newest) VALUES(1,?1,?2)
201 ON CONFLICT(singleton) DO UPDATE SET through=excluded.through,newest=excluded.newest",
202 params![through.to_be_bytes().as_slice(), newest],
203 )?;
204 crate::forget_entries::forget(db, through, newest)
205}
206
207fn forget_in_part(
213 db: &DatabaseConnection,
214 write: &[u8],
215 audience: &Audience,
216 candidates: Vec<(String, Vec<u8>)>,
217 through: u64,
218) -> Result<(), DbError> {
219 let id = decoded(merge_fields::decode_write_id(write))?;
220 let header = db.authored_part(write, audience)?.ok_or(DbError::DamagedDatabase)?;
221 let version = merge_fields::decode_write_header(&header)?.schema_version;
222 let evidence = crate::authorship::keeps_evidence(db, id)?;
223 let protected = |table: &str, key: &[u8]| -> Result<bool, DbError> {
224 if !evidence {
225 return Ok(false);
226 }
227 let Some(table) = crate::source_names::table(db, version, table, db.schema_version()?)?
228 else {
229 return Ok(false);
230 };
231 let row = RowId { table, key: key.to_vec(), audience: audience.clone() };
232 Ok(crate::ownership::creation(db, &row)?.is_some_and(|creation| creation.write == id))
233 };
234 let leaves = crate::authorship::holds_leaves(db, id, audience)?;
235 let records = if leaves {
236 let mut records = Vec::new();
237 for (table, key, stop) in db.query(
238 "SELECT table_name,key,stop FROM _coven_authored_changes WHERE write_id=?1 AND audience=?2",
239 params![write, audience],
240 |r| {
241 Ok((
242 r.get::<_, String>(0)?,
243 r.get::<_, Vec<u8>>(1)?,
244 r.get::<_, Option<Vec<u8>>>(2)?.map(counter).transpose()?,
245 ))
246 },
247 )? {
248 if stop.is_none_or(|stop| stop > through) || protected(&table, &key)? {
249 return Ok(());
250 }
251 records.push((table, key));
252 }
253 records
254 } else {
255 let mut records = Vec::new();
256 for (table, key) in candidates {
257 if !protected(&table, &key)? {
258 records.push((table, key));
259 }
260 }
261 records
262 };
263 for (table, key) in records {
264 db.internal_execute(
265 "DELETE FROM _coven_authored_changes WHERE write_id=?1 AND audience=?2 AND table_name=?3 AND key=?4",
266 params![write, audience, table, key],
267 )?;
268 }
269 let emptied = db.internal_execute(
270 "DELETE FROM _coven_authored_parts WHERE write_id=?1 AND audience=?2
271 AND NOT EXISTS(SELECT 1 FROM _coven_authored_changes c WHERE c.write_id=?1 AND c.audience=?2)",
272 params![write, audience],
273 )?;
274 if emptied > 0 {
275 db.forget_authored_part(write, audience);
276 if leaves {
277 crate::authorship::forget_part(db, id, audience)?;
278 }
279 }
280 Ok(())
281}
282
283fn forgotten_through(db: &DatabaseConnection) -> Result<(u64, Option<i64>), DbError> {
287 let forgotten = db
288 .query("SELECT through,newest FROM _coven_forgotten WHERE singleton=1", [], |r| {
289 Ok((counter(r.get(0)?)?, r.get::<_, Option<i64>>(1)?))
290 })?
291 .pop();
292 Ok(forgotten.unwrap_or((0, None)))
293}
294
295pub(crate) fn look_again(db: &DatabaseConnection) -> Result<(), DbError> {
301 db.internal_execute("DELETE FROM _coven_forgotten", [])?;
302 Ok(())
303}
304
305#[cfg(test)]
306#[path = "forget_records_tests.rs"]
307pub(crate) mod tests;