1#[cfg(all(feature = "native-sqlite", feature = "_has-encryption"))]
12compile_error!(
13 "Features `native-sqlite` and `encryption`/`encryption-cc` are mutually exclusive.\n\
14 If you ran `cargo install`, use:\n \
15 cargo install dynoxide-rs --no-default-features --features encrypted-server\n\
16 If using as a library dependency, set `default-features = false` \
17 and enable only one backend."
18);
19
20#[cfg(all(feature = "encryption", feature = "encryption-cc"))]
21compile_error!(
22 "Features `encryption` and `encryption-cc` are mutually exclusive. \
23 Use `encryption` for vendored OpenSSL or `encryption-cc` for Apple CommonCrypto."
24);
25
26#[cfg(all(feature = "encryption-cc", not(target_vendor = "apple")))]
27compile_error!(
28 "The `encryption-cc` feature is intended for Apple platforms only (CommonCrypto). \
29 Use the `encryption` feature for vendored OpenSSL on non-Apple platforms."
30);
31
32#[cfg(not(any(
33 feature = "native-sqlite",
34 feature = "_has-encryption",
35 feature = "wasm-sqlite"
36)))]
37compile_error!(
38 "A storage backend feature must be enabled: `native-sqlite`, `encryption`, \
39 `encryption-cc`, or `wasm-sqlite`. Default features include `native-sqlite`. \
40 If you used `default-features = false`, add one of these features."
41);
42
43pub mod actions;
44pub mod errors;
45pub mod expressions;
46#[cfg(feature = "import")]
47pub mod import;
48#[doc(hidden)]
49pub mod macros;
50#[cfg(feature = "mcp-server")]
51pub mod mcp;
52pub mod partiql;
53pub mod schema;
54#[cfg(feature = "http-server")]
55pub mod server;
56#[cfg(feature = "mcp-server")]
57pub(crate) mod snapshots;
58pub mod storage;
59pub mod storage_backend;
60pub mod streams;
61pub mod ttl;
62pub mod types;
63pub mod validation;
64#[cfg(any(feature = "http-server", feature = "wasm-sqlite", test))]
68pub(crate) mod dynamo_ops;
69#[cfg(any(feature = "wasm-sqlite", test))]
74pub mod wasm_api;
75#[cfg(feature = "wasm-harness")]
76pub mod wasm_harness;
77
78#[doc(hidden)]
79pub use macros::ItemInsert;
80
81use std::collections::HashMap;
82use std::sync::{Arc, Mutex};
83use web_time::Instant;
84
85pub use errors::{DynoxideError, Result};
86pub use storage::{DatabaseInfo, TableInfoEntry, TableMetadata, TableStats};
87pub use storage_backend::BackendError;
88#[cfg(feature = "wasm-sqlite")]
89pub use storage_backend::WasmBridgeBackend;
90pub use types::{AttributeValue, ConversionError, Item};
91
92#[derive(Debug, Clone, Default)]
94pub struct ImportOptions {
95 pub record_streams: bool,
97 pub set_cached_at: bool,
99}
100
101#[derive(Debug, Clone)]
103pub struct ImportResult {
104 pub items_imported: usize,
106 pub bytes_imported: usize,
108}
109
110type TransactWriteTokenCache = HashMap<
113 String,
114 (
115 Instant,
116 u64,
117 actions::transact_write_items::TransactWriteItemsResponse,
118 ),
119>;
120
121type ExecuteTransactionTokenCache = HashMap<
128 String,
129 (
130 Instant,
131 u64,
132 actions::execute_transaction::ExecuteTransactionResponse,
133 ),
134>;
135
136#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
139const MAX_TOKEN_LEN: usize = 36;
140
141#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
144const TOKEN_EXPIRY_SECS: u64 = 600;
145
146#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
163fn run_idempotent<T, H, E, R>(
164 cache: &Mutex<HashMap<String, (Instant, u64, T)>>,
165 token: Option<&str>,
166 hash_input: &H,
167 execute: E,
168 replay: R,
169) -> Result<T>
170where
171 T: Clone,
172 H: serde::Serialize,
173 E: FnOnce() -> Result<T>,
174 R: FnOnce(&T) -> T,
175{
176 if let Some(token) = token {
177 if token.len() > MAX_TOKEN_LEN {
178 return Err(DynoxideError::ValidationException(format!(
179 "1 validation error detected: Value '{token}' at 'clientRequestToken' failed to satisfy constraint: Member must have length less than or equal to {MAX_TOKEN_LEN}"
180 )));
181 }
182 }
183
184 let Some(token) = token else {
186 return execute();
187 };
188
189 let request_hash = {
192 use std::hash::{Hash, Hasher};
193 let normalised = serde_json::to_value(hash_input)
194 .and_then(|v| serde_json::to_vec(&v))
195 .unwrap_or_default();
196 let mut hasher = std::collections::hash_map::DefaultHasher::new();
197 normalised.hash(&mut hasher);
198 hasher.finish()
199 };
200
201 let mut cache = cache
202 .lock()
203 .map_err(|e| DynoxideError::InternalServerError(format!("Lock poisoned: {e}")))?;
204 cache.retain(|_, (ts, _, _)| ts.elapsed().as_secs() < TOKEN_EXPIRY_SECS);
206 if let Some((_, cached_hash, resp)) = cache.get(token) {
207 if *cached_hash != request_hash {
208 return Err(DynoxideError::IdempotentParameterMismatchException(
209 "An error occurred (IdempotentParameterMismatchException)".to_string(),
210 ));
211 }
212 let cached = resp.clone();
214 drop(cache);
215 return Ok(replay(&cached));
216 }
217 let resp = execute()?;
221 cache.insert(
222 token.to_string(),
223 (Instant::now(), request_hash, resp.clone()),
224 );
225 Ok(resp)
226}
227
228#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
233pub type RusqliteBackend = storage::Storage;
234
235#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
243pub type NativeDatabase = Database<RusqliteBackend>;
244
245#[cfg(feature = "wasm-sqlite")]
252pub type WasmDatabase = Database<WasmBridgeBackend>;
253
254#[cfg(feature = "wasm-sqlite")]
262pub const WASM_PREVIEW: bool = true;
263#[cfg(not(feature = "wasm-sqlite"))]
266pub const WASM_PREVIEW: bool = false;
267
268#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
277pub struct Database<S = RusqliteBackend> {
278 inner: Arc<Mutex<S>>,
279 idempotency_tokens: Arc<Mutex<TransactWriteTokenCache>>,
280 execute_transaction_tokens: Arc<Mutex<ExecuteTransactionTokenCache>>,
281}
282
283#[cfg(all(
289 not(any(feature = "native-sqlite", feature = "_has-encryption")),
290 feature = "wasm-sqlite"
291))]
292use async_lock::Mutex as BackendMutex;
293#[cfg(all(
294 not(any(feature = "native-sqlite", feature = "_has-encryption")),
295 not(feature = "wasm-sqlite")
296))]
297use std::sync::Mutex as BackendMutex;
298
299#[cfg(not(any(feature = "native-sqlite", feature = "_has-encryption")))]
309pub struct Database<S> {
310 inner: Arc<BackendMutex<S>>,
311 idempotency_tokens: Arc<Mutex<TransactWriteTokenCache>>,
312 execute_transaction_tokens: Arc<Mutex<ExecuteTransactionTokenCache>>,
313}
314
315impl<S> Clone for Database<S> {
317 fn clone(&self) -> Self {
318 Self {
319 inner: Arc::clone(&self.inner),
320 idempotency_tokens: Arc::clone(&self.idempotency_tokens),
321 execute_transaction_tokens: Arc::clone(&self.execute_transaction_tokens),
322 }
323 }
324}
325
326#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
327impl Database<RusqliteBackend> {
328 pub fn new(path: &str) -> Result<Self> {
330 let storage = storage::Storage::new(path)?;
331 Ok(Self {
332 inner: Arc::new(Mutex::new(storage)),
333 idempotency_tokens: Arc::new(Mutex::new(HashMap::new())),
334 execute_transaction_tokens: Arc::new(Mutex::new(HashMap::new())),
335 })
336 }
337
338 #[cfg(feature = "_has-encryption")]
359 pub fn new_encrypted(path: &str, key: &str) -> Result<Self> {
360 if key.len() != 64 || !key.bytes().all(|b| b.is_ascii_hexdigit()) {
361 return Err(DynoxideError::ValidationException(
362 "Encryption key must be a 64-character hex string (32 bytes)".to_string(),
363 ));
364 }
365
366 let storage = storage::Storage::new_encrypted(path, key)?;
367 Ok(Self {
368 inner: Arc::new(Mutex::new(storage)),
369 idempotency_tokens: Arc::new(Mutex::new(HashMap::new())),
370 execute_transaction_tokens: Arc::new(Mutex::new(HashMap::new())),
371 })
372 }
373
374 pub fn memory() -> Result<Self> {
376 let storage = storage::Storage::memory()?;
377 Ok(Self {
378 inner: Arc::new(Mutex::new(storage)),
379 idempotency_tokens: Arc::new(Mutex::new(HashMap::new())),
380 execute_transaction_tokens: Arc::new(Mutex::new(HashMap::new())),
381 })
382 }
383
384 pub(crate) fn with_storage<F, T>(&self, f: F) -> Result<T>
386 where
387 F: FnOnce(&storage::Storage) -> Result<T>,
388 {
389 let guard = self
390 .inner
391 .lock()
392 .map_err(|e| DynoxideError::InternalServerError(format!("Lock poisoned: {e}")))?;
393 f(&guard)
394 }
395
396 pub(crate) fn with_storage_mut<F, T>(&self, f: F) -> Result<T>
398 where
399 F: FnOnce(&mut storage::Storage) -> Result<T>,
400 {
401 let mut guard = self
402 .inner
403 .lock()
404 .map_err(|e| DynoxideError::InternalServerError(format!("Lock poisoned: {e}")))?;
405 f(&mut guard)
406 }
407
408 pub fn create_table(
414 &self,
415 request: actions::create_table::CreateTableRequest,
416 ) -> Result<actions::create_table::CreateTableResponse> {
417 self.with_storage(|s| pollster::block_on(actions::create_table::execute(s, request)))
418 }
419
420 pub fn delete_table(
422 &self,
423 request: actions::delete_table::DeleteTableRequest,
424 ) -> Result<actions::delete_table::DeleteTableResponse> {
425 self.with_storage(|s| pollster::block_on(actions::delete_table::execute(s, request)))
426 }
427
428 pub fn describe_table(
430 &self,
431 request: actions::describe_table::DescribeTableRequest,
432 ) -> Result<actions::describe_table::DescribeTableResponse> {
433 self.with_storage(|s| pollster::block_on(actions::describe_table::execute(s, request)))
434 }
435
436 pub fn update_table(
438 &self,
439 request: actions::update_table::UpdateTableRequest,
440 ) -> Result<actions::update_table::UpdateTableResponse> {
441 self.with_storage(|s| pollster::block_on(actions::update_table::execute(s, request)))
442 }
443
444 pub fn list_tables(
446 &self,
447 request: actions::list_tables::ListTablesRequest,
448 ) -> Result<actions::list_tables::ListTablesResponse> {
449 self.with_storage(|s| pollster::block_on(actions::list_tables::execute(s, request)))
450 }
451
452 pub fn tag_resource(
458 &self,
459 request: actions::tag_resource::TagResourceRequest,
460 ) -> Result<actions::tag_resource::TagResourceResponse> {
461 self.with_storage(|s| pollster::block_on(actions::tag_resource::execute(s, request)))
462 }
463
464 pub fn untag_resource(
466 &self,
467 request: actions::untag_resource::UntagResourceRequest,
468 ) -> Result<actions::untag_resource::UntagResourceResponse> {
469 self.with_storage(|s| pollster::block_on(actions::untag_resource::execute(s, request)))
470 }
471
472 pub fn list_tags_of_resource(
474 &self,
475 request: actions::list_tags_of_resource::ListTagsOfResourceRequest,
476 ) -> Result<actions::list_tags_of_resource::ListTagsOfResourceResponse> {
477 self.with_storage(|s| {
478 pollster::block_on(actions::list_tags_of_resource::execute(s, request))
479 })
480 }
481
482 pub fn put_item(
488 &self,
489 request: actions::put_item::PutItemRequest,
490 ) -> Result<actions::put_item::PutItemResponse> {
491 self.with_storage(|s| pollster::block_on(actions::put_item::execute(s, request)))
492 }
493
494 pub fn get_item(
496 &self,
497 request: actions::get_item::GetItemRequest,
498 ) -> Result<actions::get_item::GetItemResponse> {
499 self.with_storage(|s| pollster::block_on(actions::get_item::execute(s, request)))
500 }
501
502 pub fn delete_item(
504 &self,
505 request: actions::delete_item::DeleteItemRequest,
506 ) -> Result<actions::delete_item::DeleteItemResponse> {
507 self.with_storage(|s| pollster::block_on(actions::delete_item::execute(s, request)))
508 }
509
510 pub fn update_item(
512 &self,
513 request: actions::update_item::UpdateItemRequest,
514 ) -> Result<actions::update_item::UpdateItemResponse> {
515 self.with_storage(|s| pollster::block_on(actions::update_item::execute(s, request)))
516 }
517
518 pub fn batch_get_item(
524 &self,
525 request: actions::batch_get_item::BatchGetItemRequest,
526 ) -> Result<actions::batch_get_item::BatchGetItemResponse> {
527 self.with_storage(|s| pollster::block_on(actions::batch_get_item::execute(s, request)))
528 }
529
530 pub fn batch_write_item(
532 &self,
533 request: actions::batch_write_item::BatchWriteItemRequest,
534 ) -> Result<actions::batch_write_item::BatchWriteItemResponse> {
535 self.with_storage(|s| pollster::block_on(actions::batch_write_item::execute(s, request)))
536 }
537
538 pub fn import_items(
553 &self,
554 table_name: &str,
555 items: Vec<Item>,
556 options: ImportOptions,
557 ) -> Result<ImportResult> {
558 self.with_storage(|s| {
559 pollster::block_on(actions::import_items::execute(
560 s, table_name, items, &options,
561 ))
562 })
563 }
564
565 #[cfg(feature = "import")]
571 pub(crate) fn import_items_fresh(
572 &self,
573 table_name: &str,
574 items: Vec<Item>,
575 options: ImportOptions,
576 ) -> Result<ImportResult> {
577 self.with_storage(|s| {
578 pollster::block_on(actions::import_items::execute_skip_gsi_deletes(
579 s, table_name, items, &options,
580 ))
581 })
582 }
583
584 pub fn enable_bulk_loading(&self) -> Result<()> {
593 self.with_storage(|s| s.enable_bulk_loading())
594 }
595
596 pub fn disable_bulk_loading(&self) -> Result<()> {
598 self.with_storage(|s| s.disable_bulk_loading())
599 }
600
601 pub fn query(
607 &self,
608 request: actions::query::QueryRequest,
609 ) -> Result<actions::query::QueryResponse> {
610 self.with_storage(|s| pollster::block_on(actions::query::execute(s, request)))
611 }
612
613 pub fn scan(&self, request: actions::scan::ScanRequest) -> Result<actions::scan::ScanResponse> {
615 self.with_storage(|s| pollster::block_on(actions::scan::execute(s, request)))
616 }
617
618 pub fn transact_write_items(
629 &self,
630 request: actions::transact_write_items::TransactWriteItemsRequest,
631 ) -> Result<actions::transact_write_items::TransactWriteItemsResponse> {
632 run_idempotent(
633 &self.idempotency_tokens,
634 request.client_request_token.as_deref(),
635 &request.transact_items,
636 || {
637 self.with_storage(|s| {
638 pollster::block_on(actions::transact_write_items::execute(s, request.clone()))
639 })
640 },
641 |cached| {
642 actions::transact_write_items::replay_response(
647 &request.transact_items,
648 &request.return_consumed_capacity,
649 cached.item_collection_metrics.clone(),
650 )
651 },
652 )
653 }
654
655 pub fn transact_get_items(
657 &self,
658 request: actions::transact_get_items::TransactGetItemsRequest,
659 ) -> Result<actions::transact_get_items::TransactGetItemsResponse> {
660 self.with_storage(|s| pollster::block_on(actions::transact_get_items::execute(s, request)))
661 }
662
663 pub fn list_streams(
669 &self,
670 request: actions::list_streams::ListStreamsRequest,
671 ) -> Result<actions::list_streams::ListStreamsResponse> {
672 self.with_storage(|s| pollster::block_on(actions::list_streams::execute(s, request)))
673 }
674
675 pub fn describe_stream(
677 &self,
678 request: actions::describe_stream::DescribeStreamRequest,
679 ) -> Result<actions::describe_stream::DescribeStreamResponse> {
680 self.with_storage(|s| pollster::block_on(actions::describe_stream::execute(s, request)))
681 }
682
683 pub fn get_shard_iterator(
685 &self,
686 request: actions::get_shard_iterator::GetShardIteratorRequest,
687 ) -> Result<actions::get_shard_iterator::GetShardIteratorResponse> {
688 self.with_storage(|s| pollster::block_on(actions::get_shard_iterator::execute(s, request)))
689 }
690
691 pub fn get_records(
693 &self,
694 request: actions::get_records::GetRecordsRequest,
695 ) -> Result<actions::get_records::GetRecordsResponse> {
696 self.with_storage(|s| pollster::block_on(actions::get_records::execute(s, request)))
697 }
698
699 pub fn update_time_to_live(
705 &self,
706 request: actions::update_time_to_live::UpdateTimeToLiveRequest,
707 ) -> Result<actions::update_time_to_live::UpdateTimeToLiveResponse> {
708 self.with_storage(|s| pollster::block_on(actions::update_time_to_live::execute(s, request)))
709 }
710
711 pub fn describe_time_to_live(
713 &self,
714 request: actions::describe_time_to_live::DescribeTimeToLiveRequest,
715 ) -> Result<actions::describe_time_to_live::DescribeTimeToLiveResponse> {
716 self.with_storage(|s| {
717 pollster::block_on(actions::describe_time_to_live::execute(s, request))
718 })
719 }
720
721 pub fn sweep_ttl(&self) -> Result<usize> {
724 self.with_storage(|s| pollster::block_on(ttl::sweep_expired_items(s)))
725 }
726
727 pub fn execute_statement(
733 &self,
734 request: actions::execute_statement::ExecuteStatementRequest,
735 ) -> Result<actions::execute_statement::ExecuteStatementResponse> {
736 self.with_storage(|s| pollster::block_on(actions::execute_statement::execute(s, request)))
737 }
738
739 pub fn execute_transaction(
747 &self,
748 request: actions::execute_transaction::ExecuteTransactionRequest,
749 ) -> Result<actions::execute_transaction::ExecuteTransactionResponse> {
750 run_idempotent(
751 &self.execute_transaction_tokens,
752 request.client_request_token.as_deref(),
753 &request.transact_statements,
754 || {
755 self.with_storage(|s| {
756 pollster::block_on(actions::execute_transaction::execute(s, request.clone()))
757 })
758 },
759 |cached| {
760 actions::execute_transaction::replay_response(
763 &request.transact_statements,
764 &request.return_consumed_capacity,
765 cached.responses.clone(),
766 )
767 },
768 )
769 }
770
771 pub fn batch_execute_statement(
773 &self,
774 request: actions::batch_execute_statement::BatchExecuteStatementRequest,
775 ) -> Result<actions::batch_execute_statement::BatchExecuteStatementResponse> {
776 self.with_storage(|s| {
777 pollster::block_on(actions::batch_execute_statement::execute(s, request))
778 })
779 }
780
781 pub fn touch_cached_at(
790 &self,
791 table_name: &str,
792 pk: &str,
793 sk: &str,
794 timestamp: f64,
795 ) -> Result<()> {
796 self.with_storage(|s| s.touch_cached_at(table_name, pk, sk, timestamp))
797 }
798
799 pub fn get_lru_items(
804 &self,
805 table_name: &str,
806 limit: usize,
807 ) -> Result<Vec<(String, String, i64)>> {
808 self.with_storage(|s| s.get_lru_items(table_name, limit))
809 }
810
811 pub fn db_path(&self) -> Result<Option<String>> {
817 self.with_storage(|s| Ok(s.db_path()))
818 }
819
820 pub fn db_size_bytes(&self) -> Result<u64> {
822 self.with_storage(|s| s.db_size_bytes())
823 }
824
825 pub fn table_count(&self) -> Result<usize> {
827 self.with_storage(|s| s.table_count())
828 }
829
830 pub fn table_stats(&self) -> Result<Vec<TableStats>> {
832 self.with_storage(|s| s.table_stats())
833 }
834
835 pub fn get_table_metadata(&self, table_name: &str) -> Result<Option<storage::TableMetadata>> {
837 self.with_storage(|s| s.get_table_metadata(table_name))
838 }
839
840 pub fn database_info(&self) -> Result<DatabaseInfo> {
845 self.with_storage(|s| s.database_info())
846 }
847
848 pub fn vacuum(&self) -> Result<()> {
854 self.with_storage(|s| s.vacuum())
855 }
856
857 pub fn vacuum_into(&self, path: &str) -> Result<()> {
862 self.with_storage(|s| s.vacuum_into(path))
863 }
864
865 pub fn restore_from(&self, path: &str) -> Result<()> {
871 self.with_storage_mut(|s| s.restore_from(path))
872 }
873
874 #[cfg(feature = "mcp-server")]
879 pub(crate) fn backup_to_memory(&self) -> Result<rusqlite::Connection> {
880 self.with_storage(|s| s.backup_to_memory())
881 }
882
883 #[cfg(feature = "mcp-server")]
887 pub(crate) fn restore_from_connection(&self, source: &rusqlite::Connection) -> Result<()> {
888 self.with_storage_mut(|s| s.restore_from_connection(source))
889 }
890}
891
892#[cfg(feature = "wasm-sqlite")]
906impl Database<WasmBridgeBackend> {
907 pub async fn open(name: &str) -> Result<Self> {
910 Self::open_with(name, false).await
911 }
912
913 pub async fn open_with(name: &str, ephemeral: bool) -> Result<Self> {
916 let backend = WasmBridgeBackend::open_with(name, ephemeral)
917 .await
918 .map_err(DynoxideError::from)?;
919 Ok(Self {
920 inner: Arc::new(BackendMutex::new(backend)),
921 idempotency_tokens: Arc::new(Mutex::new(HashMap::new())),
922 execute_transaction_tokens: Arc::new(Mutex::new(HashMap::new())),
923 })
924 }
925
926 pub async fn persistence_mode(&self) -> String {
928 self.backend().await.persistence_mode().to_string()
929 }
930
931 pub async fn close(&self) -> Result<()> {
935 self.backend()
936 .await
937 .close()
938 .await
939 .map_err(DynoxideError::from)
940 }
941
942 pub(crate) async fn backend(&self) -> async_lock::MutexGuard<'_, WasmBridgeBackend> {
951 self.inner.lock().await
952 }
953
954 pub async fn create_table(
956 &self,
957 request: actions::create_table::CreateTableRequest,
958 ) -> Result<actions::create_table::CreateTableResponse> {
959 let backend = self.backend().await;
960 actions::create_table::execute(&*backend, request).await
961 }
962
963 pub async fn delete_table(
965 &self,
966 request: actions::delete_table::DeleteTableRequest,
967 ) -> Result<actions::delete_table::DeleteTableResponse> {
968 let backend = self.backend().await;
969 actions::delete_table::execute(&*backend, request).await
970 }
971
972 pub async fn describe_table(
974 &self,
975 request: actions::describe_table::DescribeTableRequest,
976 ) -> Result<actions::describe_table::DescribeTableResponse> {
977 let backend = self.backend().await;
978 actions::describe_table::execute(&*backend, request).await
979 }
980
981 pub async fn list_tables(
983 &self,
984 request: actions::list_tables::ListTablesRequest,
985 ) -> Result<actions::list_tables::ListTablesResponse> {
986 let backend = self.backend().await;
987 actions::list_tables::execute(&*backend, request).await
988 }
989
990 pub async fn put_item(
992 &self,
993 request: actions::put_item::PutItemRequest,
994 ) -> Result<actions::put_item::PutItemResponse> {
995 let backend = self.backend().await;
996 actions::put_item::execute(&*backend, request).await
997 }
998
999 pub async fn get_item(
1001 &self,
1002 request: actions::get_item::GetItemRequest,
1003 ) -> Result<actions::get_item::GetItemResponse> {
1004 let backend = self.backend().await;
1005 actions::get_item::execute(&*backend, request).await
1006 }
1007
1008 pub async fn delete_item(
1010 &self,
1011 request: actions::delete_item::DeleteItemRequest,
1012 ) -> Result<actions::delete_item::DeleteItemResponse> {
1013 let backend = self.backend().await;
1014 actions::delete_item::execute(&*backend, request).await
1015 }
1016
1017 pub async fn query(
1019 &self,
1020 request: actions::query::QueryRequest,
1021 ) -> Result<actions::query::QueryResponse> {
1022 let backend = self.backend().await;
1023 actions::query::execute(&*backend, request).await
1024 }
1025
1026 pub async fn scan(
1028 &self,
1029 request: actions::scan::ScanRequest,
1030 ) -> Result<actions::scan::ScanResponse> {
1031 let backend = self.backend().await;
1032 actions::scan::execute(&*backend, request).await
1033 }
1034}
1035
1036#[cfg(all(test, any(feature = "native-sqlite", feature = "_has-encryption")))]
1037mod tests {
1038 use super::*;
1039
1040 #[test]
1041 fn test_database_memory() {
1042 let db = Database::memory().unwrap();
1043 let _db2 = db.clone();
1045 }
1046
1047 #[test]
1048 fn test_database_with_storage() {
1049 let db = Database::memory().unwrap();
1050 let tables = db.with_storage(|s| s.list_table_names()).unwrap();
1051 assert!(tables.is_empty());
1052 }
1053
1054 #[test]
1055 fn test_database_thread_safe() {
1056 let db = Database::memory().unwrap();
1057 let db2 = db.clone();
1058
1059 let handle =
1060 std::thread::spawn(move || db2.with_storage(|s| s.list_table_names()).unwrap());
1061
1062 let tables = handle.join().unwrap();
1063 assert!(tables.is_empty());
1064 }
1065
1066 #[test]
1067 fn test_native_database_alias_round_trips() {
1068 let db: NativeDatabase = Database::memory().unwrap();
1072
1073 db.create_table(actions::create_table::CreateTableRequest {
1074 table_name: "tbl".to_string(),
1075 key_schema: vec![types::KeySchemaElement {
1076 attribute_name: "pk".to_string(),
1077 key_type: types::KeyType::HASH,
1078 }],
1079 attribute_definitions: vec![types::AttributeDefinition {
1080 attribute_name: "pk".to_string(),
1081 attribute_type: types::ScalarAttributeType::S,
1082 }],
1083 ..Default::default()
1084 })
1085 .unwrap();
1086
1087 let mut item = HashMap::new();
1088 item.insert("pk".to_string(), AttributeValue::S("a".to_string()));
1089 db.put_item(actions::put_item::PutItemRequest {
1090 table_name: "tbl".to_string(),
1091 item,
1092 ..Default::default()
1093 })
1094 .unwrap();
1095
1096 let mut key = HashMap::new();
1097 key.insert("pk".to_string(), AttributeValue::S("a".to_string()));
1098 let got = db
1099 .get_item(actions::get_item::GetItemRequest {
1100 table_name: "tbl".to_string(),
1101 key,
1102 ..Default::default()
1103 })
1104 .unwrap();
1105 assert_eq!(
1106 got.item.unwrap().get("pk"),
1107 Some(&AttributeValue::S("a".to_string()))
1108 );
1109 }
1110}