Skip to main content

coven_database/
lost.rs

1//! Decoding the durable lost-value fields into the read API.
2
3use crate::{DbError, RowKey, Slot, WriteId};
4use coven_format::loss::{Loss, LossCause, LossValues};
5use coven_merge::Rule;
6use rusqlite::types::Value;
7
8/// One durable lost cell or removed row.
9#[derive(Clone, Debug, PartialEq)]
10pub struct LostValue {
11    /// The application's table.
12    pub table: String,
13    /// The row's primary key.
14    pub key: RowKey,
15    /// The displaced cell or removed row's cells.
16    pub lost: Lost,
17    /// What replaced these values.
18    pub replaced_by: Replacement,
19    /// Old-shape values kept independently of the current schema (§18.7).
20    /// Their table or columns may no longer exist; dismissal still works.
21    pub frozen: bool,
22    pub(crate) target: LossTarget,
23}
24
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub(crate) enum LossTarget {
27    Removed { row: coven_merge::RowId, generation: u64 },
28    Acknowledgements(Vec<coven_format::dismissal::Dismissal>),
29}
30
31/// Values retained when a cell loses or a row is removed.
32#[derive(Clone, Debug, PartialEq)]
33pub enum Lost {
34    /// One displaced cell.
35    Cell(LostCell),
36    /// Every retained column, with its own setter.
37    Row(Vec<LostCell>),
38}
39
40/// One retained value and the write that set it.
41#[derive(Clone, Debug, PartialEq)]
42pub struct LostCell {
43    /// The column's name.
44    pub column: String,
45    /// The SQLite value as written, without foreign-key null substitution.
46    pub value: Value,
47    /// The write that set this cell.
48    pub set_by: WriteId,
49}
50
51/// The write, rules, breaking schema version, dropped data or refused
52/// permission responsible for a loss.
53#[derive(Clone, Debug, PartialEq)]
54pub enum Replacement {
55    /// A write that had not read the displaced value.
56    Write(WriteId),
57    /// All removal rules holding for the row.
58    Rules(Vec<RemovalRule>),
59    /// A breaking schema change the write had not read.
60    SchemaChange {
61        /// The schema version reached by the breaking change.
62        version: u32,
63    },
64    /// This original row change used dropped data (§9.5, §14.1). Sources name
65    /// generation/setter changes, additional direct-execution sources, then
66    /// verified foreign-key causes (§9.6), in authored role/execution order,
67    /// with repeats. Row identity distinguishes several causes in the same
68    /// write. An empty delete still names its write.
69    DataDependency {
70        /// The write containing this row change.
71        write: WriteId,
72        /// The failed sources, in role order.
73        sources: Vec<ChangeSource>,
74    },
75    /// A member's row change lacked permission; other changes may apply (§9.6).
76    NotAuthorized {
77        /// The write containing this row change.
78        write: WriteId,
79        /// Why it was refused.
80        reason: RowAuthorizationFailure,
81    },
82}
83
84/// Why a member's row change was refused (§9.6).
85#[derive(Clone, Debug, PartialEq, Eq)]
86pub enum RowAuthorizationFailure {
87    /// A direct target belongs to this member, or a foreign target had no
88    /// valid foreign-key derivation from an authorized direct change.
89    NotOwner {
90        /// The member whose write created the row.
91        owner: coven_foundation::id_source::MemberId,
92    },
93    /// The proposed derived payload differs from the reconstructed schema effect.
94    InvalidForeignKeyEffect,
95}
96
97/// An original row change or direct execution target used as data (§9.5, §9.6).
98#[derive(Clone, Debug, PartialEq)]
99pub struct ChangeSource {
100    /// The write containing it.
101    pub write: WriteId,
102    /// Its original table name, before any conversion.
103    pub table: String,
104    /// The row's primary key.
105    pub key: RowKey,
106    /// The row's audience.
107    pub audience: coven_merge::Audience,
108}
109
110/// A rule responsible for a removed row.
111#[derive(Clone, Debug, PartialEq, Eq)]
112pub enum RemovalRule {
113    /// A reference to an absent, stale or removed parent.
114    ForeignKey {
115        /// The referencing columns, in constraint order.
116        columns: Vec<String>,
117        /// The referenced table.
118        parent: String,
119        /// The referenced columns, in constraint order.
120        parent_columns: Vec<String>,
121    },
122    /// A failed CHECK.
123    Check {
124        /// The constraint's name.
125        constraint: String,
126    },
127    /// The row belongs to a deleted circle.
128    DeletedCircle {
129        /// The kept entry responsible for the deletion.
130        entry: Slot,
131    },
132    /// The same key is shown from another audience.
133    OtherAudience,
134    /// A competing row won the unique values.
135    Unique {
136        /// Column names or expression text, in constraint order.
137        terms: Vec<String>,
138        /// The WHERE expression of a partial constraint.
139        partial: Option<String>,
140    },
141}
142
143impl LostValue {
144    pub(crate) fn from_record(record: Loss) -> Result<Self, DbError> {
145        let key = RowKey::decode(&record.row.key)?;
146        let cell = |column, cell: coven_merge::Cell<coven_format::value::Value>| LostCell {
147            column,
148            value: crate::write_encoding::sql_value(&cell.value.value),
149            set_by: cell.write,
150        };
151        let lost = match record.values {
152            LossValues::Cell { column, cell: value } => Lost::Cell(cell(column, value)),
153            LossValues::Row(cells) => {
154                Lost::Row(cells.into_iter().map(|(column, value)| cell(column, value)).collect())
155            }
156        };
157        let original = record.cause.change_write();
158        let replaced_by = match record.cause {
159            LossCause::Write(write) => Replacement::Write(write),
160            LossCause::Rules(rules) => Replacement::Rules(
161                rules
162                    .into_iter()
163                    .map(|rule| match rule {
164                        Rule::ForeignKey(key) => RemovalRule::ForeignKey {
165                            columns: key.columns.0,
166                            parent: key.parent,
167                            parent_columns: key.parent_columns.0,
168                        },
169                        Rule::Unique(constraint) => RemovalRule::Unique {
170                            terms: constraint.terms,
171                            partial: constraint.partial,
172                        },
173                        Rule::Check(constraint) => RemovalRule::Check { constraint },
174                        Rule::DeletedCircle(entry) => RemovalRule::DeletedCircle { entry },
175                        Rule::OtherAudience => RemovalRule::OtherAudience,
176                    })
177                    .collect(),
178            ),
179            LossCause::Excluded { version, .. } => Replacement::SchemaChange { version },
180            LossCause::Dependency { write, sources } => Replacement::DataDependency {
181                write,
182                sources: sources
183                    .into_iter()
184                    .map(|source| {
185                        Ok(ChangeSource {
186                            write: source.write,
187                            key: RowKey::decode(&source.row.key)?,
188                            table: source.row.table,
189                            audience: source.row.audience,
190                        })
191                    })
192                    .collect::<Result<_, DbError>>()?,
193            },
194            LossCause::NotAuthorized { write, reason } => Replacement::NotAuthorized {
195                write,
196                reason: match reason {
197                    coven_format::loss::AuthorizationFailure::NotOwner { owner } => {
198                        RowAuthorizationFailure::NotOwner { owner }
199                    }
200                    coven_format::loss::AuthorizationFailure::InvalidForeignKeyEffect => {
201                        RowAuthorizationFailure::InvalidForeignKeyEffect
202                    }
203                },
204            },
205        };
206        let row = record.row;
207        let frozen = record.frozen;
208        let target = if matches!(replaced_by, Replacement::Rules(_)) && !frozen {
209            LossTarget::Removed { row: row.clone(), generation: record.generation }
210        } else {
211            let cells = match &lost {
212                Lost::Cell(cell) => std::slice::from_ref(cell),
213                Lost::Row(cells) => cells,
214            };
215            LossTarget::Acknowledgements(if cells.is_empty() {
216                vec![coven_format::dismissal::Dismissal {
217                    row: row.clone(),
218                    target: coven_format::dismissal::DismissalTarget::EmptyRow,
219                    write: original.expect("an empty frozen loss names its original change"),
220                }]
221            } else {
222                cells
223                    .iter()
224                    .map(|cell| coven_format::dismissal::Dismissal {
225                        row: row.clone(),
226                        target: coven_format::dismissal::DismissalTarget::Cell(cell.column.clone()),
227                        write: cell.set_by,
228                    })
229                    .collect()
230            })
231        };
232        Ok(LostValue { table: row.table, key, lost, replaced_by, frozen, target })
233    }
234}
235
236#[cfg(test)]
237#[path = "lost_tests.rs"]
238mod tests;