Skip to main content

wasm_dbms/
schema.rs

1// Rust guideline compliant 2026-03-01
2// X-WHERE-CLAUSE, M-CANONICAL-DOCS
3
4use wasm_dbms_api::prelude::{
5    CandidColumnDef, ColumnDef, DbmsResult, DeleteBehavior, Filter, Query, Value,
6};
7use wasm_dbms_memory::prelude::{AccessControl, AccessControlList, MemoryProvider};
8
9use crate::database::WasmDbmsDatabase;
10
11/// Provides schema-driven dynamic dispatch for database operations.
12///
13/// Implementations of this trait know which concrete table types exist
14/// and forward generic operations (identified by table name) to the
15/// appropriate typed methods on [`WasmDbmsDatabase`].
16///
17/// This trait is typically implemented by generated code from the
18/// `#[derive(DatabaseSchema)]` macro.
19pub trait DatabaseSchema<M, A = AccessControlList>
20where
21    M: MemoryProvider,
22    A: AccessControl,
23{
24    /// Performs a generic select for the given table name and query.
25    fn select(
26        &self,
27        dbms: &WasmDbmsDatabase<'_, M, A>,
28        table_name: &str,
29        query: Query,
30    ) -> DbmsResult<Vec<Vec<(ColumnDef, Value)>>>;
31
32    /// Performs a join query, returning results with column definitions
33    /// that include source table names.
34    fn select_join(
35        &self,
36        dbms: &WasmDbmsDatabase<'_, M, A>,
37        from_table: &str,
38        query: Query,
39    ) -> DbmsResult<Vec<Vec<(CandidColumnDef, Value)>>> {
40        crate::join::JoinEngine::new(self).join(dbms, from_table, query)
41    }
42
43    /// Returns tables and columns that reference the given table via foreign keys.
44    fn referenced_tables(&self, table: &'static str) -> Vec<(&'static str, Vec<&'static str>)>;
45
46    /// Performs an insert for the given table name.
47    fn insert(
48        &self,
49        dbms: &WasmDbmsDatabase<'_, M, A>,
50        table_name: &'static str,
51        record_values: &[(ColumnDef, Value)],
52    ) -> DbmsResult<()>;
53
54    /// Performs a delete for the given table name.
55    fn delete(
56        &self,
57        dbms: &WasmDbmsDatabase<'_, M, A>,
58        table_name: &'static str,
59        delete_behavior: DeleteBehavior,
60        filter: Option<Filter>,
61    ) -> DbmsResult<u64>;
62
63    /// Performs an update for the given table name.
64    fn update(
65        &self,
66        dbms: &WasmDbmsDatabase<'_, M, A>,
67        table_name: &'static str,
68        patch_values: &[(ColumnDef, Value)],
69        filter: Option<Filter>,
70    ) -> DbmsResult<u64>;
71
72    /// Validates an insert operation.
73    fn validate_insert(
74        &self,
75        dbms: &WasmDbmsDatabase<'_, M, A>,
76        table_name: &'static str,
77        record_values: &[(ColumnDef, Value)],
78    ) -> DbmsResult<()>;
79
80    /// Validates an update operation.
81    fn validate_update(
82        &self,
83        dbms: &WasmDbmsDatabase<'_, M, A>,
84        table_name: &'static str,
85        record_values: &[(ColumnDef, Value)],
86        old_pk: Value,
87    ) -> DbmsResult<()>;
88}
89
90#[cfg(test)]
91mod tests {
92    use wasm_dbms_api::prelude::{
93        Database as _, InsertRecord as _, Query, TableSchema as _, Text, Uint32, Value,
94    };
95    use wasm_dbms_macros::{DatabaseSchema, Table};
96    use wasm_dbms_memory::prelude::HeapMemoryProvider;
97
98    use super::DatabaseSchema as _;
99    use crate::prelude::{DbmsContext, WasmDbmsDatabase};
100
101    #[derive(Debug, Table, Clone, PartialEq, Eq)]
102    #[table = "items"]
103    pub struct Item {
104        #[primary_key]
105        pub id: Uint32,
106        pub name: Text,
107    }
108
109    #[derive(DatabaseSchema)]
110    #[tables(Item = "items")]
111    pub struct TestSchema;
112
113    fn setup() -> DbmsContext<HeapMemoryProvider> {
114        let ctx = DbmsContext::new(HeapMemoryProvider::default());
115        TestSchema::register_tables(&ctx).unwrap();
116        ctx
117    }
118
119    #[test]
120    fn test_should_register_tables_via_macro() {
121        let ctx = DbmsContext::new(HeapMemoryProvider::default());
122        TestSchema::register_tables(&ctx).unwrap();
123    }
124
125    #[test]
126    fn test_should_insert_and_select_via_schema() {
127        let ctx = setup();
128        let db = WasmDbmsDatabase::oneshot(&ctx, TestSchema);
129
130        let insert = ItemInsertRequest::from_values(&[
131            (Item::columns()[0], Value::Uint32(Uint32(1))),
132            (Item::columns()[1], Value::Text(Text("foo".to_string()))),
133        ])
134        .unwrap();
135        db.insert::<Item>(insert).unwrap();
136
137        let rows = TestSchema
138            .select(&db, "items", Query::builder().build())
139            .unwrap();
140        assert_eq!(rows.len(), 1);
141        assert_eq!(rows[0][1].1, Value::Text(Text("foo".to_string())));
142    }
143
144    #[test]
145    fn test_should_delete_via_schema() {
146        let ctx = setup();
147        let db = WasmDbmsDatabase::oneshot(&ctx, TestSchema);
148
149        let insert = ItemInsertRequest::from_values(&[
150            (Item::columns()[0], Value::Uint32(Uint32(1))),
151            (Item::columns()[1], Value::Text(Text("foo".to_string()))),
152        ])
153        .unwrap();
154        db.insert::<Item>(insert).unwrap();
155
156        let deleted = TestSchema
157            .delete(
158                &db,
159                "items",
160                wasm_dbms_api::prelude::DeleteBehavior::Restrict,
161                None,
162            )
163            .unwrap();
164        assert_eq!(deleted, 1);
165    }
166
167    #[test]
168    fn test_should_return_error_for_unknown_table() {
169        let ctx = setup();
170        let db = WasmDbmsDatabase::oneshot(&ctx, TestSchema);
171
172        let result = TestSchema.select(&db, "nonexistent", Query::builder().build());
173        assert!(result.is_err());
174    }
175
176    #[test]
177    fn test_should_return_referenced_tables() {
178        let refs = <TestSchema as super::DatabaseSchema<HeapMemoryProvider>>::referenced_tables(
179            &TestSchema,
180            "items",
181        );
182        assert!(refs.is_empty());
183    }
184
185    #[test]
186    fn test_commit_rolls_back_all_operations_on_failure() {
187        let ctx = setup();
188        let owner = vec![1, 2, 3];
189
190        // Begin a transaction and queue two inserts.
191        let tx_id = ctx.begin_transaction(owner);
192        let mut db = WasmDbmsDatabase::from_transaction(&ctx, TestSchema, tx_id);
193
194        let first = ItemInsertRequest::from_values(&[
195            (Item::columns()[0], Value::Uint32(Uint32(1))),
196            (Item::columns()[1], Value::Text(Text("first".to_string()))),
197        ])
198        .unwrap();
199        db.insert::<Item>(first).unwrap();
200
201        let second = ItemInsertRequest::from_values(&[
202            (Item::columns()[0], Value::Uint32(Uint32(2))),
203            (Item::columns()[1], Value::Text(Text("second".to_string()))),
204        ])
205        .unwrap();
206        db.insert::<Item>(second).unwrap();
207
208        // Before committing, insert PK=2 outside the transaction so the
209        // second operation will conflict at commit time.
210        let oneshot = WasmDbmsDatabase::oneshot(&ctx, TestSchema);
211        let conflicting = ItemInsertRequest::from_values(&[
212            (Item::columns()[0], Value::Uint32(Uint32(2))),
213            (
214                Item::columns()[1],
215                Value::Text(Text("conflict".to_string())),
216            ),
217        ])
218        .unwrap();
219        oneshot.insert::<Item>(conflicting).unwrap();
220
221        // Commit should fail: the first insert (PK=1) succeeds, but the
222        // second (PK=2) hits a primary key conflict.
223        let result = db.commit();
224        assert!(result.is_err());
225
226        // Verify that the first insert was also rolled back: only the
227        // conflicting row should remain.
228        let db = WasmDbmsDatabase::oneshot(&ctx, TestSchema);
229        let rows = db.select::<Item>(Query::builder().build()).unwrap();
230        assert_eq!(rows.len(), 1, "expected only the conflicting row");
231        assert_eq!(rows[0].id, Some(Uint32(2)));
232        assert_eq!(rows[0].name, Some(Text("conflict".to_string())));
233    }
234}