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 auth_material;
45pub mod errors;
46pub mod expressions;
47#[cfg(feature = "import")]
48pub mod import;
49#[doc(hidden)]
50pub mod macros;
51#[cfg(feature = "mcp-server")]
52pub mod mcp;
53#[cfg(any(feature = "http-server", feature = "mcp-server"))]
54pub(crate) mod net;
55pub mod partiql;
56pub mod schema;
57pub(crate) mod serde_errors;
61#[cfg(feature = "http-server")]
62pub mod server;
63#[cfg(feature = "mcp-server")]
64pub(crate) mod snapshots;
65pub mod storage;
66pub mod storage_backend;
67pub mod streams;
68pub mod ttl;
69pub mod types;
70pub mod validation;
71#[cfg(any(feature = "http-server", feature = "wasm-sqlite", test))]
75pub(crate) mod dynamo_ops;
76#[cfg(any(feature = "wasm-sqlite", test))]
81pub mod wasm_api;
82#[cfg(feature = "wasm-harness")]
83pub mod wasm_harness;
84
85#[doc(hidden)]
86pub use macros::ItemInsert;
87
88use std::collections::HashMap;
89use std::sync::{Arc, Mutex};
90use web_time::Instant;
91
92pub use errors::{DynoxideError, Result};
93pub use storage::{DatabaseInfo, TableInfoEntry, TableMetadata, TableStats};
94pub use storage_backend::BackendError;
95#[cfg(feature = "wasm-sqlite")]
96pub use storage_backend::WasmBridgeBackend;
97pub use types::{AttributeValue, ConversionError, Item};
98
99#[derive(Debug, Clone, Default)]
101pub struct ImportOptions {
102 pub record_streams: bool,
104 pub set_cached_at: bool,
106}
107
108#[derive(Debug, Clone)]
110pub struct ImportResult {
111 pub items_imported: usize,
113 pub bytes_imported: usize,
115}
116
117type TransactWriteTokenCache = HashMap<
120 String,
121 (
122 Instant,
123 u64,
124 actions::transact_write_items::TransactWriteItemsResponse,
125 ),
126>;
127
128type ExecuteTransactionTokenCache = HashMap<
135 String,
136 (
137 Instant,
138 u64,
139 actions::execute_transaction::ExecuteTransactionResponse,
140 ),
141>;
142
143#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
146const MAX_TOKEN_LEN: usize = 36;
147
148#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
151const TOKEN_EXPIRY_SECS: u64 = 600;
152
153#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
170fn run_idempotent<T, H, E, R>(
171 cache: &Mutex<HashMap<String, (Instant, u64, T)>>,
172 token: Option<&str>,
173 hash_input: &H,
174 execute: E,
175 replay: R,
176) -> Result<T>
177where
178 T: Clone,
179 H: serde::Serialize,
180 E: FnOnce() -> Result<T>,
181 R: FnOnce(&T) -> T,
182{
183 if let Some(token) = token {
184 if token.len() > MAX_TOKEN_LEN {
185 return Err(DynoxideError::ValidationException(format!(
186 "1 validation error detected: Value '{token}' at 'clientRequestToken' failed to satisfy constraint: Member must have length less than or equal to {MAX_TOKEN_LEN}"
187 )));
188 }
189 }
190
191 let Some(token) = token else {
193 return execute();
194 };
195
196 let request_hash = {
199 use std::hash::{Hash, Hasher};
200 let normalised = serde_json::to_value(hash_input)
201 .and_then(|v| serde_json::to_vec(&v))
202 .unwrap_or_default();
203 let mut hasher = std::collections::hash_map::DefaultHasher::new();
204 normalised.hash(&mut hasher);
205 hasher.finish()
206 };
207
208 let mut cache = cache
209 .lock()
210 .map_err(|e| DynoxideError::InternalServerError(format!("Lock poisoned: {e}")))?;
211 cache.retain(|_, (ts, _, _)| ts.elapsed().as_secs() < TOKEN_EXPIRY_SECS);
213 if let Some((_, cached_hash, resp)) = cache.get(token) {
214 if *cached_hash != request_hash {
215 return Err(DynoxideError::IdempotentParameterMismatchException(
216 "An error occurred (IdempotentParameterMismatchException)".to_string(),
217 ));
218 }
219 let cached = resp.clone();
221 drop(cache);
222 return Ok(replay(&cached));
223 }
224 let resp = execute()?;
228 cache.insert(
229 token.to_string(),
230 (Instant::now(), request_hash, resp.clone()),
231 );
232 Ok(resp)
233}
234
235#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
240pub type RusqliteBackend = storage::Storage;
241
242#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
250pub type NativeDatabase = Database<RusqliteBackend>;
251
252#[cfg(feature = "wasm-sqlite")]
259pub type WasmDatabase = Database<WasmBridgeBackend>;
260
261#[cfg(feature = "wasm-sqlite")]
269pub const WASM_PREVIEW: bool = true;
270#[cfg(not(feature = "wasm-sqlite"))]
273pub const WASM_PREVIEW: bool = false;
274
275#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
284pub struct Database<S = RusqliteBackend> {
285 inner: Arc<Mutex<S>>,
286 idempotency_tokens: Arc<Mutex<TransactWriteTokenCache>>,
287 execute_transaction_tokens: Arc<Mutex<ExecuteTransactionTokenCache>>,
288}
289
290#[cfg(all(
296 not(any(feature = "native-sqlite", feature = "_has-encryption")),
297 feature = "wasm-sqlite"
298))]
299use async_lock::Mutex as BackendMutex;
300#[cfg(all(
301 not(any(feature = "native-sqlite", feature = "_has-encryption")),
302 not(feature = "wasm-sqlite")
303))]
304use std::sync::Mutex as BackendMutex;
305
306#[cfg(not(any(feature = "native-sqlite", feature = "_has-encryption")))]
316pub struct Database<S> {
317 inner: Arc<BackendMutex<S>>,
318 idempotency_tokens: Arc<Mutex<TransactWriteTokenCache>>,
319 execute_transaction_tokens: Arc<Mutex<ExecuteTransactionTokenCache>>,
320}
321
322impl<S> Clone for Database<S> {
324 fn clone(&self) -> Self {
325 Self {
326 inner: Arc::clone(&self.inner),
327 idempotency_tokens: Arc::clone(&self.idempotency_tokens),
328 execute_transaction_tokens: Arc::clone(&self.execute_transaction_tokens),
329 }
330 }
331}
332
333#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
334impl Database<RusqliteBackend> {
335 pub fn new(path: &str) -> Result<Self> {
337 let storage = storage::Storage::new(path)?;
338 Ok(Self {
339 inner: Arc::new(Mutex::new(storage)),
340 idempotency_tokens: Arc::new(Mutex::new(HashMap::new())),
341 execute_transaction_tokens: Arc::new(Mutex::new(HashMap::new())),
342 })
343 }
344
345 #[cfg(feature = "_has-encryption")]
366 pub fn new_encrypted(path: &str, key: &str) -> Result<Self> {
367 if key.len() != 64 || !key.bytes().all(|b| b.is_ascii_hexdigit()) {
368 return Err(DynoxideError::ValidationException(
369 "Encryption key must be a 64-character hex string (32 bytes)".to_string(),
370 ));
371 }
372
373 let storage = storage::Storage::new_encrypted(path, key)?;
374 Ok(Self {
375 inner: Arc::new(Mutex::new(storage)),
376 idempotency_tokens: Arc::new(Mutex::new(HashMap::new())),
377 execute_transaction_tokens: Arc::new(Mutex::new(HashMap::new())),
378 })
379 }
380
381 pub fn memory() -> Result<Self> {
383 let storage = storage::Storage::memory()?;
384 Ok(Self {
385 inner: Arc::new(Mutex::new(storage)),
386 idempotency_tokens: Arc::new(Mutex::new(HashMap::new())),
387 execute_transaction_tokens: Arc::new(Mutex::new(HashMap::new())),
388 })
389 }
390
391 pub(crate) fn with_storage<F, T>(&self, f: F) -> Result<T>
393 where
394 F: FnOnce(&storage::Storage) -> Result<T>,
395 {
396 let guard = self
397 .inner
398 .lock()
399 .map_err(|e| DynoxideError::InternalServerError(format!("Lock poisoned: {e}")))?;
400 f(&guard)
401 }
402
403 pub(crate) fn with_storage_mut<F, T>(&self, f: F) -> Result<T>
405 where
406 F: FnOnce(&mut storage::Storage) -> Result<T>,
407 {
408 let mut guard = self
409 .inner
410 .lock()
411 .map_err(|e| DynoxideError::InternalServerError(format!("Lock poisoned: {e}")))?;
412 f(&mut guard)
413 }
414
415 pub fn create_table(
421 &self,
422 request: actions::create_table::CreateTableRequest,
423 ) -> Result<actions::create_table::CreateTableResponse> {
424 self.with_storage(|s| pollster::block_on(actions::create_table::execute(s, request)))
425 }
426
427 pub fn delete_table(
429 &self,
430 request: actions::delete_table::DeleteTableRequest,
431 ) -> Result<actions::delete_table::DeleteTableResponse> {
432 self.with_storage(|s| pollster::block_on(actions::delete_table::execute(s, request)))
433 }
434
435 pub fn describe_table(
437 &self,
438 request: actions::describe_table::DescribeTableRequest,
439 ) -> Result<actions::describe_table::DescribeTableResponse> {
440 self.with_storage(|s| pollster::block_on(actions::describe_table::execute(s, request)))
441 }
442
443 pub fn update_table(
445 &self,
446 request: actions::update_table::UpdateTableRequest,
447 ) -> Result<actions::update_table::UpdateTableResponse> {
448 self.with_storage(|s| pollster::block_on(actions::update_table::execute(s, request)))
449 }
450
451 pub fn list_tables(
453 &self,
454 request: actions::list_tables::ListTablesRequest,
455 ) -> Result<actions::list_tables::ListTablesResponse> {
456 self.with_storage(|s| pollster::block_on(actions::list_tables::execute(s, request)))
457 }
458
459 pub fn tag_resource(
465 &self,
466 request: actions::tag_resource::TagResourceRequest,
467 ) -> Result<actions::tag_resource::TagResourceResponse> {
468 self.with_storage(|s| pollster::block_on(actions::tag_resource::execute(s, request)))
469 }
470
471 pub fn untag_resource(
473 &self,
474 request: actions::untag_resource::UntagResourceRequest,
475 ) -> Result<actions::untag_resource::UntagResourceResponse> {
476 self.with_storage(|s| pollster::block_on(actions::untag_resource::execute(s, request)))
477 }
478
479 pub fn list_tags_of_resource(
481 &self,
482 request: actions::list_tags_of_resource::ListTagsOfResourceRequest,
483 ) -> Result<actions::list_tags_of_resource::ListTagsOfResourceResponse> {
484 self.with_storage(|s| {
485 pollster::block_on(actions::list_tags_of_resource::execute(s, request))
486 })
487 }
488
489 pub fn put_item(
495 &self,
496 request: actions::put_item::PutItemRequest,
497 ) -> Result<actions::put_item::PutItemResponse> {
498 self.with_storage(|s| pollster::block_on(actions::put_item::execute(s, request)))
499 }
500
501 pub fn get_item(
503 &self,
504 request: actions::get_item::GetItemRequest,
505 ) -> Result<actions::get_item::GetItemResponse> {
506 self.with_storage(|s| pollster::block_on(actions::get_item::execute(s, request)))
507 }
508
509 pub fn delete_item(
511 &self,
512 request: actions::delete_item::DeleteItemRequest,
513 ) -> Result<actions::delete_item::DeleteItemResponse> {
514 self.with_storage(|s| pollster::block_on(actions::delete_item::execute(s, request)))
515 }
516
517 pub fn update_item(
519 &self,
520 request: actions::update_item::UpdateItemRequest,
521 ) -> Result<actions::update_item::UpdateItemResponse> {
522 self.with_storage(|s| pollster::block_on(actions::update_item::execute(s, request)))
523 }
524
525 pub fn batch_get_item(
531 &self,
532 request: actions::batch_get_item::BatchGetItemRequest,
533 ) -> Result<actions::batch_get_item::BatchGetItemResponse> {
534 self.with_storage(|s| pollster::block_on(actions::batch_get_item::execute(s, request)))
535 }
536
537 pub fn batch_write_item(
539 &self,
540 request: actions::batch_write_item::BatchWriteItemRequest,
541 ) -> Result<actions::batch_write_item::BatchWriteItemResponse> {
542 self.with_storage(|s| pollster::block_on(actions::batch_write_item::execute(s, request)))
543 }
544
545 pub fn import_items(
560 &self,
561 table_name: &str,
562 items: Vec<Item>,
563 options: ImportOptions,
564 ) -> Result<ImportResult> {
565 self.with_storage(|s| {
566 pollster::block_on(actions::import_items::execute(
567 s, table_name, items, &options,
568 ))
569 })
570 }
571
572 #[cfg(feature = "import")]
578 pub(crate) fn import_items_fresh(
579 &self,
580 table_name: &str,
581 items: Vec<Item>,
582 options: ImportOptions,
583 ) -> Result<ImportResult> {
584 self.with_storage(|s| {
585 pollster::block_on(actions::import_items::execute_skip_gsi_deletes(
586 s, table_name, items, &options,
587 ))
588 })
589 }
590
591 pub fn enable_bulk_loading(&self) -> Result<()> {
600 self.with_storage(|s| s.enable_bulk_loading())
601 }
602
603 pub fn disable_bulk_loading(&self) -> Result<()> {
605 self.with_storage(|s| s.disable_bulk_loading())
606 }
607
608 pub fn query(
614 &self,
615 request: actions::query::QueryRequest,
616 ) -> Result<actions::query::QueryResponse> {
617 self.with_storage(|s| pollster::block_on(actions::query::execute(s, request)))
618 }
619
620 pub fn scan(&self, request: actions::scan::ScanRequest) -> Result<actions::scan::ScanResponse> {
622 self.with_storage(|s| pollster::block_on(actions::scan::execute(s, request)))
623 }
624
625 pub fn transact_write_items(
636 &self,
637 request: actions::transact_write_items::TransactWriteItemsRequest,
638 ) -> Result<actions::transact_write_items::TransactWriteItemsResponse> {
639 run_idempotent(
640 &self.idempotency_tokens,
641 request.client_request_token.as_deref(),
642 &request.transact_items,
643 || {
644 self.with_storage(|s| {
645 pollster::block_on(actions::transact_write_items::execute(s, request.clone()))
646 })
647 },
648 |cached| {
649 actions::transact_write_items::replay_response(
654 &request.transact_items,
655 &request.return_consumed_capacity,
656 cached.item_collection_metrics.clone(),
657 )
658 },
659 )
660 }
661
662 pub fn transact_get_items(
664 &self,
665 request: actions::transact_get_items::TransactGetItemsRequest,
666 ) -> Result<actions::transact_get_items::TransactGetItemsResponse> {
667 self.with_storage(|s| pollster::block_on(actions::transact_get_items::execute(s, request)))
668 }
669
670 pub fn list_streams(
676 &self,
677 request: actions::list_streams::ListStreamsRequest,
678 ) -> Result<actions::list_streams::ListStreamsResponse> {
679 self.with_storage(|s| pollster::block_on(actions::list_streams::execute(s, request)))
680 }
681
682 pub fn describe_stream(
684 &self,
685 request: actions::describe_stream::DescribeStreamRequest,
686 ) -> Result<actions::describe_stream::DescribeStreamResponse> {
687 self.with_storage(|s| pollster::block_on(actions::describe_stream::execute(s, request)))
688 }
689
690 pub fn get_shard_iterator(
692 &self,
693 request: actions::get_shard_iterator::GetShardIteratorRequest,
694 ) -> Result<actions::get_shard_iterator::GetShardIteratorResponse> {
695 self.with_storage(|s| pollster::block_on(actions::get_shard_iterator::execute(s, request)))
696 }
697
698 pub fn get_records(
700 &self,
701 request: actions::get_records::GetRecordsRequest,
702 ) -> Result<actions::get_records::GetRecordsResponse> {
703 self.with_storage(|s| pollster::block_on(actions::get_records::execute(s, request)))
704 }
705
706 pub fn update_time_to_live(
712 &self,
713 request: actions::update_time_to_live::UpdateTimeToLiveRequest,
714 ) -> Result<actions::update_time_to_live::UpdateTimeToLiveResponse> {
715 self.with_storage(|s| pollster::block_on(actions::update_time_to_live::execute(s, request)))
716 }
717
718 pub fn describe_time_to_live(
720 &self,
721 request: actions::describe_time_to_live::DescribeTimeToLiveRequest,
722 ) -> Result<actions::describe_time_to_live::DescribeTimeToLiveResponse> {
723 self.with_storage(|s| {
724 pollster::block_on(actions::describe_time_to_live::execute(s, request))
725 })
726 }
727
728 pub fn sweep_ttl(&self) -> Result<usize> {
731 self.with_storage(|s| pollster::block_on(ttl::sweep_expired_items(s)))
732 }
733
734 pub fn execute_statement(
740 &self,
741 request: actions::execute_statement::ExecuteStatementRequest,
742 ) -> Result<actions::execute_statement::ExecuteStatementResponse> {
743 self.with_storage(|s| pollster::block_on(actions::execute_statement::execute(s, request)))
744 }
745
746 pub fn execute_transaction(
754 &self,
755 request: actions::execute_transaction::ExecuteTransactionRequest,
756 ) -> Result<actions::execute_transaction::ExecuteTransactionResponse> {
757 run_idempotent(
758 &self.execute_transaction_tokens,
759 request.client_request_token.as_deref(),
760 &request.transact_statements,
761 || {
762 self.with_storage(|s| {
763 pollster::block_on(actions::execute_transaction::execute(s, request.clone()))
764 })
765 },
766 |cached| {
767 actions::execute_transaction::replay_response(
770 &request.transact_statements,
771 &request.return_consumed_capacity,
772 cached.responses.clone(),
773 )
774 },
775 )
776 }
777
778 pub fn batch_execute_statement(
780 &self,
781 request: actions::batch_execute_statement::BatchExecuteStatementRequest,
782 ) -> Result<actions::batch_execute_statement::BatchExecuteStatementResponse> {
783 self.with_storage(|s| {
784 pollster::block_on(actions::batch_execute_statement::execute(s, request))
785 })
786 }
787
788 pub fn touch_cached_at(
797 &self,
798 table_name: &str,
799 pk: &str,
800 sk: &str,
801 timestamp: f64,
802 ) -> Result<()> {
803 self.with_storage(|s| s.touch_cached_at(table_name, pk, sk, timestamp))
804 }
805
806 pub fn get_lru_items(
811 &self,
812 table_name: &str,
813 limit: usize,
814 ) -> Result<Vec<(String, String, i64)>> {
815 self.with_storage(|s| s.get_lru_items(table_name, limit))
816 }
817
818 pub fn db_path(&self) -> Result<Option<String>> {
824 self.with_storage(|s| Ok(s.db_path()))
825 }
826
827 pub fn db_size_bytes(&self) -> Result<u64> {
829 self.with_storage(|s| s.db_size_bytes())
830 }
831
832 pub fn table_count(&self) -> Result<usize> {
834 self.with_storage(|s| s.table_count())
835 }
836
837 pub fn table_stats(&self) -> Result<Vec<TableStats>> {
839 self.with_storage(|s| s.table_stats())
840 }
841
842 pub fn get_table_metadata(&self, table_name: &str) -> Result<Option<storage::TableMetadata>> {
844 self.with_storage(|s| s.get_table_metadata(table_name))
845 }
846
847 pub fn database_info(&self) -> Result<DatabaseInfo> {
852 self.with_storage(|s| s.database_info())
853 }
854
855 pub fn vacuum(&self) -> Result<()> {
861 self.with_storage(|s| s.vacuum())
862 }
863
864 pub fn vacuum_into(&self, path: &str) -> Result<()> {
869 self.with_storage(|s| s.vacuum_into(path))
870 }
871
872 pub fn restore_from(&self, path: &str) -> Result<()> {
878 self.with_storage_mut(|s| s.restore_from(path))
879 }
880
881 #[cfg(feature = "mcp-server")]
886 pub(crate) fn backup_to_memory(&self) -> Result<rusqlite::Connection> {
887 self.with_storage(|s| s.backup_to_memory())
888 }
889
890 #[cfg(feature = "mcp-server")]
894 pub(crate) fn restore_from_connection(&self, source: &rusqlite::Connection) -> Result<()> {
895 self.with_storage_mut(|s| s.restore_from_connection(source))
896 }
897}
898
899#[cfg(feature = "wasm-sqlite")]
913impl Database<WasmBridgeBackend> {
914 pub async fn open(name: &str) -> Result<Self> {
917 Self::open_with(name, false).await
918 }
919
920 pub async fn open_with(name: &str, ephemeral: bool) -> Result<Self> {
923 let backend = WasmBridgeBackend::open_with(name, ephemeral)
924 .await
925 .map_err(DynoxideError::from)?;
926 Ok(Self {
927 inner: Arc::new(BackendMutex::new(backend)),
928 idempotency_tokens: Arc::new(Mutex::new(HashMap::new())),
929 execute_transaction_tokens: Arc::new(Mutex::new(HashMap::new())),
930 })
931 }
932
933 pub async fn persistence_mode(&self) -> String {
935 self.backend().await.persistence_mode().to_string()
936 }
937
938 pub async fn close(&self) -> Result<()> {
942 self.backend()
943 .await
944 .close()
945 .await
946 .map_err(DynoxideError::from)
947 }
948
949 pub(crate) async fn backend(&self) -> async_lock::MutexGuard<'_, WasmBridgeBackend> {
958 self.inner.lock().await
959 }
960
961 pub async fn create_table(
963 &self,
964 request: actions::create_table::CreateTableRequest,
965 ) -> Result<actions::create_table::CreateTableResponse> {
966 let backend = self.backend().await;
967 actions::create_table::execute(&*backend, request).await
968 }
969
970 pub async fn delete_table(
972 &self,
973 request: actions::delete_table::DeleteTableRequest,
974 ) -> Result<actions::delete_table::DeleteTableResponse> {
975 let backend = self.backend().await;
976 actions::delete_table::execute(&*backend, request).await
977 }
978
979 pub async fn describe_table(
981 &self,
982 request: actions::describe_table::DescribeTableRequest,
983 ) -> Result<actions::describe_table::DescribeTableResponse> {
984 let backend = self.backend().await;
985 actions::describe_table::execute(&*backend, request).await
986 }
987
988 pub async fn list_tables(
990 &self,
991 request: actions::list_tables::ListTablesRequest,
992 ) -> Result<actions::list_tables::ListTablesResponse> {
993 let backend = self.backend().await;
994 actions::list_tables::execute(&*backend, request).await
995 }
996
997 pub async fn put_item(
999 &self,
1000 request: actions::put_item::PutItemRequest,
1001 ) -> Result<actions::put_item::PutItemResponse> {
1002 let backend = self.backend().await;
1003 actions::put_item::execute(&*backend, request).await
1004 }
1005
1006 pub async fn get_item(
1008 &self,
1009 request: actions::get_item::GetItemRequest,
1010 ) -> Result<actions::get_item::GetItemResponse> {
1011 let backend = self.backend().await;
1012 actions::get_item::execute(&*backend, request).await
1013 }
1014
1015 pub async fn delete_item(
1017 &self,
1018 request: actions::delete_item::DeleteItemRequest,
1019 ) -> Result<actions::delete_item::DeleteItemResponse> {
1020 let backend = self.backend().await;
1021 actions::delete_item::execute(&*backend, request).await
1022 }
1023
1024 pub async fn query(
1026 &self,
1027 request: actions::query::QueryRequest,
1028 ) -> Result<actions::query::QueryResponse> {
1029 let backend = self.backend().await;
1030 actions::query::execute(&*backend, request).await
1031 }
1032
1033 pub async fn scan(
1035 &self,
1036 request: actions::scan::ScanRequest,
1037 ) -> Result<actions::scan::ScanResponse> {
1038 let backend = self.backend().await;
1039 actions::scan::execute(&*backend, request).await
1040 }
1041}
1042
1043#[cfg(all(test, any(feature = "native-sqlite", feature = "_has-encryption")))]
1044mod tests {
1045 use super::*;
1046
1047 #[test]
1048 fn test_database_memory() {
1049 let db = Database::memory().unwrap();
1050 let _db2 = db.clone();
1052 }
1053
1054 #[test]
1055 fn test_database_with_storage() {
1056 let db = Database::memory().unwrap();
1057 let tables = db.with_storage(|s| s.list_table_names()).unwrap();
1058 assert!(tables.is_empty());
1059 }
1060
1061 #[test]
1062 fn test_database_thread_safe() {
1063 let db = Database::memory().unwrap();
1064 let db2 = db.clone();
1065
1066 let handle =
1067 std::thread::spawn(move || db2.with_storage(|s| s.list_table_names()).unwrap());
1068
1069 let tables = handle.join().unwrap();
1070 assert!(tables.is_empty());
1071 }
1072
1073 #[test]
1074 fn test_native_database_alias_round_trips() {
1075 let db: NativeDatabase = Database::memory().unwrap();
1079
1080 db.create_table(actions::create_table::CreateTableRequest {
1081 table_name: "tbl".to_string(),
1082 key_schema: vec![types::KeySchemaElement {
1083 attribute_name: "pk".to_string(),
1084 key_type: types::KeyType::HASH,
1085 }],
1086 attribute_definitions: vec![types::AttributeDefinition {
1087 attribute_name: "pk".to_string(),
1088 attribute_type: types::ScalarAttributeType::S,
1089 }],
1090 ..Default::default()
1091 })
1092 .unwrap();
1093
1094 let mut item = HashMap::new();
1095 item.insert("pk".to_string(), AttributeValue::S("a".to_string()));
1096 db.put_item(actions::put_item::PutItemRequest {
1097 table_name: "tbl".to_string(),
1098 item,
1099 ..Default::default()
1100 })
1101 .unwrap();
1102
1103 let mut key = HashMap::new();
1104 key.insert("pk".to_string(), AttributeValue::S("a".to_string()));
1105 let got = db
1106 .get_item(actions::get_item::GetItemRequest {
1107 table_name: "tbl".to_string(),
1108 key,
1109 ..Default::default()
1110 })
1111 .unwrap();
1112 assert_eq!(
1113 got.item.unwrap().get("pk"),
1114 Some(&AttributeValue::S("a".to_string()))
1115 );
1116 }
1117}