1use 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
11pub trait DatabaseSchema<M, A = AccessControlList>
20where
21 M: MemoryProvider,
22 A: AccessControl,
23{
24 fn select(
26 &self,
27 dbms: &WasmDbmsDatabase<'_, M, A>,
28 table_name: &str,
29 query: Query,
30 ) -> DbmsResult<Vec<Vec<(ColumnDef, Value)>>>;
31
32 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 fn referenced_tables(&self, table: &'static str) -> Vec<(&'static str, Vec<&'static str>)>;
45
46 fn insert(
48 &self,
49 dbms: &WasmDbmsDatabase<'_, M, A>,
50 table_name: &'static str,
51 record_values: &[(ColumnDef, Value)],
52 ) -> DbmsResult<()>;
53
54 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 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 fn validate_insert(
74 &self,
75 dbms: &WasmDbmsDatabase<'_, M, A>,
76 table_name: &'static str,
77 record_values: &[(ColumnDef, Value)],
78 ) -> DbmsResult<()>;
79
80 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 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 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 let result = db.commit();
237 assert!(result.is_err());
238
239 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 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}