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(Debug, Table, Clone, PartialEq, Eq)]
110    #[table = "products"]
111    pub struct Product {
112        #[primary_key]
113        pub id: Uint32,
114        #[index]
115        pub sku: Text,
116        #[index(group = "category_brand")]
117        pub category: Text,
118        #[index(group = "category_brand")]
119        pub brand: Text,
120    }
121
122    #[derive(DatabaseSchema)]
123    #[tables(Item = "items")]
124    pub struct TestSchema;
125
126    fn setup() -> DbmsContext<HeapMemoryProvider> {
127        let ctx = DbmsContext::new(HeapMemoryProvider::default());
128        TestSchema::register_tables(&ctx).unwrap();
129        ctx
130    }
131
132    #[test]
133    fn test_should_register_tables_via_macro() {
134        let ctx = DbmsContext::new(HeapMemoryProvider::default());
135        TestSchema::register_tables(&ctx).unwrap();
136    }
137
138    #[test]
139    fn test_should_insert_and_select_via_schema() {
140        let ctx = setup();
141        let db = WasmDbmsDatabase::oneshot(&ctx, TestSchema);
142
143        let insert = ItemInsertRequest::from_values(&[
144            (Item::columns()[0], Value::Uint32(Uint32(1))),
145            (Item::columns()[1], Value::Text(Text("foo".to_string()))),
146        ])
147        .unwrap();
148        db.insert::<Item>(insert).unwrap();
149
150        let rows = TestSchema
151            .select(&db, "items", Query::builder().build())
152            .unwrap();
153        assert_eq!(rows.len(), 1);
154        assert_eq!(rows[0][1].1, Value::Text(Text("foo".to_string())));
155    }
156
157    #[test]
158    fn test_should_delete_via_schema() {
159        let ctx = setup();
160        let db = WasmDbmsDatabase::oneshot(&ctx, TestSchema);
161
162        let insert = ItemInsertRequest::from_values(&[
163            (Item::columns()[0], Value::Uint32(Uint32(1))),
164            (Item::columns()[1], Value::Text(Text("foo".to_string()))),
165        ])
166        .unwrap();
167        db.insert::<Item>(insert).unwrap();
168
169        let deleted = TestSchema
170            .delete(
171                &db,
172                "items",
173                wasm_dbms_api::prelude::DeleteBehavior::Restrict,
174                None,
175            )
176            .unwrap();
177        assert_eq!(deleted, 1);
178    }
179
180    #[test]
181    fn test_should_return_error_for_unknown_table() {
182        let ctx = setup();
183        let db = WasmDbmsDatabase::oneshot(&ctx, TestSchema);
184
185        let result = TestSchema.select(&db, "nonexistent", Query::builder().build());
186        assert!(result.is_err());
187    }
188
189    #[test]
190    fn test_should_return_referenced_tables() {
191        let refs = <TestSchema as super::DatabaseSchema<HeapMemoryProvider>>::referenced_tables(
192            &TestSchema,
193            "items",
194        );
195        assert!(refs.is_empty());
196    }
197
198    #[test]
199    fn test_commit_rolls_back_all_operations_on_failure() {
200        let ctx = setup();
201        let owner = vec![1, 2, 3];
202
203        // Begin a transaction and queue two inserts.
204        let tx_id = ctx.begin_transaction(owner);
205        let mut db = WasmDbmsDatabase::from_transaction(&ctx, TestSchema, tx_id);
206
207        let first = ItemInsertRequest::from_values(&[
208            (Item::columns()[0], Value::Uint32(Uint32(1))),
209            (Item::columns()[1], Value::Text(Text("first".to_string()))),
210        ])
211        .unwrap();
212        db.insert::<Item>(first).unwrap();
213
214        let second = ItemInsertRequest::from_values(&[
215            (Item::columns()[0], Value::Uint32(Uint32(2))),
216            (Item::columns()[1], Value::Text(Text("second".to_string()))),
217        ])
218        .unwrap();
219        db.insert::<Item>(second).unwrap();
220
221        // Before committing, insert PK=2 outside the transaction so the
222        // second operation will conflict at commit time.
223        let oneshot = WasmDbmsDatabase::oneshot(&ctx, TestSchema);
224        let conflicting = ItemInsertRequest::from_values(&[
225            (Item::columns()[0], Value::Uint32(Uint32(2))),
226            (
227                Item::columns()[1],
228                Value::Text(Text("conflict".to_string())),
229            ),
230        ])
231        .unwrap();
232        oneshot.insert::<Item>(conflicting).unwrap();
233
234        // Commit should fail: the first insert (PK=1) succeeds, but the
235        // second (PK=2) hits a primary key conflict.
236        let result = db.commit();
237        assert!(result.is_err());
238
239        // Verify that the first insert was also rolled back: only the
240        // conflicting row should remain.
241        let db = WasmDbmsDatabase::oneshot(&ctx, TestSchema);
242        let rows = db.select::<Item>(Query::builder().build()).unwrap();
243        assert_eq!(rows.len(), 1, "expected only the conflicting row");
244        assert_eq!(rows[0].id, Some(Uint32(2)));
245        assert_eq!(rows[0].name, Some(Text("conflict".to_string())));
246    }
247
248    #[test]
249    fn test_indexes_contains_pk_by_default() {
250        let indexes = Item::indexes();
251        assert_eq!(indexes.len(), 1);
252        assert_eq!(indexes[0].columns(), &["id"]);
253    }
254
255    #[test]
256    fn test_indexes_single_and_composite() {
257        let indexes = Product::indexes();
258        // [pk("id"), standalone("sku"), composite("category", "brand")]
259        assert_eq!(indexes.len(), 3);
260        assert_eq!(indexes[0].columns(), &["id"]);
261        assert_eq!(indexes[1].columns(), &["sku"]);
262        assert_eq!(indexes[2].columns(), &["category", "brand"]);
263    }
264}