1use crate::{DbError, RowKey, Slot, WriteId};
4use coven_format::loss::{Loss, LossCause, LossValues};
5use coven_merge::Rule;
6use rusqlite::types::Value;
7
8#[derive(Clone, Debug, PartialEq)]
10pub struct LostValue {
11 pub table: String,
13 pub key: RowKey,
15 pub lost: Lost,
17 pub replaced_by: Replacement,
19 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#[derive(Clone, Debug, PartialEq)]
33pub enum Lost {
34 Cell(LostCell),
36 Row(Vec<LostCell>),
38}
39
40#[derive(Clone, Debug, PartialEq)]
42pub struct LostCell {
43 pub column: String,
45 pub value: Value,
47 pub set_by: WriteId,
49}
50
51#[derive(Clone, Debug, PartialEq)]
54pub enum Replacement {
55 Write(WriteId),
57 Rules(Vec<RemovalRule>),
59 SchemaChange {
61 version: u32,
63 },
64 DataDependency {
70 write: WriteId,
72 sources: Vec<ChangeSource>,
74 },
75 NotAuthorized {
77 write: WriteId,
79 reason: RowAuthorizationFailure,
81 },
82}
83
84#[derive(Clone, Debug, PartialEq, Eq)]
86pub enum RowAuthorizationFailure {
87 NotOwner {
90 owner: coven_foundation::id_source::MemberId,
92 },
93 InvalidForeignKeyEffect,
95}
96
97#[derive(Clone, Debug, PartialEq)]
99pub struct ChangeSource {
100 pub write: WriteId,
102 pub table: String,
104 pub key: RowKey,
106 pub audience: coven_merge::Audience,
108}
109
110#[derive(Clone, Debug, PartialEq, Eq)]
112pub enum RemovalRule {
113 ForeignKey {
115 columns: Vec<String>,
117 parent: String,
119 parent_columns: Vec<String>,
121 },
122 Check {
124 constraint: String,
126 },
127 DeletedCircle {
129 entry: Slot,
131 },
132 OtherAudience,
134 Unique {
136 terms: Vec<String>,
138 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;