1use wasm_dbms_api::prelude::{
5 AggregateFunction, AggregatedRow, ColumnDef, DbmsResult, DeleteBehavior, Filter, JoinColumnDef,
6 Query, TableSchemaSnapshot, Value,
7};
8use wasm_dbms_memory::prelude::{AccessControl, AccessControlList, MemoryProvider};
9
10use crate::database::WasmDbmsDatabase;
11
12pub trait DatabaseSchema<M, A = AccessControlList>
21where
22 M: MemoryProvider,
23 A: AccessControl,
24{
25 fn select(
27 &self,
28 dbms: &WasmDbmsDatabase<'_, M, A>,
29 table_name: &str,
30 query: Query,
31 ) -> DbmsResult<Vec<Vec<(ColumnDef, Value)>>>;
32
33 fn select_join(
36 &self,
37 dbms: &WasmDbmsDatabase<'_, M, A>,
38 from_table: &str,
39 query: Query,
40 ) -> DbmsResult<Vec<Vec<(JoinColumnDef, Value)>>> {
41 crate::join::JoinEngine::new(self).join(dbms, from_table, query)
42 }
43
44 fn aggregate(
46 &self,
47 dbms: &WasmDbmsDatabase<'_, M, A>,
48 table_name: &str,
49 query: Query,
50 aggregates: &[AggregateFunction],
51 ) -> DbmsResult<Vec<AggregatedRow>>;
52
53 fn referenced_tables(&self, table: &'static str) -> Vec<(&'static str, Vec<&'static str>)>;
55
56 fn insert(
58 &self,
59 dbms: &WasmDbmsDatabase<'_, M, A>,
60 table_name: &'static str,
61 record_values: &[(ColumnDef, Value)],
62 ) -> DbmsResult<()>;
63
64 fn delete(
66 &self,
67 dbms: &WasmDbmsDatabase<'_, M, A>,
68 table_name: &'static str,
69 delete_behavior: DeleteBehavior,
70 filter: Option<Filter>,
71 ) -> DbmsResult<u64>;
72
73 fn update(
75 &self,
76 dbms: &WasmDbmsDatabase<'_, M, A>,
77 table_name: &'static str,
78 patch_values: &[(ColumnDef, Value)],
79 filter: Option<Filter>,
80 ) -> DbmsResult<u64>;
81
82 fn validate_insert(
84 &self,
85 dbms: &WasmDbmsDatabase<'_, M, A>,
86 table_name: &'static str,
87 record_values: &[(ColumnDef, Value)],
88 ) -> DbmsResult<()>;
89
90 fn validate_update(
92 &self,
93 dbms: &WasmDbmsDatabase<'_, M, A>,
94 table_name: &'static str,
95 record_values: &[(ColumnDef, Value)],
96 old_pk: Value,
97 ) -> DbmsResult<()>;
98
99 fn migrate_default(table: &str, column: &str) -> Option<Value>
107 where
108 Self: Sized;
109
110 fn migrate_default_dyn(&self, table: &str, column: &str) -> Option<Value>;
116
117 fn migrate_transform(table: &str, column: &str, old: Value) -> DbmsResult<Option<Value>>
122 where
123 Self: Sized;
124
125 fn migrate_transform_dyn(
127 &self,
128 table: &str,
129 column: &str,
130 old: Value,
131 ) -> DbmsResult<Option<Value>>;
132
133 fn compiled_snapshots() -> Vec<TableSchemaSnapshot>
139 where
140 Self: Sized;
141
142 fn compiled_snapshots_dyn(&self) -> Vec<TableSchemaSnapshot>;
147
148 fn renamed_from_dyn(&self, table: &str, column: &str) -> Vec<&'static str>;
154}
155
156#[cfg(test)]
157mod tests {
158 use wasm_dbms_api::prelude::{
159 Database as _, DbmsResult, InsertRecord as _, Migrate, Query, TableSchema as _, Text,
160 Uint32, Value,
161 };
162 use wasm_dbms_macros::{DatabaseSchema, Table};
163 use wasm_dbms_memory::prelude::HeapMemoryProvider;
164
165 use super::DatabaseSchema as _;
166 use crate::prelude::{DbmsContext, WasmDbmsDatabase};
167
168 #[derive(Debug, Table, Clone, PartialEq, Eq)]
169 #[table = "items"]
170 pub struct Item {
171 #[primary_key]
172 pub id: Uint32,
173 pub name: Text,
174 }
175
176 #[derive(Debug, Table, Clone, PartialEq, Eq)]
177 #[table = "products"]
178 pub struct Product {
179 #[primary_key]
180 pub id: Uint32,
181 #[index]
182 pub sku: Text,
183 #[index(group = "category_brand")]
184 pub category: Text,
185 #[index(group = "category_brand")]
186 pub brand: Text,
187 }
188
189 #[derive(Debug, Table, Clone, PartialEq, Eq)]
193 #[table = "score_defaulted"]
194 pub struct ScoreDefaulted {
195 #[primary_key]
196 pub id: Uint32,
197 #[default = 42]
198 pub score: Uint32,
199 }
200
201 #[derive(Debug, Table, Clone, PartialEq, Eq)]
205 #[table = "renamed_table"]
206 pub struct RenamedTable {
207 #[primary_key]
208 pub id: Uint32,
209 #[renamed_from("old_name", "older_name")]
210 pub name: Text,
211 }
212
213 #[derive(Debug, Table, Clone, PartialEq, Eq)]
217 #[table = "custom_migrate"]
218 #[migrate]
219 pub struct CustomMigrate {
220 #[primary_key]
221 pub id: Uint32,
222 pub label: Text,
223 }
224
225 impl Migrate for CustomMigrate {
226 fn default_value(column: &str) -> Option<Value> {
227 if column == "label" {
228 Some(Value::Text(Text("user-default".to_string())))
229 } else {
230 None
231 }
232 }
233
234 fn transform_column(column: &str, _old: Value) -> DbmsResult<Option<Value>> {
235 if column == "label" {
236 Ok(Some(Value::Text(Text("transformed".to_string()))))
237 } else {
238 Ok(None)
239 }
240 }
241 }
242
243 #[derive(DatabaseSchema)]
244 #[tables(Item = "items")]
245 pub struct TestSchema;
246
247 #[derive(DatabaseSchema)]
251 #[tables(
252 Item = "items",
253 ScoreDefaulted = "score_defaulted",
254 RenamedTable = "renamed_table",
255 CustomMigrate = "custom_migrate"
256 )]
257 pub struct MigrationSchema;
258
259 fn setup() -> DbmsContext<HeapMemoryProvider> {
260 let ctx = DbmsContext::new(HeapMemoryProvider::default());
261 TestSchema::register_tables(&ctx).unwrap();
262 ctx
263 }
264
265 #[test]
266 fn test_should_register_tables_via_macro() {
267 let ctx = DbmsContext::new(HeapMemoryProvider::default());
268 TestSchema::register_tables(&ctx).unwrap();
269 }
270
271 #[test]
272 fn test_should_insert_and_select_via_schema() {
273 let ctx = setup();
274 let db = WasmDbmsDatabase::oneshot(&ctx, TestSchema);
275
276 let insert = ItemInsertRequest::from_values(&[
277 (Item::columns()[0], Value::Uint32(Uint32(1))),
278 (Item::columns()[1], Value::Text(Text("foo".to_string()))),
279 ])
280 .unwrap();
281 db.insert::<Item>(insert).unwrap();
282
283 let rows = TestSchema
284 .select(&db, "items", Query::builder().build())
285 .unwrap();
286 assert_eq!(rows.len(), 1);
287 assert_eq!(rows[0][1].1, Value::Text(Text("foo".to_string())));
288 }
289
290 #[test]
291 fn test_should_delete_via_schema() {
292 let ctx = setup();
293 let db = WasmDbmsDatabase::oneshot(&ctx, TestSchema);
294
295 let insert = ItemInsertRequest::from_values(&[
296 (Item::columns()[0], Value::Uint32(Uint32(1))),
297 (Item::columns()[1], Value::Text(Text("foo".to_string()))),
298 ])
299 .unwrap();
300 db.insert::<Item>(insert).unwrap();
301
302 let deleted = TestSchema
303 .delete(
304 &db,
305 "items",
306 wasm_dbms_api::prelude::DeleteBehavior::Restrict,
307 None,
308 )
309 .unwrap();
310 assert_eq!(deleted, 1);
311 }
312
313 #[test]
314 fn test_should_return_error_for_unknown_table() {
315 let ctx = setup();
316 let db = WasmDbmsDatabase::oneshot(&ctx, TestSchema);
317
318 let result = TestSchema.select(&db, "nonexistent", Query::builder().build());
319 assert!(result.is_err());
320 }
321
322 #[test]
323 fn test_should_return_referenced_tables() {
324 let refs = <TestSchema as super::DatabaseSchema<HeapMemoryProvider>>::referenced_tables(
325 &TestSchema,
326 "items",
327 );
328 assert!(refs.is_empty());
329 }
330
331 #[test]
332 fn test_commit_rolls_back_all_operations_on_failure() {
333 let ctx = setup();
334 let owner = vec![1, 2, 3];
335
336 let tx_id = ctx.begin_transaction(owner);
338 let mut db = WasmDbmsDatabase::from_transaction(&ctx, TestSchema, tx_id);
339
340 let first = ItemInsertRequest::from_values(&[
341 (Item::columns()[0], Value::Uint32(Uint32(1))),
342 (Item::columns()[1], Value::Text(Text("first".to_string()))),
343 ])
344 .unwrap();
345 db.insert::<Item>(first).unwrap();
346
347 let second = ItemInsertRequest::from_values(&[
348 (Item::columns()[0], Value::Uint32(Uint32(2))),
349 (Item::columns()[1], Value::Text(Text("second".to_string()))),
350 ])
351 .unwrap();
352 db.insert::<Item>(second).unwrap();
353
354 let oneshot = WasmDbmsDatabase::oneshot(&ctx, TestSchema);
357 let conflicting = ItemInsertRequest::from_values(&[
358 (Item::columns()[0], Value::Uint32(Uint32(2))),
359 (
360 Item::columns()[1],
361 Value::Text(Text("conflict".to_string())),
362 ),
363 ])
364 .unwrap();
365 oneshot.insert::<Item>(conflicting).unwrap();
366
367 let result = db.commit();
370 assert!(result.is_err());
371
372 let db = WasmDbmsDatabase::oneshot(&ctx, TestSchema);
375 let rows = db.select::<Item>(Query::builder().build()).unwrap();
376 assert_eq!(rows.len(), 1, "expected only the conflicting row");
377 assert_eq!(rows[0].id, Some(Uint32(2)));
378 assert_eq!(rows[0].name, Some(Text("conflict".to_string())));
379 }
380
381 #[test]
382 fn test_indexes_contains_pk_by_default() {
383 let indexes = Item::indexes();
384 assert_eq!(indexes.len(), 1);
385 assert_eq!(indexes[0].columns(), &["id"]);
386 }
387
388 #[test]
389 fn test_indexes_single_and_composite() {
390 let indexes = Product::indexes();
391 assert_eq!(indexes.len(), 3);
393 assert_eq!(indexes[0].columns(), &["id"]);
394 assert_eq!(indexes[1].columns(), &["sku"]);
395 assert_eq!(indexes[2].columns(), &["category", "brand"]);
396 }
397
398 #[test]
404 fn test_default_attribute_emits_constructor_on_column_def() {
405 let columns = ScoreDefaulted::columns();
406 let id = columns.iter().find(|c| c.name == "id").unwrap();
407 let score = columns.iter().find(|c| c.name == "score").unwrap();
408
409 assert!(id.default.is_none(), "id has no #[default]");
410 let ctor = score.default.expect("score must have a default");
411 assert_eq!(ctor(), Value::Uint32(Uint32(42)));
412 }
413
414 #[test]
417 fn test_renamed_from_attribute_populates_slice() {
418 let columns = RenamedTable::columns();
419 let id = columns.iter().find(|c| c.name == "id").unwrap();
420 let name = columns.iter().find(|c| c.name == "name").unwrap();
421
422 assert!(id.renamed_from.is_empty());
423 assert_eq!(name.renamed_from, &["old_name", "older_name"]);
424 }
425
426 #[test]
429 fn test_table_macro_emits_default_migrate_impl() {
430 assert_eq!(
431 <Item as Migrate>::default_value("id"),
432 None,
433 "default Migrate returns None for default_value"
434 );
435 assert!(matches!(
436 <Item as Migrate>::transform_column("id", Value::Uint32(Uint32(7))),
437 Ok(None)
438 ));
439 }
440
441 #[test]
444 fn test_migrate_struct_attribute_uses_user_impl() {
445 assert_eq!(
446 <CustomMigrate as Migrate>::default_value("label"),
447 Some(Value::Text(Text("user-default".to_string())))
448 );
449 assert_eq!(<CustomMigrate as Migrate>::default_value("id"), None);
450
451 let transformed =
452 <CustomMigrate as Migrate>::transform_column("label", Value::Text(Text("x".into())))
453 .expect("transform_column must succeed");
454 assert_eq!(transformed, Some(Value::Text(Text("transformed".into()))));
455 }
456
457 #[test]
461 fn test_migrate_default_dispatch_falls_back_to_column_default() {
462 let value = <MigrationSchema as super::DatabaseSchema<HeapMemoryProvider>>::migrate_default(
463 "score_defaulted",
464 "score",
465 );
466 assert_eq!(value, Some(Value::Uint32(Uint32(42))));
467 }
468
469 #[test]
472 fn test_migrate_default_dispatch_uses_user_override() {
473 let value = <MigrationSchema as super::DatabaseSchema<HeapMemoryProvider>>::migrate_default(
474 "custom_migrate",
475 "label",
476 );
477 assert_eq!(value, Some(Value::Text(Text("user-default".to_string()))));
478 }
479
480 #[test]
483 fn test_migrate_default_dispatch_unknown_table_returns_none() {
484 let value = <MigrationSchema as super::DatabaseSchema<HeapMemoryProvider>>::migrate_default(
485 "nonexistent",
486 "anything",
487 );
488 assert!(value.is_none());
489 }
490
491 #[test]
494 fn test_migrate_default_dispatch_known_table_unknown_column_returns_none() {
495 let value = <MigrationSchema as super::DatabaseSchema<HeapMemoryProvider>>::migrate_default(
496 "items", "name",
497 );
498 assert!(value.is_none());
499 }
500
501 #[test]
504 fn test_migrate_transform_dispatch_uses_user_override() {
505 let value =
506 <MigrationSchema as super::DatabaseSchema<HeapMemoryProvider>>::migrate_transform(
507 "custom_migrate",
508 "label",
509 Value::Text(Text("x".into())),
510 )
511 .expect("transform must succeed");
512 assert_eq!(value, Some(Value::Text(Text("transformed".into()))));
513 }
514
515 #[test]
518 fn test_migrate_transform_dispatch_default_impl_returns_none() {
519 let value =
520 <MigrationSchema as super::DatabaseSchema<HeapMemoryProvider>>::migrate_transform(
521 "items",
522 "id",
523 Value::Uint32(Uint32(1)),
524 )
525 .expect("transform must succeed");
526 assert!(value.is_none());
527 }
528
529 #[test]
532 fn test_migrate_transform_dispatch_unknown_table_errors() {
533 let result =
534 <MigrationSchema as super::DatabaseSchema<HeapMemoryProvider>>::migrate_transform(
535 "nonexistent",
536 "anything",
537 Value::Null,
538 );
539 assert!(result.is_err());
540 }
541
542 #[test]
547 fn test_compiled_snapshots_one_per_table_in_order() {
548 let snapshots =
549 <MigrationSchema as super::DatabaseSchema<HeapMemoryProvider>>::compiled_snapshots();
550 assert_eq!(snapshots.len(), 4);
551 assert_eq!(
552 snapshots
553 .iter()
554 .map(|s| s.name.as_str())
555 .collect::<Vec<_>>(),
556 vec![
557 "items",
558 "score_defaulted",
559 "renamed_table",
560 "custom_migrate"
561 ],
562 );
563
564 let score_snapshot = snapshots
565 .iter()
566 .find(|s| s.name == "score_defaulted")
567 .unwrap();
568 let score_col = score_snapshot
569 .columns
570 .iter()
571 .find(|c| c.name == "score")
572 .unwrap();
573 assert_eq!(score_col.default, Some(Value::Uint32(Uint32(42))));
574 assert_eq!(score_snapshot.primary_key, "id");
575 }
576}