Skip to main content

wasm_dbms/
transaction.rs

1// Rust guideline compliant 2026-02-28
2
3//! Transaction management for the DBMS engine.
4
5pub(crate) mod journal;
6mod overlay;
7pub mod session;
8
9use wasm_dbms_api::prelude::{
10    ColumnDef, DbmsResult, DeleteBehavior, Filter, TableSchema, UpdateRecord as _, Value,
11};
12
13pub use self::overlay::{DatabaseOverlay, IndexOverlay};
14
15/// A transaction represents a sequence of operations performed as a single
16/// logical unit of work.
17#[derive(Debug, Default)]
18pub struct Transaction {
19    /// Stack of operations performed in this transaction.
20    pub(crate) operations: Vec<TransactionOp>,
21    /// Overlay to track uncommitted changes.
22    overlay: DatabaseOverlay,
23}
24
25impl Transaction {
26    /// Inserts a new insert operation into the transaction.
27    pub fn insert<T>(&mut self, values: Vec<(ColumnDef, Value)>) -> DbmsResult<()>
28    where
29        T: TableSchema,
30    {
31        self.overlay.insert::<T>(values.clone())?;
32        self.operations.push(TransactionOp::Insert {
33            table: T::table_name(),
34            values,
35        });
36        Ok(())
37    }
38
39    /// Inserts a new update operation into the transaction.
40    ///
41    /// `rows` is a list of `(primary_key, current_row)` pairs for each affected record.
42    /// The current row is needed to track old indexed values in the overlay.
43    pub fn update<T>(
44        &mut self,
45        patch: T::Update,
46        filter: Option<Filter>,
47        rows: Vec<(Value, Vec<(ColumnDef, Value)>)>,
48    ) -> DbmsResult<()>
49    where
50        T: TableSchema,
51    {
52        let patch_values = patch.update_values();
53        let overlay_patch: Vec<_> = patch_values
54            .iter()
55            .map(|(col, val)| (col.name, val.clone()))
56            .collect();
57
58        for (pk, current_row) in rows {
59            self.overlay
60                .update::<T>(pk, overlay_patch.clone(), &current_row);
61        }
62
63        self.operations.push(TransactionOp::Update {
64            table: T::table_name(),
65            patch: patch_values,
66            filter,
67        });
68        Ok(())
69    }
70
71    /// Inserts a new delete operation into the transaction.
72    ///
73    /// `rows` is a list of `(primary_key, current_row)` pairs for each affected record.
74    /// The current row is needed to track removed indexed values in the overlay.
75    pub fn delete<T>(
76        &mut self,
77        behaviour: DeleteBehavior,
78        filter: Option<Filter>,
79        rows: Vec<(Value, Vec<(ColumnDef, Value)>)>,
80    ) -> DbmsResult<()>
81    where
82        T: TableSchema,
83    {
84        for (pk, current_row) in rows {
85            self.overlay.delete::<T>(pk, &current_row);
86        }
87
88        self.operations.push(TransactionOp::Delete {
89            table: T::table_name(),
90            behaviour,
91            filter,
92        });
93        Ok(())
94    }
95
96    /// Returns a reference to the overlay.
97    pub fn overlay(&self) -> &DatabaseOverlay {
98        &self.overlay
99    }
100
101    /// Returns a mutable reference to the overlay.
102    pub fn overlay_mut(&mut self) -> &mut DatabaseOverlay {
103        &mut self.overlay
104    }
105}
106
107/// An operation within a transaction.
108#[derive(Debug)]
109pub enum TransactionOp {
110    Insert {
111        table: &'static str,
112        values: Vec<(ColumnDef, Value)>,
113    },
114    Delete {
115        table: &'static str,
116        behaviour: DeleteBehavior,
117        filter: Option<Filter>,
118    },
119    Update {
120        table: &'static str,
121        patch: Vec<(ColumnDef, Value)>,
122        filter: Option<Filter>,
123    },
124}
125
126#[cfg(test)]
127mod tests {
128
129    use wasm_dbms_api::prelude::{
130        Database as _, InsertRecord as _, Query, TableSchema as _, Text, Uint32, UpdateRecord as _,
131        Value,
132    };
133    use wasm_dbms_macros::{DatabaseSchema, Table};
134    use wasm_dbms_memory::prelude::HeapMemoryProvider;
135
136    use super::*;
137    use crate::prelude::{DbmsContext, WasmDbmsDatabase};
138
139    #[derive(Debug, Table, Clone, PartialEq, Eq)]
140    #[table = "items"]
141    pub struct Item {
142        #[primary_key]
143        pub id: Uint32,
144        pub name: Text,
145    }
146
147    #[derive(DatabaseSchema)]
148    #[tables(Item = "items")]
149    pub struct TestSchema;
150
151    fn setup() -> DbmsContext<HeapMemoryProvider> {
152        let ctx = DbmsContext::new(HeapMemoryProvider::default());
153        TestSchema::register_tables(&ctx).unwrap();
154        ctx
155    }
156
157    #[test]
158    fn test_transaction_insert_records_operation() {
159        let mut tx = Transaction::default();
160        let values = vec![
161            (Item::columns()[0], Value::Uint32(Uint32(1))),
162            (Item::columns()[1], Value::Text(Text("foo".to_string()))),
163        ];
164        tx.insert::<Item>(values).unwrap();
165        assert_eq!(tx.operations.len(), 1);
166        assert!(matches!(
167            &tx.operations[0],
168            TransactionOp::Insert { table: "items", .. }
169        ));
170    }
171
172    #[test]
173    fn test_transaction_update_records_operation() {
174        let mut tx = Transaction::default();
175        let patch = ItemUpdateRequest::from_values(
176            &[(Item::columns()[1], Value::Text(Text("bar".to_string())))],
177            Some(Filter::eq("id", Value::Uint32(Uint32(1)))),
178        );
179        let current_row = vec![
180            (Item::columns()[0], Value::Uint32(Uint32(1))),
181            (Item::columns()[1], Value::Text(Text("foo".to_string()))),
182        ];
183        tx.update::<Item>(
184            patch,
185            Some(Filter::eq("id", Value::Uint32(Uint32(1)))),
186            vec![(Value::Uint32(Uint32(1)), current_row)],
187        )
188        .unwrap();
189        assert_eq!(tx.operations.len(), 1);
190        assert!(matches!(
191            &tx.operations[0],
192            TransactionOp::Update { table: "items", .. }
193        ));
194    }
195
196    #[test]
197    fn test_transaction_delete_records_operation() {
198        let mut tx = Transaction::default();
199        let current_row = vec![
200            (Item::columns()[0], Value::Uint32(Uint32(1))),
201            (Item::columns()[1], Value::Text(Text("foo".to_string()))),
202        ];
203        tx.delete::<Item>(
204            DeleteBehavior::Restrict,
205            Some(Filter::eq("id", Value::Uint32(Uint32(1)))),
206            vec![(Value::Uint32(Uint32(1)), current_row)],
207        )
208        .unwrap();
209        assert_eq!(tx.operations.len(), 1);
210        assert!(matches!(
211            &tx.operations[0],
212            TransactionOp::Delete {
213                table: "items",
214                behaviour: DeleteBehavior::Restrict,
215                ..
216            }
217        ));
218    }
219
220    #[test]
221    fn test_transaction_overlay_accessors() {
222        let mut tx = Transaction::default();
223        // Overlay should start empty
224        let overlay = tx.overlay();
225        let overlay_str = format!("{overlay:?}");
226        assert!(overlay_str.contains("DatabaseOverlay"));
227
228        let _overlay_mut = tx.overlay_mut();
229    }
230
231    #[test]
232    fn test_transaction_multiple_operations() {
233        let mut tx = Transaction::default();
234        let insert_values = vec![
235            (Item::columns()[0], Value::Uint32(Uint32(1))),
236            (Item::columns()[1], Value::Text(Text("a".to_string()))),
237        ];
238        tx.insert::<Item>(insert_values.clone()).unwrap();
239        tx.delete::<Item>(
240            DeleteBehavior::Cascade,
241            None,
242            vec![(Value::Uint32(Uint32(1)), insert_values)],
243        )
244        .unwrap();
245        assert_eq!(tx.operations.len(), 2);
246    }
247
248    #[test]
249    fn test_rollback_discards_transaction() {
250        let ctx = setup();
251        let owner = vec![1, 2, 3];
252        let tx_id = ctx.begin_transaction(owner);
253        let mut db = WasmDbmsDatabase::from_transaction(&ctx, TestSchema, tx_id);
254
255        let insert = ItemInsertRequest::from_values(&[
256            (Item::columns()[0], Value::Uint32(Uint32(42))),
257            (
258                Item::columns()[1],
259                Value::Text(Text("rolled_back".to_string())),
260            ),
261        ])
262        .unwrap();
263        db.insert::<Item>(insert).unwrap();
264
265        db.rollback().unwrap();
266
267        // After rollback, the record should not exist
268        let db = WasmDbmsDatabase::oneshot(&ctx, TestSchema);
269        let rows = db.select::<Item>(Query::builder().build()).unwrap();
270        assert!(rows.is_empty());
271    }
272
273    #[test]
274    fn test_rollback_without_transaction_returns_error() {
275        let ctx = setup();
276        let mut db = WasmDbmsDatabase::oneshot(&ctx, TestSchema);
277        let result = db.rollback();
278        assert!(result.is_err());
279    }
280}