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