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;
52#[cfg(any(feature = "http-server", feature = "mcp-server"))]
53pub(crate) mod net;
54pub mod partiql;
55pub mod schema;
56#[cfg(feature = "http-server")]
57pub mod server;
58#[cfg(feature = "mcp-server")]
59pub(crate) mod snapshots;
60pub mod storage;
61pub mod storage_backend;
62pub mod streams;
63pub mod ttl;
64pub mod types;
65pub mod validation;
66#[cfg(any(feature = "http-server", feature = "wasm-sqlite", test))]
70pub(crate) mod dynamo_ops;
71#[cfg(any(feature = "wasm-sqlite", test))]
76pub mod wasm_api;
77#[cfg(feature = "wasm-harness")]
78pub mod wasm_harness;
79
80#[doc(hidden)]
81pub use macros::ItemInsert;
82
83use std::collections::HashMap;
84use std::sync::{Arc, Mutex};
85use web_time::Instant;
86
87pub use errors::{DynoxideError, Result};
88pub use storage::{DatabaseInfo, TableInfoEntry, TableMetadata, TableStats};
89pub use storage_backend::BackendError;
90#[cfg(feature = "wasm-sqlite")]
91pub use storage_backend::WasmBridgeBackend;
92pub use types::{AttributeValue, ConversionError, Item};
93
94#[derive(Debug, Clone, Default)]
96pub struct ImportOptions {
97 pub record_streams: bool,
99 pub set_cached_at: bool,
101}
102
103#[derive(Debug, Clone)]
105pub struct ImportResult {
106 pub items_imported: usize,
108 pub bytes_imported: usize,
110}
111
112type TransactWriteTokenCache = HashMap<
115 String,
116 (
117 Instant,
118 u64,
119 actions::transact_write_items::TransactWriteItemsResponse,
120 ),
121>;
122
123type ExecuteTransactionTokenCache = HashMap<
130 String,
131 (
132 Instant,
133 u64,
134 actions::execute_transaction::ExecuteTransactionResponse,
135 ),
136>;
137
138#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
141const MAX_TOKEN_LEN: usize = 36;
142
143#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
146const TOKEN_EXPIRY_SECS: u64 = 600;
147
148#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
165fn run_idempotent<T, H, E, R>(
166 cache: &Mutex<HashMap<String, (Instant, u64, T)>>,
167 token: Option<&str>,
168 hash_input: &H,
169 execute: E,
170 replay: R,
171) -> Result<T>
172where
173 T: Clone,
174 H: serde::Serialize,
175 E: FnOnce() -> Result<T>,
176 R: FnOnce(&T) -> T,
177{
178 if let Some(token) = token {
179 if token.len() > MAX_TOKEN_LEN {
180 return Err(DynoxideError::ValidationException(format!(
181 "1 validation error detected: Value '{token}' at 'clientRequestToken' failed to satisfy constraint: Member must have length less than or equal to {MAX_TOKEN_LEN}"
182 )));
183 }
184 }
185
186 let Some(token) = token else {
188 return execute();
189 };
190
191 let request_hash = {
194 use std::hash::{Hash, Hasher};
195 let normalised = serde_json::to_value(hash_input)
196 .and_then(|v| serde_json::to_vec(&v))
197 .unwrap_or_default();
198 let mut hasher = std::collections::hash_map::DefaultHasher::new();
199 normalised.hash(&mut hasher);
200 hasher.finish()
201 };
202
203 let mut cache = cache
204 .lock()
205 .map_err(|e| DynoxideError::InternalServerError(format!("Lock poisoned: {e}")))?;
206 cache.retain(|_, (ts, _, _)| ts.elapsed().as_secs() < TOKEN_EXPIRY_SECS);
208 if let Some((_, cached_hash, resp)) = cache.get(token) {
209 if *cached_hash != request_hash {
210 return Err(DynoxideError::IdempotentParameterMismatchException(
211 "An error occurred (IdempotentParameterMismatchException)".to_string(),
212 ));
213 }
214 let cached = resp.clone();
216 drop(cache);
217 return Ok(replay(&cached));
218 }
219 let resp = execute()?;
223 cache.insert(
224 token.to_string(),
225 (Instant::now(), request_hash, resp.clone()),
226 );
227 Ok(resp)
228}
229
230#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
235pub type RusqliteBackend = storage::Storage;
236
237#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
245pub type NativeDatabase = Database<RusqliteBackend>;
246
247#[cfg(feature = "wasm-sqlite")]
254pub type WasmDatabase = Database<WasmBridgeBackend>;
255
256#[cfg(feature = "wasm-sqlite")]
264pub const WASM_PREVIEW: bool = true;
265#[cfg(not(feature = "wasm-sqlite"))]
268pub const WASM_PREVIEW: bool = false;
269
270#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
279pub struct Database<S = RusqliteBackend> {
280 inner: Arc<Mutex<S>>,
281 idempotency_tokens: Arc<Mutex<TransactWriteTokenCache>>,
282 execute_transaction_tokens: Arc<Mutex<ExecuteTransactionTokenCache>>,
283}
284
285#[cfg(all(
291 not(any(feature = "native-sqlite", feature = "_has-encryption")),
292 feature = "wasm-sqlite"
293))]
294use async_lock::Mutex as BackendMutex;
295#[cfg(all(
296 not(any(feature = "native-sqlite", feature = "_has-encryption")),
297 not(feature = "wasm-sqlite")
298))]
299use std::sync::Mutex as BackendMutex;
300
301#[cfg(not(any(feature = "native-sqlite", feature = "_has-encryption")))]
311pub struct Database<S> {
312 inner: Arc<BackendMutex<S>>,
313 idempotency_tokens: Arc<Mutex<TransactWriteTokenCache>>,
314 execute_transaction_tokens: Arc<Mutex<ExecuteTransactionTokenCache>>,
315}
316
317impl<S> Clone for Database<S> {
319 fn clone(&self) -> Self {
320 Self {
321 inner: Arc::clone(&self.inner),
322 idempotency_tokens: Arc::clone(&self.idempotency_tokens),
323 execute_transaction_tokens: Arc::clone(&self.execute_transaction_tokens),
324 }
325 }
326}
327
328#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
329impl Database<RusqliteBackend> {
330 pub fn new(path: &str) -> Result<Self> {
332 let storage = storage::Storage::new(path)?;
333 Ok(Self {
334 inner: Arc::new(Mutex::new(storage)),
335 idempotency_tokens: Arc::new(Mutex::new(HashMap::new())),
336 execute_transaction_tokens: Arc::new(Mutex::new(HashMap::new())),
337 })
338 }
339
340 #[cfg(feature = "_has-encryption")]
361 pub fn new_encrypted(path: &str, key: &str) -> Result<Self> {
362 if key.len() != 64 || !key.bytes().all(|b| b.is_ascii_hexdigit()) {
363 return Err(DynoxideError::ValidationException(
364 "Encryption key must be a 64-character hex string (32 bytes)".to_string(),
365 ));
366 }
367
368 let storage = storage::Storage::new_encrypted(path, key)?;
369 Ok(Self {
370 inner: Arc::new(Mutex::new(storage)),
371 idempotency_tokens: Arc::new(Mutex::new(HashMap::new())),
372 execute_transaction_tokens: Arc::new(Mutex::new(HashMap::new())),
373 })
374 }
375
376 pub fn memory() -> Result<Self> {
378 let storage = storage::Storage::memory()?;
379 Ok(Self {
380 inner: Arc::new(Mutex::new(storage)),
381 idempotency_tokens: Arc::new(Mutex::new(HashMap::new())),
382 execute_transaction_tokens: Arc::new(Mutex::new(HashMap::new())),
383 })
384 }
385
386 pub(crate) fn with_storage<F, T>(&self, f: F) -> Result<T>
388 where
389 F: FnOnce(&storage::Storage) -> Result<T>,
390 {
391 let guard = self
392 .inner
393 .lock()
394 .map_err(|e| DynoxideError::InternalServerError(format!("Lock poisoned: {e}")))?;
395 f(&guard)
396 }
397
398 pub(crate) fn with_storage_mut<F, T>(&self, f: F) -> Result<T>
400 where
401 F: FnOnce(&mut storage::Storage) -> Result<T>,
402 {
403 let mut guard = self
404 .inner
405 .lock()
406 .map_err(|e| DynoxideError::InternalServerError(format!("Lock poisoned: {e}")))?;
407 f(&mut guard)
408 }
409
410 pub fn create_table(
416 &self,
417 request: actions::create_table::CreateTableRequest,
418 ) -> Result<actions::create_table::CreateTableResponse> {
419 self.with_storage(|s| pollster::block_on(actions::create_table::execute(s, request)))
420 }
421
422 pub fn delete_table(
424 &self,
425 request: actions::delete_table::DeleteTableRequest,
426 ) -> Result<actions::delete_table::DeleteTableResponse> {
427 self.with_storage(|s| pollster::block_on(actions::delete_table::execute(s, request)))
428 }
429
430 pub fn describe_table(
432 &self,
433 request: actions::describe_table::DescribeTableRequest,
434 ) -> Result<actions::describe_table::DescribeTableResponse> {
435 self.with_storage(|s| pollster::block_on(actions::describe_table::execute(s, request)))
436 }
437
438 pub fn update_table(
440 &self,
441 request: actions::update_table::UpdateTableRequest,
442 ) -> Result<actions::update_table::UpdateTableResponse> {
443 self.with_storage(|s| pollster::block_on(actions::update_table::execute(s, request)))
444 }
445
446 pub fn list_tables(
448 &self,
449 request: actions::list_tables::ListTablesRequest,
450 ) -> Result<actions::list_tables::ListTablesResponse> {
451 self.with_storage(|s| pollster::block_on(actions::list_tables::execute(s, request)))
452 }
453
454 pub fn tag_resource(
460 &self,
461 request: actions::tag_resource::TagResourceRequest,
462 ) -> Result<actions::tag_resource::TagResourceResponse> {
463 self.with_storage(|s| pollster::block_on(actions::tag_resource::execute(s, request)))
464 }
465
466 pub fn untag_resource(
468 &self,
469 request: actions::untag_resource::UntagResourceRequest,
470 ) -> Result<actions::untag_resource::UntagResourceResponse> {
471 self.with_storage(|s| pollster::block_on(actions::untag_resource::execute(s, request)))
472 }
473
474 pub fn list_tags_of_resource(
476 &self,
477 request: actions::list_tags_of_resource::ListTagsOfResourceRequest,
478 ) -> Result<actions::list_tags_of_resource::ListTagsOfResourceResponse> {
479 self.with_storage(|s| {
480 pollster::block_on(actions::list_tags_of_resource::execute(s, request))
481 })
482 }
483
484 pub fn put_item(
490 &self,
491 request: actions::put_item::PutItemRequest,
492 ) -> Result<actions::put_item::PutItemResponse> {
493 self.with_storage(|s| pollster::block_on(actions::put_item::execute(s, request)))
494 }
495
496 pub fn get_item(
498 &self,
499 request: actions::get_item::GetItemRequest,
500 ) -> Result<actions::get_item::GetItemResponse> {
501 self.with_storage(|s| pollster::block_on(actions::get_item::execute(s, request)))
502 }
503
504 pub fn delete_item(
506 &self,
507 request: actions::delete_item::DeleteItemRequest,
508 ) -> Result<actions::delete_item::DeleteItemResponse> {
509 self.with_storage(|s| pollster::block_on(actions::delete_item::execute(s, request)))
510 }
511
512 pub fn update_item(
514 &self,
515 request: actions::update_item::UpdateItemRequest,
516 ) -> Result<actions::update_item::UpdateItemResponse> {
517 self.with_storage(|s| pollster::block_on(actions::update_item::execute(s, request)))
518 }
519
520 pub fn batch_get_item(
526 &self,
527 request: actions::batch_get_item::BatchGetItemRequest,
528 ) -> Result<actions::batch_get_item::BatchGetItemResponse> {
529 self.with_storage(|s| pollster::block_on(actions::batch_get_item::execute(s, request)))
530 }
531
532 pub fn batch_write_item(
534 &self,
535 request: actions::batch_write_item::BatchWriteItemRequest,
536 ) -> Result<actions::batch_write_item::BatchWriteItemResponse> {
537 self.with_storage(|s| pollster::block_on(actions::batch_write_item::execute(s, request)))
538 }
539
540 pub fn import_items(
555 &self,
556 table_name: &str,
557 items: Vec<Item>,
558 options: ImportOptions,
559 ) -> Result<ImportResult> {
560 self.with_storage(|s| {
561 pollster::block_on(actions::import_items::execute(
562 s, table_name, items, &options,
563 ))
564 })
565 }
566
567 #[cfg(feature = "import")]
573 pub(crate) fn import_items_fresh(
574 &self,
575 table_name: &str,
576 items: Vec<Item>,
577 options: ImportOptions,
578 ) -> Result<ImportResult> {
579 self.with_storage(|s| {
580 pollster::block_on(actions::import_items::execute_skip_gsi_deletes(
581 s, table_name, items, &options,
582 ))
583 })
584 }
585
586 pub fn enable_bulk_loading(&self) -> Result<()> {
595 self.with_storage(|s| s.enable_bulk_loading())
596 }
597
598 pub fn disable_bulk_loading(&self) -> Result<()> {
600 self.with_storage(|s| s.disable_bulk_loading())
601 }
602
603 pub fn query(
609 &self,
610 request: actions::query::QueryRequest,
611 ) -> Result<actions::query::QueryResponse> {
612 self.with_storage(|s| pollster::block_on(actions::query::execute(s, request)))
613 }
614
615 pub fn scan(&self, request: actions::scan::ScanRequest) -> Result<actions::scan::ScanResponse> {
617 self.with_storage(|s| pollster::block_on(actions::scan::execute(s, request)))
618 }
619
620 pub fn transact_write_items(
631 &self,
632 request: actions::transact_write_items::TransactWriteItemsRequest,
633 ) -> Result<actions::transact_write_items::TransactWriteItemsResponse> {
634 run_idempotent(
635 &self.idempotency_tokens,
636 request.client_request_token.as_deref(),
637 &request.transact_items,
638 || {
639 self.with_storage(|s| {
640 pollster::block_on(actions::transact_write_items::execute(s, request.clone()))
641 })
642 },
643 |cached| {
644 actions::transact_write_items::replay_response(
649 &request.transact_items,
650 &request.return_consumed_capacity,
651 cached.item_collection_metrics.clone(),
652 )
653 },
654 )
655 }
656
657 pub fn transact_get_items(
659 &self,
660 request: actions::transact_get_items::TransactGetItemsRequest,
661 ) -> Result<actions::transact_get_items::TransactGetItemsResponse> {
662 self.with_storage(|s| pollster::block_on(actions::transact_get_items::execute(s, request)))
663 }
664
665 pub fn list_streams(
671 &self,
672 request: actions::list_streams::ListStreamsRequest,
673 ) -> Result<actions::list_streams::ListStreamsResponse> {
674 self.with_storage(|s| pollster::block_on(actions::list_streams::execute(s, request)))
675 }
676
677 pub fn describe_stream(
679 &self,
680 request: actions::describe_stream::DescribeStreamRequest,
681 ) -> Result<actions::describe_stream::DescribeStreamResponse> {
682 self.with_storage(|s| pollster::block_on(actions::describe_stream::execute(s, request)))
683 }
684
685 pub fn get_shard_iterator(
687 &self,
688 request: actions::get_shard_iterator::GetShardIteratorRequest,
689 ) -> Result<actions::get_shard_iterator::GetShardIteratorResponse> {
690 self.with_storage(|s| pollster::block_on(actions::get_shard_iterator::execute(s, request)))
691 }
692
693 pub fn get_records(
695 &self,
696 request: actions::get_records::GetRecordsRequest,
697 ) -> Result<actions::get_records::GetRecordsResponse> {
698 self.with_storage(|s| pollster::block_on(actions::get_records::execute(s, request)))
699 }
700
701 pub fn update_time_to_live(
707 &self,
708 request: actions::update_time_to_live::UpdateTimeToLiveRequest,
709 ) -> Result<actions::update_time_to_live::UpdateTimeToLiveResponse> {
710 self.with_storage(|s| pollster::block_on(actions::update_time_to_live::execute(s, request)))
711 }
712
713 pub fn describe_time_to_live(
715 &self,
716 request: actions::describe_time_to_live::DescribeTimeToLiveRequest,
717 ) -> Result<actions::describe_time_to_live::DescribeTimeToLiveResponse> {
718 self.with_storage(|s| {
719 pollster::block_on(actions::describe_time_to_live::execute(s, request))
720 })
721 }
722
723 pub fn sweep_ttl(&self) -> Result<usize> {
726 self.with_storage(|s| pollster::block_on(ttl::sweep_expired_items(s)))
727 }
728
729 pub fn execute_statement(
735 &self,
736 request: actions::execute_statement::ExecuteStatementRequest,
737 ) -> Result<actions::execute_statement::ExecuteStatementResponse> {
738 self.with_storage(|s| pollster::block_on(actions::execute_statement::execute(s, request)))
739 }
740
741 pub fn execute_transaction(
749 &self,
750 request: actions::execute_transaction::ExecuteTransactionRequest,
751 ) -> Result<actions::execute_transaction::ExecuteTransactionResponse> {
752 run_idempotent(
753 &self.execute_transaction_tokens,
754 request.client_request_token.as_deref(),
755 &request.transact_statements,
756 || {
757 self.with_storage(|s| {
758 pollster::block_on(actions::execute_transaction::execute(s, request.clone()))
759 })
760 },
761 |cached| {
762 actions::execute_transaction::replay_response(
765 &request.transact_statements,
766 &request.return_consumed_capacity,
767 cached.responses.clone(),
768 )
769 },
770 )
771 }
772
773 pub fn batch_execute_statement(
775 &self,
776 request: actions::batch_execute_statement::BatchExecuteStatementRequest,
777 ) -> Result<actions::batch_execute_statement::BatchExecuteStatementResponse> {
778 self.with_storage(|s| {
779 pollster::block_on(actions::batch_execute_statement::execute(s, request))
780 })
781 }
782
783 pub fn touch_cached_at(
792 &self,
793 table_name: &str,
794 pk: &str,
795 sk: &str,
796 timestamp: f64,
797 ) -> Result<()> {
798 self.with_storage(|s| s.touch_cached_at(table_name, pk, sk, timestamp))
799 }
800
801 pub fn get_lru_items(
806 &self,
807 table_name: &str,
808 limit: usize,
809 ) -> Result<Vec<(String, String, i64)>> {
810 self.with_storage(|s| s.get_lru_items(table_name, limit))
811 }
812
813 pub fn db_path(&self) -> Result<Option<String>> {
819 self.with_storage(|s| Ok(s.db_path()))
820 }
821
822 pub fn db_size_bytes(&self) -> Result<u64> {
824 self.with_storage(|s| s.db_size_bytes())
825 }
826
827 pub fn table_count(&self) -> Result<usize> {
829 self.with_storage(|s| s.table_count())
830 }
831
832 pub fn table_stats(&self) -> Result<Vec<TableStats>> {
834 self.with_storage(|s| s.table_stats())
835 }
836
837 pub fn get_table_metadata(&self, table_name: &str) -> Result<Option<storage::TableMetadata>> {
839 self.with_storage(|s| s.get_table_metadata(table_name))
840 }
841
842 pub fn database_info(&self) -> Result<DatabaseInfo> {
847 self.with_storage(|s| s.database_info())
848 }
849
850 pub fn vacuum(&self) -> Result<()> {
856 self.with_storage(|s| s.vacuum())
857 }
858
859 pub fn vacuum_into(&self, path: &str) -> Result<()> {
864 self.with_storage(|s| s.vacuum_into(path))
865 }
866
867 pub fn restore_from(&self, path: &str) -> Result<()> {
873 self.with_storage_mut(|s| s.restore_from(path))
874 }
875
876 #[cfg(feature = "mcp-server")]
881 pub(crate) fn backup_to_memory(&self) -> Result<rusqlite::Connection> {
882 self.with_storage(|s| s.backup_to_memory())
883 }
884
885 #[cfg(feature = "mcp-server")]
889 pub(crate) fn restore_from_connection(&self, source: &rusqlite::Connection) -> Result<()> {
890 self.with_storage_mut(|s| s.restore_from_connection(source))
891 }
892}
893
894#[cfg(feature = "wasm-sqlite")]
908impl Database<WasmBridgeBackend> {
909 pub async fn open(name: &str) -> Result<Self> {
912 Self::open_with(name, false).await
913 }
914
915 pub async fn open_with(name: &str, ephemeral: bool) -> Result<Self> {
918 let backend = WasmBridgeBackend::open_with(name, ephemeral)
919 .await
920 .map_err(DynoxideError::from)?;
921 Ok(Self {
922 inner: Arc::new(BackendMutex::new(backend)),
923 idempotency_tokens: Arc::new(Mutex::new(HashMap::new())),
924 execute_transaction_tokens: Arc::new(Mutex::new(HashMap::new())),
925 })
926 }
927
928 pub async fn persistence_mode(&self) -> String {
930 self.backend().await.persistence_mode().to_string()
931 }
932
933 pub async fn close(&self) -> Result<()> {
937 self.backend()
938 .await
939 .close()
940 .await
941 .map_err(DynoxideError::from)
942 }
943
944 pub(crate) async fn backend(&self) -> async_lock::MutexGuard<'_, WasmBridgeBackend> {
953 self.inner.lock().await
954 }
955
956 pub async fn create_table(
958 &self,
959 request: actions::create_table::CreateTableRequest,
960 ) -> Result<actions::create_table::CreateTableResponse> {
961 let backend = self.backend().await;
962 actions::create_table::execute(&*backend, request).await
963 }
964
965 pub async fn delete_table(
967 &self,
968 request: actions::delete_table::DeleteTableRequest,
969 ) -> Result<actions::delete_table::DeleteTableResponse> {
970 let backend = self.backend().await;
971 actions::delete_table::execute(&*backend, request).await
972 }
973
974 pub async fn describe_table(
976 &self,
977 request: actions::describe_table::DescribeTableRequest,
978 ) -> Result<actions::describe_table::DescribeTableResponse> {
979 let backend = self.backend().await;
980 actions::describe_table::execute(&*backend, request).await
981 }
982
983 pub async fn list_tables(
985 &self,
986 request: actions::list_tables::ListTablesRequest,
987 ) -> Result<actions::list_tables::ListTablesResponse> {
988 let backend = self.backend().await;
989 actions::list_tables::execute(&*backend, request).await
990 }
991
992 pub async fn put_item(
994 &self,
995 request: actions::put_item::PutItemRequest,
996 ) -> Result<actions::put_item::PutItemResponse> {
997 let backend = self.backend().await;
998 actions::put_item::execute(&*backend, request).await
999 }
1000
1001 pub async fn get_item(
1003 &self,
1004 request: actions::get_item::GetItemRequest,
1005 ) -> Result<actions::get_item::GetItemResponse> {
1006 let backend = self.backend().await;
1007 actions::get_item::execute(&*backend, request).await
1008 }
1009
1010 pub async fn delete_item(
1012 &self,
1013 request: actions::delete_item::DeleteItemRequest,
1014 ) -> Result<actions::delete_item::DeleteItemResponse> {
1015 let backend = self.backend().await;
1016 actions::delete_item::execute(&*backend, request).await
1017 }
1018
1019 pub async fn query(
1021 &self,
1022 request: actions::query::QueryRequest,
1023 ) -> Result<actions::query::QueryResponse> {
1024 let backend = self.backend().await;
1025 actions::query::execute(&*backend, request).await
1026 }
1027
1028 pub async fn scan(
1030 &self,
1031 request: actions::scan::ScanRequest,
1032 ) -> Result<actions::scan::ScanResponse> {
1033 let backend = self.backend().await;
1034 actions::scan::execute(&*backend, request).await
1035 }
1036}
1037
1038#[cfg(all(test, any(feature = "native-sqlite", feature = "_has-encryption")))]
1039mod tests {
1040 use super::*;
1041
1042 #[test]
1043 fn test_database_memory() {
1044 let db = Database::memory().unwrap();
1045 let _db2 = db.clone();
1047 }
1048
1049 #[test]
1050 fn test_database_with_storage() {
1051 let db = Database::memory().unwrap();
1052 let tables = db.with_storage(|s| s.list_table_names()).unwrap();
1053 assert!(tables.is_empty());
1054 }
1055
1056 #[test]
1057 fn test_database_thread_safe() {
1058 let db = Database::memory().unwrap();
1059 let db2 = db.clone();
1060
1061 let handle =
1062 std::thread::spawn(move || db2.with_storage(|s| s.list_table_names()).unwrap());
1063
1064 let tables = handle.join().unwrap();
1065 assert!(tables.is_empty());
1066 }
1067
1068 #[test]
1069 fn test_native_database_alias_round_trips() {
1070 let db: NativeDatabase = Database::memory().unwrap();
1074
1075 db.create_table(actions::create_table::CreateTableRequest {
1076 table_name: "tbl".to_string(),
1077 key_schema: vec![types::KeySchemaElement {
1078 attribute_name: "pk".to_string(),
1079 key_type: types::KeyType::HASH,
1080 }],
1081 attribute_definitions: vec![types::AttributeDefinition {
1082 attribute_name: "pk".to_string(),
1083 attribute_type: types::ScalarAttributeType::S,
1084 }],
1085 ..Default::default()
1086 })
1087 .unwrap();
1088
1089 let mut item = HashMap::new();
1090 item.insert("pk".to_string(), AttributeValue::S("a".to_string()));
1091 db.put_item(actions::put_item::PutItemRequest {
1092 table_name: "tbl".to_string(),
1093 item,
1094 ..Default::default()
1095 })
1096 .unwrap();
1097
1098 let mut key = HashMap::new();
1099 key.insert("pk".to_string(), AttributeValue::S("a".to_string()));
1100 let got = db
1101 .get_item(actions::get_item::GetItemRequest {
1102 table_name: "tbl".to_string(),
1103 key,
1104 ..Default::default()
1105 })
1106 .unwrap();
1107 assert_eq!(
1108 got.item.unwrap().get("pk"),
1109 Some(&AttributeValue::S("a".to_string()))
1110 );
1111 }
1112}