1use crate::sqlite::DatabaseConnection;
3use crate::write_encoding::{audience, counter, damaged};
4use crate::{DbError, StoreState, StoreVersion};
5use coven_format::{
6 admin::SnapshotId, snapshot_rows::ChangeVerdict, value::Slot, write::WriteHeader,
7};
8use coven_foundation::id_source::DeviceId;
9use coven_merge::Audience;
10use rusqlite::params;
11use std::collections::BTreeSet;
12
13#[derive(Clone, Debug, PartialEq, Eq)]
16pub enum Boundary {
17 SchemaChange(StoreVersion),
19 Reset {
21 entry: Slot,
23 snapshot: SnapshotId,
25 },
26}
27
28impl Boundary {
29 pub fn entry(&self) -> Slot {
31 match self {
32 Self::SchemaChange(version) => version.entry,
33 Self::Reset { entry, .. } => *entry,
34 }
35 }
36
37 pub fn snapshot(&self) -> &SnapshotId {
39 match self {
40 Self::SchemaChange(version) => &version.snapshot,
41 Self::Reset { snapshot, .. } => snapshot,
42 }
43 }
44
45 pub fn schema_in_place(state: &StoreState, version: u32, snapshot: &SnapshotId) -> bool {
48 version <= state.reached(&snapshot.audience)
49 }
50
51 pub fn reset_in_place(state: &StoreState, snapshot: &SnapshotId) -> bool {
53 state.resets.get(&snapshot.audience) == Some(snapshot)
54 }
55
56 pub fn already_in_place(&self, state: &StoreState) -> bool {
58 match self {
59 Self::SchemaChange(version) => {
60 Self::schema_in_place(state, version.number, &version.snapshot)
61 }
62 Self::Reset { snapshot, .. } => Self::reset_in_place(state, snapshot),
63 }
64 }
65
66 pub fn apply(&self, state: &mut StoreState) {
68 match self {
69 Self::SchemaChange(version) => {
70 state.schema.insert(version.snapshot.audience.clone(), version.clone());
71 }
72 Self::Reset { snapshot, .. } => {
73 state.resets.insert(snapshot.audience.clone(), snapshot.clone());
74 }
75 }
76 }
77
78 pub(crate) fn load_selected(database: &DatabaseConnection) -> Result<Vec<Self>, DbError> {
79 Ok(retained(database)?.into_iter().map(|row| row.boundary).collect())
80 }
81}
82
83#[derive(Clone, Debug, PartialEq, Eq)]
85pub struct BoundaryCoverage {
86 pub boundary: Boundary,
88 pub included: u64,
90}
91
92pub(crate) fn record(database: &DatabaseConnection, boundary: &Boundary) -> Result<(), DbError> {
94 let (kind, version) = match boundary {
95 Boundary::SchemaChange(version) => ("schema", Some(version.number)),
96 Boundary::Reset { .. } => ("reset", None),
97 };
98 let entry = boundary.entry();
99 let snapshot = boundary.snapshot();
100 database.internal_execute(
101 "INSERT INTO _coven_boundaries(slot,kind,version,audience,snapshot_device,snapshot_number)
102 VALUES(?1,?2,?3,?4,?5,?6) ON CONFLICT(slot) DO NOTHING",
103 params![
104 entry.get().to_be_bytes().as_slice(),
105 kind,
106 version,
107 &snapshot.audience,
108 snapshot.device,
109 snapshot.number.to_be_bytes().as_slice()
110 ],
111 )?;
112 Ok(())
113}
114
115pub(crate) fn record_coverage(
119 database: &DatabaseConnection,
120 coverage: &BoundaryCoverage,
121) -> Result<bool, DbError> {
122 record(database, &coverage.boundary)?;
123 let previous: (Option<Vec<u8>>, bool) = database.query_row(
124 "SELECT included,applied FROM _coven_boundaries WHERE slot=?1",
125 [coverage.boundary.entry().get().to_be_bytes().as_slice()],
126 |row| Ok((row.get(0)?, row.get(1)?)),
127 )?;
128 let included = coverage.included.to_be_bytes().to_vec();
129 if let Some(checked) = previous.0 {
130 if checked != included {
131 return Err(crate::snapshot_error::invalid("checked boundary coverage changed"));
132 }
133 }
134 if previous.1 {
135 return Ok(false);
136 }
137 database.internal_execute(
138 "UPDATE _coven_boundaries SET included=?2,applied=1 WHERE slot=?1",
139 params![coverage.boundary.entry().get().to_be_bytes().as_slice(), included],
140 )?;
141 Ok(true)
142}
143
144pub(crate) fn replace(
148 database: &DatabaseConnection,
149 boundaries: Vec<BoundaryCoverage>,
150) -> Result<Vec<Slot>, DbError> {
151 let mut ids = BTreeSet::new();
152 for boundary in &boundaries {
153 if !ids.insert(boundary.boundary.entry()) {
154 return Err(crate::snapshot_error::invalid("reload repeats a boundary slot"));
155 }
156 }
157 let previous: BTreeSet<_> = database
158 .query("SELECT slot FROM _coven_boundaries WHERE applied=1 AND kind='reset'", [], |row| {
159 crate::store_state_tables::slot(row, 0)
160 })?
161 .into_iter()
162 .collect();
163 let changed_resets = boundaries
164 .iter()
165 .filter_map(|coverage| match &coverage.boundary {
166 Boundary::Reset { entry, .. } if ids.contains(entry) && !previous.contains(entry) => {
167 Some(*entry)
168 }
169 _ => None,
170 })
171 .collect();
172 database.internal_execute("DELETE FROM _coven_boundaries", [])?;
173 for boundary in &boundaries {
174 record_coverage(database, boundary)?;
175 }
176 Ok(changed_resets)
177}
178
179struct RetainedBoundary {
180 boundary: Boundary,
181 included: Option<u64>,
182 applied: bool,
183}
184
185fn retained(database: &DatabaseConnection) -> Result<Vec<RetainedBoundary>, DbError> {
186 database.query(
187 "SELECT b.kind,b.version,b.audience,b.snapshot_device,b.snapshot_number,b.slot,b.included,b.applied
188 FROM _coven_boundaries b ORDER BY b.slot",
189 [],
190 |row| {
191 let snapshot = SnapshotId {
192 audience: audience(row, 2)?,
193 device: row.get::<_, DeviceId>(3)?,
194 number: counter(row.get(4)?)?,
195 };
196 let entry = crate::store_state_tables::slot(row, 5)?;
197 let boundary = match (row.get::<_, String>(0)?.as_str(), row.get::<_, Option<u32>>(1)?) {
198 ("schema", Some(number)) => Boundary::SchemaChange(StoreVersion { number, snapshot, entry }),
199 ("reset", None) => Boundary::Reset { entry, snapshot },
200 _ => return Err(damaged(0, rusqlite::types::Type::Text)),
201 };
202 let included = row.get::<_, Option<Vec<u8>>>(6)?.map(counter).transpose()?;
203 Ok(RetainedBoundary { boundary, included, applied: row.get(7)? })
204 },
205 )
206}
207
208#[derive(Clone, Debug, PartialEq, Eq)]
210pub(crate) enum BoundaryRule {
211 Raise(u32),
214 Reset(Slot),
216}
217
218pub(crate) struct WriteBoundary {
220 pub(crate) audience: Audience,
221 pub(crate) rule: BoundaryRule,
222 pub(crate) included: u64,
223}
224
225impl WriteBoundary {
226 pub(crate) fn load(database: &DatabaseConnection) -> Result<Vec<Self>, DbError> {
227 let mut state = StoreState::default();
228 let mut result = Vec::new();
229 for row in retained(database)? {
230 if row.boundary.already_in_place(&state) {
231 continue;
232 }
233 row.boundary.apply(&mut state);
234 if row.applied {
235 result.push(Self {
236 audience: row.boundary.snapshot().audience.clone(),
237 rule: match &row.boundary {
238 Boundary::SchemaChange(version) => BoundaryRule::Raise(version.number),
239 Boundary::Reset { entry, .. } => BoundaryRule::Reset(*entry),
240 },
241 included: row.included.ok_or(DbError::DamagedDatabase)?,
242 });
243 }
244 }
245 Ok(result)
246 }
247
248 pub(crate) fn judging(database: &DatabaseConnection) -> Result<Vec<Self>, DbError> {
254 let mut boundaries = Self::load(database)?;
255 for (audience, version, included) in crate::raise_capture::provisional(database)? {
256 let realized = boundaries.iter().any(|b| {
257 b.audience == audience
258 && matches!(b.rule, BoundaryRule::Raise(number) if number >= version)
259 });
260 if !realized {
261 boundaries.push(Self { audience, rule: BoundaryRule::Raise(version), included });
262 }
263 }
264 Ok(boundaries)
265 }
266
267 pub(crate) fn excludes(
272 &self,
273 header: &WriteHeader,
274 audience: &Audience,
275 slot: Option<Slot>,
276 ) -> Option<ChangeVerdict> {
277 if *audience != self.audience || slot.is_some_and(|slot| slot.get() <= self.included) {
278 return None;
279 }
280 match &self.rule {
281 BoundaryRule::Raise(version) => {
282 (header.schema_version < *version).then_some(ChangeVerdict::Excluded(*version))
283 }
284 BoundaryRule::Reset(entry) => {
285 (header.read_through < entry.get()).then_some(ChangeVerdict::Ignored(*entry))
286 }
287 }
288 }
289
290 pub(crate) fn ignore(
293 boundaries: &[Self],
294 header: &WriteHeader,
295 audience: &Audience,
296 slot: Option<Slot>,
297 ) -> bool {
298 matches!(
299 Self::part_verdict(boundaries, header, audience, slot),
300 Some(ChangeVerdict::Ignored(_))
301 )
302 }
303
304 pub(crate) fn part_verdict(
308 boundaries: &[Self],
309 header: &WriteHeader,
310 audience: &Audience,
311 slot: Option<Slot>,
312 ) -> Option<ChangeVerdict> {
313 let verdicts: Vec<_> = boundaries
314 .iter()
315 .filter_map(|boundary| boundary.excludes(header, audience, slot))
316 .collect();
317 verdicts
318 .iter()
319 .find(|verdict| matches!(verdict, ChangeVerdict::Ignored(_)))
320 .cloned()
321 .or_else(|| {
322 verdicts
323 .iter()
324 .filter_map(|verdict| match verdict {
325 ChangeVerdict::Excluded(version) => Some(*version),
326 _ => None,
327 })
328 .max()
329 .map(ChangeVerdict::Excluded)
330 })
331 }
332}
333#[cfg(test)]
334#[path = "write_boundary_tests.rs"]
335pub(crate) mod tests;