Skip to main content

coven_format/
loss.rs

1//! One loss record for displaced cells, removed rows and excluded writes.
2
3use crate::error::{require, Error, Rule as FormatRule};
4use crate::value::{name, positive, row, Value};
5use crate::wire::{Decoder, Encoder, Wire};
6use crate::write_sources::ChangeId;
7use coven_foundation::id_source::MemberId;
8use coven_merge::{Cell, LostKey, LostValue, RowId, Rule, WriteId};
9use std::collections::{BTreeMap, BTreeSet};
10
11/// A loss independent of local SQLite identities. Active losses follow merge;
12/// frozen losses keep their values and cause after their merge history is gone.
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct Loss {
15    /// Original table, key and audience.
16    pub row: RowId,
17    /// Cell incarnation, removed-row generation, or excluded change's generation.
18    pub generation: u64,
19    /// Whether this loss is frozen outside merge.
20    pub frozen: bool,
21    /// Values as written, with each value's setter.
22    pub values: LossValues,
23    /// What displaced these values.
24    pub cause: LossCause,
25}
26impl Wire for Loss {
27    fn put(&self, out: &mut Encoder) -> Result<(), Error> {
28        self.row.put(out)?;
29        self.generation.put(out)?;
30        u8::from(self.frozen).put(out)?;
31        self.values.put(out)?;
32        self.cause.put(out)
33    }
34    fn get(input: &mut Decoder<'_>) -> Result<Self, Error> {
35        let row = Wire::get(input)?;
36        let generation = Wire::get(input)?;
37        let frozen = match u8::get(input)? {
38            0 => false,
39            1 => true,
40            tag => return Err(Error::UnknownTag { field: "loss retirement", tag }),
41        };
42        Ok(Self { row, generation, frozen, values: Wire::get(input)?, cause: Wire::get(input)? })
43    }
44}
45
46/// The cell or whole row retained by a loss.
47#[derive(Clone, Debug, PartialEq, Eq)]
48pub enum LossValues {
49    /// One concurrently displaced cell.
50    Cell {
51        /// The displaced column.
52        column: String,
53        /// Its written value and setter.
54        cell: Cell<Value>,
55    },
56    /// Every retained column of a removed or excluded row.
57    Row(BTreeMap<String, Cell<Value>>),
58}
59impl Wire for LossValues {
60    fn put(&self, out: &mut Encoder) -> Result<(), Error> {
61        match self {
62            Self::Cell { column, cell } => {
63                0u8.put(out)?;
64                column.put(out)?;
65                cell.put(out)
66            }
67            Self::Row(cells) => {
68                1u8.put(out)?;
69                cells.put(out)
70            }
71        }
72    }
73    fn get(input: &mut Decoder<'_>) -> Result<Self, Error> {
74        match u8::get(input)? {
75            0 => Ok(Self::Cell { column: crate::wire::get_name(input)?, cell: Wire::get(input)? }),
76            1 => Ok(Self::Row(crate::wire::get_name_map(input)?)),
77            tag => Err(Error::UnknownTag { field: "loss values", tag }),
78        }
79    }
80}
81
82/// The replacing write, removal rules, or boundary that excluded a write.
83#[derive(Clone, Debug, PartialEq, Eq)]
84pub enum LossCause {
85    /// A replacing write that had not read the lost setter.
86    Write(WriteId),
87    /// Every rule holding when the row was removed.
88    Rules(BTreeSet<Rule>),
89    /// A breaking schema change that excluded the write, or whose migration
90    /// write deleted a hidden row. Resets create no losses (§19.4).
91    Excluded {
92        /// The excluded write, including deletions with no retained cells.
93        write: WriteId,
94        /// The schema version reached by the breaking change.
95        version: u32,
96    },
97    /// An admitted row change that used data which does not survive (§9.5):
98    /// its generation source, a cell setter it replaced, a source its
99    /// operations used, or a foreign-key cause in its own write.
100    Dependency {
101        /// The write whose row change this is, even for an empty delete.
102        write: WriteId,
103        /// Failed sources in role order, with repeats: generation, replaced
104        /// setters, additional execution sources, then foreign-key causes.
105        sources: Vec<ChangeId>,
106    },
107    /// A member's row change without permission: a direct change of another
108    /// member's owned row, or a foreign-key effect the receiver could not
109    /// derive exactly from an authorized direct change (§9.6).
110    NotAuthorized {
111        /// The write whose row change this is, even for an empty delete.
112        write: WriteId,
113        /// Why the change was refused.
114        reason: AuthorizationFailure,
115    },
116}
117
118/// Why a row change lacked permission (§9.6).
119#[derive(Clone, Debug, PartialEq, Eq)]
120pub enum AuthorizationFailure {
121    /// The row belongs to this member, and the change was not an exact
122    /// foreign-key effect of an authorized change.
123    NotOwner {
124        /// The member whose write created the row.
125        owner: MemberId,
126    },
127    /// A derived change differs from the schema's effect of authorized
128    /// direct changes in its write, or has no authorized root.
129    InvalidForeignKeyEffect,
130}
131impl Wire for AuthorizationFailure {
132    fn put(&self, out: &mut Encoder) -> Result<(), Error> {
133        match self {
134            Self::NotOwner { owner } => {
135                0u8.put(out)?;
136                owner.put(out)
137            }
138            Self::InvalidForeignKeyEffect => 1u8.put(out),
139        }
140    }
141    fn get(input: &mut Decoder<'_>) -> Result<Self, Error> {
142        match u8::get(input)? {
143            0 => Ok(Self::NotOwner { owner: Wire::get(input)? }),
144            1 => Ok(Self::InvalidForeignKeyEffect),
145            tag => Err(Error::UnknownTag { field: "authorization failure", tag }),
146        }
147    }
148}
149impl Wire for LossCause {
150    fn put(&self, out: &mut Encoder) -> Result<(), Error> {
151        match self {
152            Self::Write(write) => {
153                0u8.put(out)?;
154                write.put(out)
155            }
156            Self::Rules(rules) => {
157                1u8.put(out)?;
158                rules.put(out)
159            }
160            Self::Excluded { write, version } => {
161                2u8.put(out)?;
162                write.put(out)?;
163                version.put(out)
164            }
165            Self::Dependency { write, sources } => {
166                3u8.put(out)?;
167                write.put(out)?;
168                sources.put(out)
169            }
170            Self::NotAuthorized { write, reason } => {
171                4u8.put(out)?;
172                write.put(out)?;
173                reason.put(out)
174            }
175        }
176    }
177    fn get(input: &mut Decoder<'_>) -> Result<Self, Error> {
178        match u8::get(input)? {
179            0 => Ok(Self::Write(Wire::get(input)?)),
180            1 => Ok(Self::Rules(Wire::get(input)?)),
181            2 => Ok(Self::Excluded { write: Wire::get(input)?, version: Wire::get(input)? }),
182            3 => Ok(Self::Dependency { write: Wire::get(input)?, sources: Wire::get(input)? }),
183            4 => Ok(Self::NotAuthorized { write: Wire::get(input)?, reason: Wire::get(input)? }),
184            tag => Err(Error::UnknownTag { field: "loss cause", tag }),
185        }
186    }
187}
188
189/// Stable identity within a row and generation; values and causes may change.
190#[derive(PartialEq, Eq, PartialOrd, Ord)]
191pub(crate) enum LossIdentity {
192    Cell(LostKey),
193    Row(BTreeMap<String, WriteId>),
194}
195impl LossCause {
196    /// The original change's own write, for a frozen change loss.
197    pub fn change_write(&self) -> Option<WriteId> {
198        match self {
199            Self::Excluded { write, .. }
200            | Self::Dependency { write, .. }
201            | Self::NotAuthorized { write, .. } => Some(*write),
202            Self::Write(_) | Self::Rules(_) => None,
203        }
204    }
205
206    pub(crate) fn tag(&self) -> u8 {
207        match self {
208            Self::Write(_) => 0,
209            Self::Rules(_) => 1,
210            Self::Excluded { .. } => 2,
211            Self::Dependency { .. } => 3,
212            Self::NotAuthorized { .. } => 4,
213        }
214    }
215
216    /// A frozen original row change: excluded, dependent or unauthorized.
217    pub fn freezes_change(&self) -> bool {
218        self.change_write().is_some()
219    }
220}
221
222impl Loss {
223    /// Project a concurrent merge loss into the common record.
224    pub fn cell(row: RowId, key: LostKey, value: LostValue<Value>) -> Self {
225        Self {
226            row,
227            generation: value.incarnation,
228            frozen: false,
229            values: LossValues::Cell {
230                column: key.column,
231                cell: Cell { write: key.write, value: value.value },
232            },
233            cause: LossCause::Write(value.replaced_by),
234        }
235    }
236
237    /// This loss frozen outside merge: it keeps its written values, setters,
238    /// names and cause, without live parent links (D7).
239    pub fn freeze(mut self) -> Self {
240        self.frozen = true;
241        match &mut self.values {
242            LossValues::Cell { cell, .. } => cell.value.parents.clear(),
243            LossValues::Row(cells) => {
244                for cell in cells.values_mut() {
245                    cell.value.parents.clear();
246                }
247            }
248        }
249        self
250    }
251
252    /// Project a row removed by rules into the common record.
253    pub fn removed(state: &coven_merge::RowState<Value>, rules: BTreeSet<Rule>) -> Self {
254        Self {
255            row: state.row().clone(),
256            generation: state.generation(),
257            frozen: false,
258            values: LossValues::Row(state.cells().clone()),
259            cause: LossCause::Rules(rules),
260        }
261    }
262
263    pub(crate) fn key(&self) -> (RowId, u64, LossIdentity, bool, u8, Option<WriteId>) {
264        let identity = match &self.values {
265            LossValues::Cell { column, cell } => {
266                LossIdentity::Cell(LostKey { column: column.clone(), write: cell.write })
267            }
268            LossValues::Row(cells) => {
269                LossIdentity::Row(cells.iter().map(|(n, c)| (n.clone(), c.write)).collect())
270            }
271        };
272        (
273            self.row.clone(),
274            self.generation,
275            identity,
276            self.frozen,
277            self.cause.tag(),
278            self.cause.change_write(),
279        )
280    }
281
282    pub(crate) fn validate(&self) -> Result<(), Error> {
283        row(&self.row)?;
284        match (&self.values, &self.cause) {
285            (LossValues::Cell { .. }, LossCause::Write(write)) => positive(write.number)?,
286            (LossValues::Row(cells), LossCause::Rules(rules)) => {
287                require(
288                    !cells.is_empty() && !rules.is_empty(),
289                    "removed loss",
290                    FormatRule::Required,
291                )?;
292                crate::merge_wire::rules(rules)?;
293                for rule in rules {
294                    if matches!(rule, Rule::DeletedCircle(_) | Rule::OtherAudience) {
295                        require(
296                            matches!(self.row.audience, coven_merge::Audience::Circle(_)),
297                            "loss audience",
298                            FormatRule::Audience,
299                        )?;
300                    }
301                }
302            }
303            (LossValues::Row(cells), LossCause::Excluded { write, version }) => {
304                positive(write.number)?;
305                // An excluded write names itself as setter; a row a migration
306                // write deleted through its generation keeps each original
307                // setter (D7).
308                for cell in cells.values() {
309                    positive(cell.write.number)?;
310                }
311                require(self.frozen, "excluded loss", FormatRule::Required)?;
312                require(*version > 0, "breaking schema version", FormatRule::Required)?;
313            }
314            (LossValues::Row(cells), LossCause::Dependency { write, sources }) => {
315                positive(write.number)?;
316                require(
317                    cells.values().all(|cell| cell.write == *write),
318                    "dependent setters",
319                    FormatRule::Required,
320                )?;
321                require(self.frozen, "dependent loss", FormatRule::Required)?;
322                require(!sources.is_empty(), "dependency sources", FormatRule::Required)?;
323                for source in sources {
324                    positive(source.write.number)?;
325                    require(
326                        dependency_source(&self.row, *write, source),
327                        "dependency source row",
328                        FormatRule::Required,
329                    )?;
330                }
331            }
332            (LossValues::Row(cells), LossCause::NotAuthorized { write, .. }) => {
333                positive(write.number)?;
334                require(
335                    cells.values().all(|cell| cell.write == *write),
336                    "unauthorized setters",
337                    FormatRule::Required,
338                )?;
339                require(self.frozen, "unauthorized loss", FormatRule::Required)?;
340            }
341            _ => {
342                return Err(Error::Invalid {
343                    field: "loss cause",
344                    rule: FormatRule::ColumnOperation,
345                })
346            }
347        }
348        if !self.cause.freezes_change() {
349            require(
350                coven_merge::generation_present(self.generation),
351                "loss incarnation",
352                FormatRule::Generation,
353            )?;
354        }
355        let cell = |n: &str, cell: &Cell<Value>| {
356            name(n)?;
357            positive(cell.write.number)?;
358            crate::merge_wire::column(&cell.value)?;
359            if self.frozen {
360                require(cell.value.parents.is_empty(), "frozen references", FormatRule::Required)?;
361            } else {
362                for parent in cell.value.parents.values() {
363                    parent.validate_written(&self.row).map_err(Error::Merge)?;
364                }
365            }
366            Ok(())
367        };
368        match &self.values {
369            LossValues::Cell { column, cell: value } => cell(column, value),
370            LossValues::Row(cells) => cells.iter().try_for_each(|(n, c)| cell(n, c)),
371        }
372    }
373
374    /// The writes this loss names, which a snapshot carrying it must cover.
375    pub(crate) fn named_writes(&self) -> Vec<WriteId> {
376        let mut writes: Vec<WriteId> = match &self.values {
377            LossValues::Cell { cell, .. } => vec![cell.write],
378            LossValues::Row(cells) => cells.values().map(|cell| cell.write).collect(),
379        };
380        match self.cause {
381            LossCause::Write(write)
382            | LossCause::Excluded { write, .. }
383            | LossCause::NotAuthorized { write, .. } => writes.push(write),
384            LossCause::Dependency { write, ref sources } => {
385                writes.push(write);
386                writes.extend(sources.iter().map(|s| s.write));
387            }
388            _ => {}
389        }
390        writes
391    }
392}
393
394/// A dependent change's source: a change at the same row (its generation,
395/// setters and execution sources), or a foreign-key cause in its own write.
396pub(crate) fn dependency_source(row: &RowId, write: WriteId, source: &ChangeId) -> bool {
397    source.write == write || (source.row.key == row.key && source.row.audience == row.audience)
398}
399
400#[cfg(test)]
401#[path = "loss_tests.rs"]
402mod tests;