Skip to main content

panproto_inst/
table_edit.rs

1//! Edit algebra for functor instances (model of `ThEditableStructure`).
2//!
3//! A [`TableEdit`] is an element of the edit monoid for relational
4//! (table-shaped) instances. The monoid operations are
5//! [`TableEdit::identity`], [`TableEdit::compose`], and
6//! [`TableEdit::apply`] (the partial monoid action on [`FInstance`]).
7
8use std::collections::HashMap;
9
10use serde::{Deserialize, Serialize};
11
12use crate::edit_error::EditError;
13use crate::functor::FInstance;
14use crate::value::Value;
15
16/// A model of `ThEditableStructure` for functor instances.
17///
18/// Each variant is a primitive relational mutation: inserting or
19/// deleting rows, or updating individual cells.
20#[derive(Clone, Debug, Serialize, Deserialize)]
21pub enum TableEdit {
22    /// The monoid identity: no change.
23    Identity,
24
25    /// Insert a row into a table.
26    InsertRow {
27        /// Table name (schema vertex ID).
28        table: String,
29        /// The row data (column name to value).
30        row: HashMap<String, Value>,
31    },
32
33    /// Delete a row from a table by key column value.
34    DeleteRow {
35        /// Table name.
36        table: String,
37        /// The key value identifying the row to delete.
38        key: Value,
39        /// The key column name.
40        key_column: String,
41    },
42
43    /// Update a single cell in a table.
44    UpdateCell {
45        /// Table name.
46        table: String,
47        /// Key value identifying the row.
48        key: Value,
49        /// Key column name.
50        key_column: String,
51        /// Column to update.
52        column: String,
53        /// New value.
54        value: Value,
55    },
56
57    /// A sequence of edits applied in order.
58    Sequence(Vec<Self>),
59}
60
61impl TableEdit {
62    /// The monoid identity element.
63    #[must_use]
64    pub const fn identity() -> Self {
65        Self::Identity
66    }
67
68    /// Monoid multiplication: compose two edits into a sequence.
69    ///
70    /// Nested sequences are flattened and identity elements are elided.
71    #[must_use]
72    pub fn compose(self, other: Self) -> Self {
73        let mut steps = Vec::new();
74        flatten_into(&mut steps, self);
75        flatten_into(&mut steps, other);
76        match steps.len() {
77            0 => Self::Identity,
78            1 => steps.into_iter().next().unwrap_or(Self::Identity),
79            _ => Self::Sequence(steps),
80        }
81    }
82
83    /// Returns `true` if this edit is the identity (no-op).
84    #[must_use]
85    pub fn is_identity(&self) -> bool {
86        match self {
87            Self::Identity => true,
88            Self::Sequence(steps) => steps.iter().all(Self::is_identity),
89            _ => false,
90        }
91    }
92
93    /// Apply this edit to a functor instance, mutating it in place.
94    ///
95    /// # Errors
96    ///
97    /// Returns [`EditError`] if the edit cannot be applied.
98    pub fn apply(&self, instance: &mut FInstance) -> Result<(), EditError> {
99        match self {
100            Self::Identity => Ok(()),
101
102            Self::InsertRow { table, row } => {
103                instance
104                    .tables
105                    .entry(table.clone())
106                    .or_default()
107                    .push(row.clone());
108                Ok(())
109            }
110
111            Self::DeleteRow {
112                table,
113                key,
114                key_column,
115            } => {
116                let rows = instance
117                    .tables
118                    .get_mut(table.as_str())
119                    .ok_or_else(|| EditError::TableNotFound(table.clone()))?;
120                let before = rows.len();
121                rows.retain(|row| row.get(key_column.as_str()) != Some(key));
122                if rows.len() == before {
123                    return Err(EditError::RowNotFound {
124                        table: table.clone(),
125                        key: format!("{key:?}"),
126                    });
127                }
128                Ok(())
129            }
130
131            Self::UpdateCell {
132                table,
133                key,
134                key_column,
135                column,
136                value,
137            } => {
138                let rows = instance
139                    .tables
140                    .get_mut(table.as_str())
141                    .ok_or_else(|| EditError::TableNotFound(table.clone()))?;
142                let row = rows
143                    .iter_mut()
144                    .find(|r| r.get(key_column.as_str()) == Some(key))
145                    .ok_or_else(|| EditError::RowNotFound {
146                        table: table.clone(),
147                        key: format!("{key:?}"),
148                    })?;
149                row.insert(column.clone(), value.clone());
150                Ok(())
151            }
152
153            Self::Sequence(steps) => {
154                for step in steps {
155                    step.apply(instance)?;
156                }
157                Ok(())
158            }
159        }
160    }
161}
162
163/// Flatten nested sequences and strip identities.
164fn flatten_into(out: &mut Vec<TableEdit>, edit: TableEdit) {
165    match edit {
166        TableEdit::Identity => {}
167        TableEdit::Sequence(steps) => {
168            for step in steps {
169                flatten_into(out, step);
170            }
171        }
172        other => out.push(other),
173    }
174}
175
176#[cfg(test)]
177#[allow(clippy::unwrap_used)]
178mod tests {
179    use std::collections::HashMap;
180
181    use crate::functor::FInstance;
182    use crate::value::Value;
183
184    use super::TableEdit;
185
186    fn sample_instance() -> FInstance {
187        let mut row1 = HashMap::new();
188        row1.insert("id".into(), Value::Int(1));
189        row1.insert("name".into(), Value::Str("alice".into()));
190
191        let mut row2 = HashMap::new();
192        row2.insert("id".into(), Value::Int(2));
193        row2.insert("name".into(), Value::Str("bob".into()));
194
195        FInstance::new().with_table("users", vec![row1, row2])
196    }
197
198    #[test]
199    fn identity_is_noop() {
200        let mut inst = sample_instance();
201        TableEdit::identity().apply(&mut inst).unwrap();
202        assert_eq!(inst.row_count("users"), 2);
203    }
204
205    #[test]
206    fn insert_row() {
207        let mut inst = sample_instance();
208        let mut row = HashMap::new();
209        row.insert("id".into(), Value::Int(3));
210        row.insert("name".into(), Value::Str("charlie".into()));
211
212        let edit = TableEdit::InsertRow {
213            table: "users".into(),
214            row,
215        };
216        edit.apply(&mut inst).unwrap();
217        assert_eq!(inst.row_count("users"), 3);
218    }
219
220    #[test]
221    fn delete_row() {
222        let mut inst = sample_instance();
223        let edit = TableEdit::DeleteRow {
224            table: "users".into(),
225            key: Value::Int(1),
226            key_column: "id".into(),
227        };
228        edit.apply(&mut inst).unwrap();
229        assert_eq!(inst.row_count("users"), 1);
230    }
231
232    #[test]
233    fn update_cell() {
234        let mut inst = sample_instance();
235        let edit = TableEdit::UpdateCell {
236            table: "users".into(),
237            key: Value::Int(1),
238            key_column: "id".into(),
239            column: "name".into(),
240            value: Value::Str("alicia".into()),
241        };
242        edit.apply(&mut inst).unwrap();
243        let rows = &inst.tables["users"];
244        let row = rows
245            .iter()
246            .find(|r| r.get("id") == Some(&Value::Int(1)))
247            .unwrap();
248        assert_eq!(row.get("name"), Some(&Value::Str("alicia".into())));
249    }
250
251    #[test]
252    fn insert_then_delete_is_identity() {
253        let mut inst = sample_instance();
254        let original_count = inst.row_count("users");
255
256        let mut row = HashMap::new();
257        row.insert("id".into(), Value::Int(99));
258        row.insert("name".into(), Value::Str("temp".into()));
259
260        let edit = TableEdit::InsertRow {
261            table: "users".into(),
262            row,
263        }
264        .compose(TableEdit::DeleteRow {
265            table: "users".into(),
266            key: Value::Int(99),
267            key_column: "id".into(),
268        });
269        edit.apply(&mut inst).unwrap();
270        assert_eq!(inst.row_count("users"), original_count);
271    }
272
273    #[test]
274    fn delete_from_nonexistent_table_fails() {
275        let mut inst = sample_instance();
276        let edit = TableEdit::DeleteRow {
277            table: "nonexistent".into(),
278            key: Value::Int(1),
279            key_column: "id".into(),
280        };
281        assert!(edit.apply(&mut inst).is_err());
282    }
283
284    #[test]
285    fn monoid_identity_law() {
286        let mut inst1 = sample_instance();
287        let mut inst2 = sample_instance();
288
289        let mut row = HashMap::new();
290        row.insert("id".into(), Value::Int(5));
291        row.insert("name".into(), Value::Str("eve".into()));
292
293        let edit = TableEdit::InsertRow {
294            table: "users".into(),
295            row: row.clone(),
296        };
297
298        TableEdit::identity()
299            .compose(edit.clone())
300            .apply(&mut inst1)
301            .unwrap();
302        edit.apply(&mut inst2).unwrap();
303
304        assert_eq!(inst1.row_count("users"), inst2.row_count("users"));
305    }
306}