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(all(
33 feature = "wasm-sqlite",
34 any(
35 feature = "native-sqlite",
36 feature = "_has-encryption",
37 feature = "cli"
38 )
39))]
40compile_error!(
41 "The `wasm-sqlite` backend is mutually exclusive with the native backends and \
42 with the CLI, server and MCP features, which are native-only.\n\
43 If your manifest names none of those, they came from the defaults: Cargo adds \
44 the default features to whatever you list, so `features = [\"wasm-sqlite\"]` \
45 enables the native backend as well. Turn them off:\n \
46 dynoxide-rs = { version = \"...\", default-features = false, features = [\"wasm-sqlite\"] }"
47);
48
49#[cfg(not(any(
50 feature = "native-sqlite",
51 feature = "_has-encryption",
52 feature = "wasm-sqlite"
53)))]
54compile_error!(
55 "A storage backend feature must be enabled: `native-sqlite`, `encryption`, \
56 `encryption-cc`, or `wasm-sqlite`. Default features include `native-sqlite`. \
57 If you used `default-features = false`, add one of these features."
58);
59
60pub mod actions;
61pub mod auth_material;
62pub mod bench_counters;
63pub mod errors;
64pub mod expressions;
65#[cfg(feature = "import")]
66pub mod import;
67#[doc(hidden)]
68pub mod macros;
69#[cfg(feature = "mcp-server")]
70pub mod mcp;
71#[cfg(any(feature = "http-server", feature = "mcp-server"))]
72pub(crate) mod net;
73pub mod partiql;
74pub mod schema;
75pub(crate) mod serde_errors;
79#[cfg(any(feature = "http-server", feature = "wasm-sqlite", test))]
83pub(crate) mod serialization_checks;
84#[cfg(feature = "http-server")]
85pub mod server;
86#[cfg(feature = "mcp-server")]
87pub(crate) mod snapshots;
88pub mod storage;
89pub mod storage_backend;
90pub mod streams;
91pub mod ttl;
92pub mod types;
93pub mod validation;
94#[cfg(any(feature = "http-server", feature = "wasm-sqlite", test))]
98pub(crate) mod dynamo_ops;
99#[cfg(any(feature = "wasm-sqlite", test))]
104pub mod wasm_api;
105#[cfg(feature = "wasm-harness")]
106pub mod wasm_harness;
107
108#[doc(hidden)]
109pub use macros::ItemInsert;
110
111use std::collections::HashMap;
112use std::sync::{Arc, Mutex};
113use web_time::{Duration, Instant};
114
115pub use errors::{DynoxideError, Result};
116pub use storage::{DatabaseInfo, TableInfoEntry, TableMetadata, TableStats};
117pub use storage_backend::BackendError;
118#[cfg(feature = "wasm-sqlite")]
119pub use storage_backend::WasmBridgeBackend;
120
121pub use types::{AttributeValue, ConversionError, Item};
122
123pub const PRODUCT_VERSION: &str = env!("DYNOXIDE_PRODUCT_VERSION");
131
132use actions::vector_lifecycle::VectorIndexLifecycle;
133
134#[derive(Debug, Clone, Default)]
136pub struct ImportOptions {
137 pub record_streams: bool,
139 pub set_cached_at: bool,
141}
142
143#[derive(Debug, Clone)]
145pub struct ImportResult {
146 pub items_imported: usize,
148 pub bytes_imported: usize,
150}
151
152type TokenSlot<T> = (Instant, u64, Option<T>);
160
161type TokenCache<T> = HashMap<String, TokenSlot<T>>;
163
164type TransactWriteTokenCache = TokenCache<actions::transact_write_items::CachedWrite>;
168
169type ExecuteTransactionTokenCache = TokenCache<actions::execute_transaction::CachedTransaction>;
175
176#[derive(Default)]
181pub struct TokenCaches {
182 #[cfg_attr(
186 not(any(feature = "native-sqlite", feature = "_has-encryption")),
187 allow(dead_code)
188 )]
189 transact_write: Mutex<TransactWriteTokenCache>,
190 execute_transaction: Mutex<ExecuteTransactionTokenCache>,
191}
192
193impl TokenCaches {
194 pub fn new() -> Self {
196 Self::default()
197 }
198
199 #[cfg(any(feature = "wasm-sqlite", test))]
200 pub(crate) fn execute_transaction(&self) -> &Mutex<ExecuteTransactionTokenCache> {
201 &self.execute_transaction
202 }
203}
204
205const MAX_TOKEN_LEN: usize = 36;
207
208const TOKEN_EXPIRY_SECS: u64 = 600;
211
212fn validate_token(token: Option<&str>) -> Result<()> {
214 match token {
215 Some(token) if token.len() > MAX_TOKEN_LEN => {
216 Err(DynoxideError::ValidationException(format!(
217 "1 validation error detected: Value '{token}' at 'clientRequestToken' failed to satisfy constraint: Member must have length less than or equal to {MAX_TOKEN_LEN}"
218 )))
219 }
220 _ => Ok(()),
221 }
222}
223
224fn request_hash<H: serde::Serialize>(input: &H) -> u64 {
231 use std::hash::{Hash, Hasher};
232 let normalised = serde_json::to_value(input)
233 .and_then(|v| serde_json::to_vec(&v))
234 .unwrap_or_default();
235 let mut hasher = std::collections::hash_map::DefaultHasher::new();
236 normalised.hash(&mut hasher);
237 hasher.finish()
238}
239
240fn lock_cache<T>(cache: &Mutex<TokenCache<T>>) -> Result<std::sync::MutexGuard<'_, TokenCache<T>>> {
241 cache
242 .lock()
243 .map_err(|e| DynoxideError::InternalServerError(format!("Lock poisoned: {e}")))
244}
245
246fn evict_expired<T>(cache: &mut TokenCache<T>, window: Duration) {
252 cache.retain(|_, (claimed_at, _, _)| claimed_at.elapsed() < window);
253}
254
255fn token_window() -> Duration {
257 Duration::from_secs(TOKEN_EXPIRY_SECS)
258}
259
260#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
278fn run_idempotent<T, H, E, R>(
279 cache: &Mutex<TokenCache<T>>,
280 token: Option<&str>,
281 hash_input: &H,
282 execute: E,
283 replay: R,
284) -> Result<T>
285where
286 T: Clone,
287 H: serde::Serialize,
288 E: FnOnce() -> Result<T>,
289 R: FnOnce(&T) -> T,
290{
291 validate_token(token)?;
292
293 let Some(token) = token else {
295 return execute();
296 };
297 let hash = request_hash(hash_input);
298
299 let mut cache = lock_cache(cache)?;
300 evict_expired(&mut cache, token_window());
301 let cached = match cache.get(token) {
302 Some((_, cached_hash, _)) if *cached_hash != hash => {
303 return Err(DynoxideError::IdempotentParameterMismatchException(
304 "An error occurred (IdempotentParameterMismatchException)".to_string(),
305 ));
306 }
307 Some((_, _, slot)) => slot.clone(),
311 None => None,
312 };
313 if let Some(cached) = cached {
314 drop(cache);
316 return Ok(replay(&cached));
317 }
318 let resp = execute()?;
322 cache.insert(
323 token.to_string(),
324 (Instant::now(), hash, Some(resp.clone())),
325 );
326 Ok(resp)
327}
328
329#[cfg(any(feature = "wasm-sqlite", test))]
332const IN_FLIGHT_MESSAGE: &str = "a call under this ClientRequestToken is still in flight";
333
334#[cfg(any(feature = "wasm-sqlite", test))]
336enum TokenClaim<T> {
337 Marked(Instant),
341 InFlight,
343 Mismatch,
345 Hit(T),
347}
348
349#[cfg(any(feature = "wasm-sqlite", test))]
355fn lookup_or_claim<T: Clone>(
356 cache: &Mutex<TokenCache<T>>,
357 token: &str,
358 hash: u64,
359) -> Result<TokenClaim<T>> {
360 lookup_or_claim_within(cache, token, hash, token_window())
361}
362
363#[cfg(any(feature = "wasm-sqlite", test))]
366fn lookup_or_claim_within<T: Clone>(
367 cache: &Mutex<TokenCache<T>>,
368 token: &str,
369 hash: u64,
370 window: Duration,
371) -> Result<TokenClaim<T>> {
372 let mut cache = lock_cache(cache)?;
373 evict_expired(&mut cache, window);
374 Ok(match cache.get(token) {
375 Some((_, cached_hash, _)) if *cached_hash != hash => TokenClaim::Mismatch,
376 Some((_, _, None)) => TokenClaim::InFlight,
377 Some((_, _, Some(resp))) => TokenClaim::Hit(resp.clone()),
378 None => {
379 let claimed_at = Instant::now();
380 cache.insert(token.to_string(), (claimed_at, hash, None));
381 TokenClaim::Marked(claimed_at)
382 }
383 })
384}
385
386#[cfg(any(feature = "wasm-sqlite", test))]
393fn still_ours<T>(cache: &TokenCache<T>, token: &str, claimed_at: Instant) -> bool {
394 matches!(cache.get(token), Some((at, _, None)) if *at == claimed_at)
395}
396
397#[cfg(any(feature = "wasm-sqlite", test))]
399fn record_complete<T>(
400 cache: &Mutex<TokenCache<T>>,
401 token: &str,
402 claimed_at: Instant,
403 hash: u64,
404 resp: T,
405) -> Result<()> {
406 let mut cache = lock_cache(cache)?;
407 if still_ours(&cache, token, claimed_at) {
408 cache.insert(token.to_string(), (claimed_at, hash, Some(resp)));
409 }
410 Ok(())
411}
412
413#[cfg(any(feature = "wasm-sqlite", test))]
416fn clear_claim<T>(cache: &Mutex<TokenCache<T>>, token: &str, claimed_at: Instant) -> Result<()> {
417 let mut cache = lock_cache(cache)?;
418 if still_ours(&cache, token, claimed_at) {
419 cache.remove(token);
420 }
421 Ok(())
422}
423
424#[cfg(any(feature = "wasm-sqlite", test))]
437async fn run_idempotent_async<T, H, F, R>(
438 cache: &Mutex<TokenCache<T>>,
439 token: Option<&str>,
440 hash_input: &H,
441 execute: F,
442 replay: R,
443) -> Result<T>
444where
445 T: Clone,
446 H: serde::Serialize,
447 F: std::future::Future<Output = Result<T>>,
448 R: FnOnce(&T) -> T,
449{
450 validate_token(token)?;
451
452 let Some(token) = token else {
453 return execute.await;
454 };
455 let hash = request_hash(hash_input);
456
457 let claimed_at = match lookup_or_claim(cache, token, hash)? {
458 TokenClaim::Hit(cached) => return Ok(replay(&cached)),
459 TokenClaim::Mismatch => {
460 return Err(DynoxideError::IdempotentParameterMismatchException(
461 "An error occurred (IdempotentParameterMismatchException)".to_string(),
462 ));
463 }
464 TokenClaim::InFlight => {
465 return Err(DynoxideError::InternalServerError(
466 IN_FLIGHT_MESSAGE.to_string(),
467 ));
468 }
469 TokenClaim::Marked(claimed_at) => claimed_at,
470 };
471
472 match execute.await {
473 Ok(resp) => {
474 let _ = record_complete(cache, token, claimed_at, hash, resp.clone());
478 Ok(resp)
479 }
480 Err(e) => {
481 let _ = clear_claim(cache, token, claimed_at);
482 Err(e)
483 }
484 }
485}
486
487#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
492pub type RusqliteBackend = storage::Storage;
493
494#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
502pub type NativeDatabase = Database<RusqliteBackend>;
503
504#[cfg(feature = "wasm-sqlite")]
511pub type WasmDatabase = Database<WasmBridgeBackend>;
512
513#[cfg(feature = "wasm-sqlite")]
525pub const WASM_BACKEND: bool = true;
526#[cfg(not(feature = "wasm-sqlite"))]
529pub const WASM_BACKEND: bool = false;
530
531#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
540pub struct Database<S = RusqliteBackend> {
541 inner: Arc<Mutex<S>>,
542 tokens: Arc<TokenCaches>,
543 vector_lifecycle: Arc<VectorIndexLifecycle>,
544}
545
546#[cfg(all(
552 not(any(feature = "native-sqlite", feature = "_has-encryption")),
553 feature = "wasm-sqlite"
554))]
555use async_lock::Mutex as BackendMutex;
556#[cfg(all(
557 not(any(feature = "native-sqlite", feature = "_has-encryption")),
558 not(feature = "wasm-sqlite")
559))]
560use std::sync::Mutex as BackendMutex;
561
562#[cfg(not(any(feature = "native-sqlite", feature = "_has-encryption")))]
572pub struct Database<S> {
573 inner: Arc<BackendMutex<S>>,
574 tokens: Arc<TokenCaches>,
575 vector_lifecycle: Arc<VectorIndexLifecycle>,
576}
577
578impl<S> Clone for Database<S> {
580 fn clone(&self) -> Self {
581 Self {
582 inner: Arc::clone(&self.inner),
583 tokens: Arc::clone(&self.tokens),
584 vector_lifecycle: Arc::clone(&self.vector_lifecycle),
585 }
586 }
587}
588
589#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
590impl Database<RusqliteBackend> {
591 pub fn new(path: &str) -> Result<Self> {
593 let storage = storage::Storage::new(path)?;
594 Ok(Self {
595 inner: Arc::new(Mutex::new(storage)),
596 tokens: Arc::new(TokenCaches::new()),
597 vector_lifecycle: Arc::new(VectorIndexLifecycle::new()),
598 })
599 }
600
601 #[cfg(feature = "_has-encryption")]
622 pub fn new_encrypted(path: &str, key: &str) -> Result<Self> {
623 if key.len() != 64 || !key.bytes().all(|b| b.is_ascii_hexdigit()) {
624 return Err(DynoxideError::ValidationException(
625 "Encryption key must be a 64-character hex string (32 bytes)".to_string(),
626 ));
627 }
628
629 let storage = storage::Storage::new_encrypted(path, key)?;
630 Ok(Self {
631 inner: Arc::new(Mutex::new(storage)),
632 tokens: Arc::new(TokenCaches::new()),
633 vector_lifecycle: Arc::new(VectorIndexLifecycle::new()),
634 })
635 }
636
637 pub fn memory() -> Result<Self> {
639 let storage = storage::Storage::memory()?;
640 Ok(Self {
641 inner: Arc::new(Mutex::new(storage)),
642 tokens: Arc::new(TokenCaches::new()),
643 vector_lifecycle: Arc::new(VectorIndexLifecycle::new()),
644 })
645 }
646
647 pub fn memory_with_clock(clock: Arc<dyn storage_backend::Clock>) -> Result<Self> {
654 let storage = storage::Storage::memory()?.with_clock(clock);
655 Ok(Self {
656 inner: Arc::new(Mutex::new(storage)),
657 tokens: Arc::new(TokenCaches::new()),
658 vector_lifecycle: Arc::new(VectorIndexLifecycle::new()),
659 })
660 }
661
662 pub(crate) fn with_storage<F, T>(&self, f: F) -> Result<T>
664 where
665 F: FnOnce(&storage::Storage) -> Result<T>,
666 {
667 let guard = self
668 .inner
669 .lock()
670 .map_err(|e| DynoxideError::InternalServerError(format!("Lock poisoned: {e}")))?;
671 f(&guard)
672 }
673
674 pub(crate) fn with_storage_mut<F, T>(&self, f: F) -> Result<T>
676 where
677 F: FnOnce(&mut storage::Storage) -> Result<T>,
678 {
679 let mut guard = self
680 .inner
681 .lock()
682 .map_err(|e| DynoxideError::InternalServerError(format!("Lock poisoned: {e}")))?;
683 f(&mut guard)
684 }
685
686 pub fn create_table(
692 &self,
693 request: actions::create_table::CreateTableRequest,
694 ) -> Result<actions::create_table::CreateTableResponse> {
695 self.with_storage(|s| pollster::block_on(actions::create_table::execute(s, request)))
696 }
697
698 pub fn delete_table(
700 &self,
701 request: actions::delete_table::DeleteTableRequest,
702 ) -> Result<actions::delete_table::DeleteTableResponse> {
703 self.with_storage(|s| {
704 pollster::block_on(actions::delete_table::execute(
705 s,
706 request,
707 &self.vector_lifecycle,
708 ))
709 })
710 }
711
712 pub fn describe_table(
714 &self,
715 request: actions::describe_table::DescribeTableRequest,
716 ) -> Result<actions::describe_table::DescribeTableResponse> {
717 self.with_storage(|s| {
718 pollster::block_on(actions::describe_table::execute(
719 s,
720 request,
721 &self.vector_lifecycle,
722 ))
723 })
724 }
725
726 pub fn update_table(
728 &self,
729 request: actions::update_table::UpdateTableRequest,
730 ) -> Result<actions::update_table::UpdateTableResponse> {
731 self.with_storage(|s| {
732 pollster::block_on(actions::update_table::execute(
733 s,
734 request,
735 &self.vector_lifecycle,
736 ))
737 })
738 }
739
740 pub fn list_tables(
742 &self,
743 request: actions::list_tables::ListTablesRequest,
744 ) -> Result<actions::list_tables::ListTablesResponse> {
745 self.with_storage(|s| pollster::block_on(actions::list_tables::execute(s, request)))
746 }
747
748 pub fn tag_resource(
754 &self,
755 request: actions::tag_resource::TagResourceRequest,
756 ) -> Result<actions::tag_resource::TagResourceResponse> {
757 self.with_storage(|s| pollster::block_on(actions::tag_resource::execute(s, request)))
758 }
759
760 pub fn untag_resource(
762 &self,
763 request: actions::untag_resource::UntagResourceRequest,
764 ) -> Result<actions::untag_resource::UntagResourceResponse> {
765 self.with_storage(|s| pollster::block_on(actions::untag_resource::execute(s, request)))
766 }
767
768 pub fn list_tags_of_resource(
770 &self,
771 request: actions::list_tags_of_resource::ListTagsOfResourceRequest,
772 ) -> Result<actions::list_tags_of_resource::ListTagsOfResourceResponse> {
773 self.with_storage(|s| {
774 pollster::block_on(actions::list_tags_of_resource::execute(s, request))
775 })
776 }
777
778 pub fn put_item(
784 &self,
785 request: actions::put_item::PutItemRequest,
786 ) -> Result<actions::put_item::PutItemResponse> {
787 self.with_storage(|s| pollster::block_on(actions::put_item::execute(s, request)))
788 }
789
790 pub fn get_item(
792 &self,
793 request: actions::get_item::GetItemRequest,
794 ) -> Result<actions::get_item::GetItemResponse> {
795 self.with_storage(|s| pollster::block_on(actions::get_item::execute(s, request)))
796 }
797
798 pub fn delete_item(
800 &self,
801 request: actions::delete_item::DeleteItemRequest,
802 ) -> Result<actions::delete_item::DeleteItemResponse> {
803 self.with_storage(|s| pollster::block_on(actions::delete_item::execute(s, request)))
804 }
805
806 pub fn update_item(
808 &self,
809 request: actions::update_item::UpdateItemRequest,
810 ) -> Result<actions::update_item::UpdateItemResponse> {
811 self.with_storage(|s| pollster::block_on(actions::update_item::execute(s, request)))
812 }
813
814 pub fn batch_get_item(
820 &self,
821 request: actions::batch_get_item::BatchGetItemRequest,
822 ) -> Result<actions::batch_get_item::BatchGetItemResponse> {
823 self.with_storage(|s| pollster::block_on(actions::batch_get_item::execute(s, request)))
824 }
825
826 pub fn batch_write_item(
828 &self,
829 request: actions::batch_write_item::BatchWriteItemRequest,
830 ) -> Result<actions::batch_write_item::BatchWriteItemResponse> {
831 self.with_storage(|s| pollster::block_on(actions::batch_write_item::execute(s, request)))
832 }
833
834 pub fn import_items(
849 &self,
850 table_name: &str,
851 items: Vec<Item>,
852 options: ImportOptions,
853 ) -> Result<ImportResult> {
854 self.with_storage(|s| {
855 pollster::block_on(actions::import_items::execute(
856 s, table_name, items, &options,
857 ))
858 })
859 }
860
861 #[cfg(feature = "import")]
867 pub(crate) fn import_items_fresh(
868 &self,
869 table_name: &str,
870 items: Vec<Item>,
871 options: ImportOptions,
872 ) -> Result<ImportResult> {
873 self.with_storage(|s| {
874 pollster::block_on(actions::import_items::execute_skip_gsi_deletes(
875 s, table_name, items, &options,
876 ))
877 })
878 }
879
880 pub fn enable_bulk_loading(&self) -> Result<()> {
889 self.with_storage(|s| s.enable_bulk_loading())
890 }
891
892 pub fn disable_bulk_loading(&self) -> Result<()> {
894 self.with_storage(|s| s.disable_bulk_loading())
895 }
896
897 pub fn query(
903 &self,
904 request: actions::query::QueryRequest,
905 ) -> Result<actions::query::QueryResponse> {
906 self.with_storage(|s| pollster::block_on(actions::query::execute(s, request)))
907 }
908
909 pub fn scan(&self, request: actions::scan::ScanRequest) -> Result<actions::scan::ScanResponse> {
911 self.with_storage(|s| pollster::block_on(actions::scan::execute(s, request)))
912 }
913
914 pub fn search_vectors(
916 &self,
917 request: actions::search_vectors::SearchVectorsRequest,
918 ) -> Result<actions::search_vectors::SearchVectorsResponse> {
919 self.with_storage(|s| {
920 pollster::block_on(actions::search_vectors::execute(
921 s,
922 request,
923 &self.vector_lifecycle,
924 ))
925 })
926 }
927
928 pub fn transact_write_items(
939 &self,
940 request: actions::transact_write_items::TransactWriteItemsRequest,
941 ) -> Result<actions::transact_write_items::TransactWriteItemsResponse> {
942 run_idempotent(
943 &self.tokens.transact_write,
944 request.client_request_token.as_deref(),
945 &request.transact_items,
946 || {
947 self.with_storage(|s| {
948 pollster::block_on(actions::transact_write_items::execute_cached(
949 s,
950 request.clone(),
951 ))
952 })
953 },
954 |cached| {
955 actions::transact_write_items::replay_response(
960 cached,
961 &request.return_consumed_capacity,
962 )
963 },
964 )
965 .map(|cached| cached.response)
966 }
967
968 pub fn transact_get_items(
970 &self,
971 request: actions::transact_get_items::TransactGetItemsRequest,
972 ) -> Result<actions::transact_get_items::TransactGetItemsResponse> {
973 self.with_storage(|s| pollster::block_on(actions::transact_get_items::execute(s, request)))
974 }
975
976 pub fn list_streams(
982 &self,
983 request: actions::list_streams::ListStreamsRequest,
984 ) -> Result<actions::list_streams::ListStreamsResponse> {
985 self.with_storage(|s| pollster::block_on(actions::list_streams::execute(s, request)))
986 }
987
988 pub fn describe_stream(
990 &self,
991 request: actions::describe_stream::DescribeStreamRequest,
992 ) -> Result<actions::describe_stream::DescribeStreamResponse> {
993 self.with_storage(|s| pollster::block_on(actions::describe_stream::execute(s, request)))
994 }
995
996 pub fn get_shard_iterator(
998 &self,
999 request: actions::get_shard_iterator::GetShardIteratorRequest,
1000 ) -> Result<actions::get_shard_iterator::GetShardIteratorResponse> {
1001 self.with_storage(|s| pollster::block_on(actions::get_shard_iterator::execute(s, request)))
1002 }
1003
1004 pub fn get_records(
1006 &self,
1007 request: actions::get_records::GetRecordsRequest,
1008 ) -> Result<actions::get_records::GetRecordsResponse> {
1009 self.with_storage(|s| pollster::block_on(actions::get_records::execute(s, request)))
1010 }
1011
1012 pub fn update_time_to_live(
1018 &self,
1019 request: actions::update_time_to_live::UpdateTimeToLiveRequest,
1020 ) -> Result<actions::update_time_to_live::UpdateTimeToLiveResponse> {
1021 self.with_storage(|s| pollster::block_on(actions::update_time_to_live::execute(s, request)))
1022 }
1023
1024 pub fn describe_time_to_live(
1026 &self,
1027 request: actions::describe_time_to_live::DescribeTimeToLiveRequest,
1028 ) -> Result<actions::describe_time_to_live::DescribeTimeToLiveResponse> {
1029 self.with_storage(|s| {
1030 pollster::block_on(actions::describe_time_to_live::execute(s, request))
1031 })
1032 }
1033
1034 pub fn sweep_ttl(&self) -> Result<usize> {
1037 self.with_storage(|s| pollster::block_on(ttl::sweep_expired_items(s)))
1038 }
1039
1040 pub fn execute_statement(
1046 &self,
1047 request: actions::execute_statement::ExecuteStatementRequest,
1048 ) -> Result<actions::execute_statement::ExecuteStatementResponse> {
1049 self.with_storage(|s| pollster::block_on(actions::execute_statement::execute(s, request)))
1050 }
1051
1052 pub fn execute_transaction(
1060 &self,
1061 request: actions::execute_transaction::ExecuteTransactionRequest,
1062 ) -> Result<actions::execute_transaction::ExecuteTransactionResponse> {
1063 actions::execute_transaction::reject_bad_capacity_mode(
1067 request.return_consumed_capacity.as_deref(),
1068 )?;
1069 run_idempotent(
1070 &self.tokens.execute_transaction,
1071 request.client_request_token.as_deref(),
1072 &request.transact_statements,
1073 || {
1074 self.with_storage(|s| {
1075 pollster::block_on(actions::execute_transaction::execute_cached(
1076 s,
1077 request.clone(),
1078 ))
1079 })
1080 },
1081 |cached| {
1082 actions::execute_transaction::replay_response(
1085 cached,
1086 &request.return_consumed_capacity,
1087 )
1088 },
1089 )
1090 .map(|cached| cached.response)
1091 }
1092
1093 pub fn batch_execute_statement(
1095 &self,
1096 request: actions::batch_execute_statement::BatchExecuteStatementRequest,
1097 ) -> Result<actions::batch_execute_statement::BatchExecuteStatementResponse> {
1098 self.with_storage(|s| {
1099 pollster::block_on(actions::batch_execute_statement::execute(s, request))
1100 })
1101 }
1102
1103 pub fn touch_cached_at(
1112 &self,
1113 table_name: &str,
1114 pk: &str,
1115 sk: &str,
1116 timestamp: f64,
1117 ) -> Result<()> {
1118 self.with_storage(|s| s.touch_cached_at(table_name, pk, sk, timestamp))
1119 }
1120
1121 pub fn get_lru_items(
1126 &self,
1127 table_name: &str,
1128 limit: usize,
1129 ) -> Result<Vec<(String, String, i64)>> {
1130 self.with_storage(|s| s.get_lru_items(table_name, limit))
1131 }
1132
1133 pub fn db_path(&self) -> Result<Option<String>> {
1139 self.with_storage(|s| Ok(s.db_path()))
1140 }
1141
1142 pub fn db_size_bytes(&self) -> Result<u64> {
1144 self.with_storage(|s| s.db_size_bytes())
1145 }
1146
1147 pub fn table_count(&self) -> Result<usize> {
1149 self.with_storage(|s| s.table_count())
1150 }
1151
1152 pub fn table_stats(&self) -> Result<Vec<TableStats>> {
1154 self.with_storage(|s| s.table_stats())
1155 }
1156
1157 pub fn get_table_metadata(&self, table_name: &str) -> Result<Option<storage::TableMetadata>> {
1159 self.with_storage(|s| s.get_table_metadata(table_name))
1160 }
1161
1162 pub fn database_info(&self) -> Result<DatabaseInfo> {
1167 self.with_storage(|s| s.database_info())
1168 }
1169
1170 pub fn vacuum(&self) -> Result<()> {
1176 self.with_storage(|s| s.vacuum())
1177 }
1178
1179 pub fn vacuum_into(&self, path: &str) -> Result<()> {
1184 self.with_storage(|s| s.vacuum_into(path))
1185 }
1186
1187 pub fn restore_from(&self, path: &str) -> Result<()> {
1193 self.with_storage_mut(|s| s.restore_from(path))
1194 }
1195
1196 #[cfg(feature = "mcp-server")]
1201 pub(crate) fn backup_to_memory(&self) -> Result<rusqlite::Connection> {
1202 self.with_storage(|s| s.backup_to_memory())
1203 }
1204
1205 #[cfg(feature = "mcp-server")]
1209 pub(crate) fn restore_from_connection(&self, source: &rusqlite::Connection) -> Result<()> {
1210 self.with_storage_mut(|s| s.restore_from_connection(source))
1211 }
1212}
1213
1214#[cfg(feature = "wasm-sqlite")]
1228impl Database<WasmBridgeBackend> {
1229 pub async fn open(name: &str) -> Result<Self> {
1232 Self::open_with(name, false).await
1233 }
1234
1235 pub async fn open_with(name: &str, ephemeral: bool) -> Result<Self> {
1238 let backend = WasmBridgeBackend::open_with(name, ephemeral)
1239 .await
1240 .map_err(DynoxideError::from)?;
1241 Ok(Self {
1242 inner: Arc::new(BackendMutex::new(backend)),
1243 tokens: Arc::new(TokenCaches::new()),
1244 vector_lifecycle: Arc::new(VectorIndexLifecycle::new()),
1245 })
1246 }
1247
1248 pub async fn persistence_mode(&self) -> String {
1250 self.backend().await.persistence_mode().to_string()
1251 }
1252
1253 pub async fn close(&self) -> Result<()> {
1257 self.backend()
1258 .await
1259 .close()
1260 .await
1261 .map_err(DynoxideError::from)
1262 }
1263
1264 pub(crate) async fn backend(&self) -> async_lock::MutexGuard<'_, WasmBridgeBackend> {
1273 self.inner.lock().await
1274 }
1275
1276 pub(crate) fn token_caches(&self) -> &TokenCaches {
1280 &self.tokens
1281 }
1282
1283 pub(crate) fn vector_lifecycle(&self) -> &VectorIndexLifecycle {
1287 &self.vector_lifecycle
1288 }
1289
1290 pub async fn create_table(
1292 &self,
1293 request: actions::create_table::CreateTableRequest,
1294 ) -> Result<actions::create_table::CreateTableResponse> {
1295 let backend = self.backend().await;
1296 actions::create_table::execute(&*backend, request).await
1297 }
1298
1299 pub async fn delete_table(
1301 &self,
1302 request: actions::delete_table::DeleteTableRequest,
1303 ) -> Result<actions::delete_table::DeleteTableResponse> {
1304 let backend = self.backend().await;
1305 actions::delete_table::execute(&*backend, request, &self.vector_lifecycle).await
1306 }
1307
1308 pub async fn describe_table(
1310 &self,
1311 request: actions::describe_table::DescribeTableRequest,
1312 ) -> Result<actions::describe_table::DescribeTableResponse> {
1313 let backend = self.backend().await;
1314 actions::describe_table::execute(&*backend, request, &self.vector_lifecycle).await
1315 }
1316
1317 pub async fn list_tables(
1319 &self,
1320 request: actions::list_tables::ListTablesRequest,
1321 ) -> Result<actions::list_tables::ListTablesResponse> {
1322 let backend = self.backend().await;
1323 actions::list_tables::execute(&*backend, request).await
1324 }
1325
1326 pub async fn put_item(
1328 &self,
1329 request: actions::put_item::PutItemRequest,
1330 ) -> Result<actions::put_item::PutItemResponse> {
1331 let backend = self.backend().await;
1332 actions::put_item::execute(&*backend, request).await
1333 }
1334
1335 pub async fn get_item(
1337 &self,
1338 request: actions::get_item::GetItemRequest,
1339 ) -> Result<actions::get_item::GetItemResponse> {
1340 let backend = self.backend().await;
1341 actions::get_item::execute(&*backend, request).await
1342 }
1343
1344 pub async fn delete_item(
1346 &self,
1347 request: actions::delete_item::DeleteItemRequest,
1348 ) -> Result<actions::delete_item::DeleteItemResponse> {
1349 let backend = self.backend().await;
1350 actions::delete_item::execute(&*backend, request).await
1351 }
1352
1353 pub async fn query(
1355 &self,
1356 request: actions::query::QueryRequest,
1357 ) -> Result<actions::query::QueryResponse> {
1358 let backend = self.backend().await;
1359 actions::query::execute(&*backend, request).await
1360 }
1361
1362 pub async fn scan(
1364 &self,
1365 request: actions::scan::ScanRequest,
1366 ) -> Result<actions::scan::ScanResponse> {
1367 let backend = self.backend().await;
1368 actions::scan::execute(&*backend, request).await
1369 }
1370
1371 pub async fn search_vectors(
1375 &self,
1376 request: actions::search_vectors::SearchVectorsRequest,
1377 ) -> Result<actions::search_vectors::SearchVectorsResponse> {
1378 let backend = self.backend().await;
1379 actions::search_vectors::execute(&*backend, request, &self.vector_lifecycle).await
1380 }
1381}
1382
1383#[cfg(all(test, any(feature = "native-sqlite", feature = "_has-encryption")))]
1384mod tests {
1385 use super::*;
1386
1387 #[test]
1388 fn test_database_memory() {
1389 let db = Database::memory().unwrap();
1390 let _db2 = db.clone();
1392 }
1393
1394 #[test]
1395 fn test_database_with_storage() {
1396 let db = Database::memory().unwrap();
1397 let tables = db.with_storage(|s| s.list_table_names()).unwrap();
1398 assert!(tables.is_empty());
1399 }
1400
1401 #[test]
1402 fn test_database_thread_safe() {
1403 let db = Database::memory().unwrap();
1404 let db2 = db.clone();
1405
1406 let handle =
1407 std::thread::spawn(move || db2.with_storage(|s| s.list_table_names()).unwrap());
1408
1409 let tables = handle.join().unwrap();
1410 assert!(tables.is_empty());
1411 }
1412
1413 #[test]
1414 fn test_native_database_alias_round_trips() {
1415 let db: NativeDatabase = Database::memory().unwrap();
1419
1420 db.create_table(actions::create_table::CreateTableRequest {
1421 table_name: "tbl".to_string(),
1422 key_schema: vec![types::KeySchemaElement {
1423 attribute_name: "pk".to_string(),
1424 key_type: types::KeyType::HASH,
1425 }],
1426 attribute_definitions: vec![types::AttributeDefinition {
1427 attribute_name: "pk".to_string(),
1428 attribute_type: types::ScalarAttributeType::S,
1429 }],
1430 ..Default::default()
1431 })
1432 .unwrap();
1433
1434 let mut item = HashMap::new();
1435 item.insert("pk".to_string(), AttributeValue::S("a".to_string()));
1436 db.put_item(actions::put_item::PutItemRequest {
1437 table_name: "tbl".to_string(),
1438 item,
1439 ..Default::default()
1440 })
1441 .unwrap();
1442
1443 let mut key = HashMap::new();
1444 key.insert("pk".to_string(), AttributeValue::S("a".to_string()));
1445 let got = db
1446 .get_item(actions::get_item::GetItemRequest {
1447 table_name: "tbl".to_string(),
1448 key,
1449 ..Default::default()
1450 })
1451 .unwrap();
1452 assert_eq!(
1453 got.item.unwrap().get("pk"),
1454 Some(&AttributeValue::S("a".to_string()))
1455 );
1456 }
1457}
1458
1459#[cfg(test)]
1465mod idempotency_tests {
1466 use super::*;
1467 use std::cell::Cell;
1468
1469 const KEY: &str = "statements";
1470 const TOKEN: &str = "tok";
1471
1472 fn cache() -> Mutex<TokenCache<u32>> {
1473 Mutex::new(HashMap::new())
1474 }
1475
1476 fn hash() -> u64 {
1477 request_hash(&KEY)
1478 }
1479
1480 fn drive<F>(cache: &Mutex<TokenCache<u32>>, token: Option<&str>, execute: F) -> Result<u32>
1481 where
1482 F: std::future::Future<Output = Result<u32>>,
1483 {
1484 pollster::block_on(run_idempotent_async(cache, token, &KEY, execute, |c| *c))
1485 }
1486
1487 #[test]
1488 fn a_claim_is_visible_to_the_next_caller_and_settles_into_a_hit() {
1489 let cache = cache();
1490
1491 let TokenClaim::Marked(at) = lookup_or_claim(&cache, TOKEN, hash()).unwrap() else {
1492 panic!("the first caller should have claimed the token");
1493 };
1494 assert!(matches!(
1495 lookup_or_claim(&cache, TOKEN, hash()).unwrap(),
1496 TokenClaim::InFlight
1497 ));
1498
1499 record_complete(&cache, TOKEN, at, hash(), 7).unwrap();
1500 assert!(matches!(
1501 lookup_or_claim(&cache, TOKEN, hash()).unwrap(),
1502 TokenClaim::Hit(7)
1503 ));
1504 }
1505
1506 #[test]
1507 fn only_one_caller_can_claim_a_token() {
1508 use std::sync::Barrier;
1512
1513 const N: usize = 16;
1514 let cache = cache();
1515 let barrier = Barrier::new(N);
1516
1517 let marked = std::thread::scope(|scope| {
1518 let handles: Vec<_> = (0..N)
1519 .map(|_| {
1520 scope.spawn(|| {
1521 barrier.wait();
1522 matches!(
1523 lookup_or_claim(&cache, TOKEN, hash()).unwrap(),
1524 TokenClaim::Marked(_)
1525 )
1526 })
1527 })
1528 .collect();
1529 handles
1530 .into_iter()
1531 .map(|h| h.join().unwrap())
1532 .filter(|claimed| *claimed)
1533 .count()
1534 });
1535
1536 assert_eq!(marked, 1);
1537 }
1538
1539 #[test]
1540 fn a_call_whose_claim_expired_does_not_disturb_the_caller_that_took_over() {
1541 let cache = cache();
1545 let TokenClaim::Marked(first) = lookup_or_claim(&cache, TOKEN, hash()).unwrap() else {
1546 panic!("the first caller should have claimed the token");
1547 };
1548 let TokenClaim::Marked(second) =
1549 lookup_or_claim_within(&cache, TOKEN, hash(), Duration::ZERO).unwrap()
1550 else {
1551 panic!("the expired claim should have been re-issued");
1552 };
1553 assert_ne!(first, second);
1554
1555 record_complete(&cache, TOKEN, first, hash(), 1).unwrap();
1557 assert!(matches!(
1558 lookup_or_claim(&cache, TOKEN, hash()).unwrap(),
1559 TokenClaim::InFlight
1560 ));
1561
1562 clear_claim(&cache, TOKEN, first).unwrap();
1564 assert!(matches!(
1565 lookup_or_claim(&cache, TOKEN, hash()).unwrap(),
1566 TokenClaim::InFlight
1567 ));
1568
1569 record_complete(&cache, TOKEN, second, hash(), 2).unwrap();
1571 assert!(matches!(
1572 lookup_or_claim(&cache, TOKEN, hash()).unwrap(),
1573 TokenClaim::Hit(2)
1574 ));
1575 }
1576
1577 #[test]
1578 fn a_different_request_under_the_same_token_mismatches_in_both_states() {
1579 let other = request_hash(&"different");
1580
1581 let cache = cache();
1582 let TokenClaim::Marked(at) = lookup_or_claim(&cache, TOKEN, hash()).unwrap() else {
1583 panic!("expected a free slot to claim");
1584 };
1585 assert!(matches!(
1586 lookup_or_claim(&cache, TOKEN, other).unwrap(),
1587 TokenClaim::Mismatch
1588 ));
1589
1590 record_complete(&cache, TOKEN, at, hash(), 7).unwrap();
1591 assert!(matches!(
1592 lookup_or_claim(&cache, TOKEN, other).unwrap(),
1593 TokenClaim::Mismatch
1594 ));
1595 }
1596
1597 #[test]
1598 fn the_claim_lands_before_the_call_is_polled() {
1599 let cache = cache();
1601 let claimed_first = Cell::new(false);
1602
1603 let out = drive(&cache, Some(TOKEN), async {
1604 let slots = cache
1607 .try_lock()
1608 .expect("the cache lock must be released before the call runs");
1609 claimed_first.set(matches!(slots.get(TOKEN), Some((_, h, None)) if *h == hash()));
1610 Ok(7)
1611 });
1612
1613 assert_eq!(out.unwrap(), 7);
1614 assert!(claimed_first.get());
1615 }
1616
1617 #[test]
1618 fn a_caller_arriving_under_a_live_claim_does_not_execute() {
1619 let cache = cache();
1620 let second_ran = Cell::new(false);
1621
1622 let out = drive(&cache, Some(TOKEN), async {
1623 let second = drive(&cache, Some(TOKEN), async {
1624 second_ran.set(true);
1625 Ok(0)
1626 });
1627 assert_eq!(second.unwrap_err().to_string(), IN_FLIGHT_MESSAGE);
1630 assert!(matches!(
1633 lookup_or_claim(&cache, TOKEN, hash()).unwrap(),
1634 TokenClaim::InFlight
1635 ));
1636 Ok(1)
1637 });
1638
1639 assert_eq!(out.unwrap(), 1);
1640 assert!(!second_ran.get(), "the second call must not run the work");
1641 assert!(matches!(
1643 lookup_or_claim(&cache, TOKEN, hash()).unwrap(),
1644 TokenClaim::Hit(1)
1645 ));
1646 }
1647
1648 #[test]
1649 fn the_synchronous_driver_treats_a_live_claim_as_absent() {
1650 let cache = cache();
1655 lookup_or_claim(&cache, TOKEN, hash()).unwrap();
1656
1657 let out = run_idempotent(&cache, Some(TOKEN), &KEY, || Ok(9), |c| *c).unwrap();
1658 assert_eq!(
1659 out, 9,
1660 "a claimed slot is not treated as a replayable result"
1661 );
1662 }
1663
1664 #[test]
1665 fn a_settled_token_replays_without_re_executing() {
1666 let cache = cache();
1667 let runs = Cell::new(0);
1668
1669 for _ in 0..2 {
1670 let out = drive(&cache, Some(TOKEN), async {
1671 runs.set(runs.get() + 1);
1672 Ok(7)
1673 });
1674 assert_eq!(out.unwrap(), 7);
1675 }
1676 assert_eq!(runs.get(), 1);
1677 }
1678
1679 #[test]
1680 fn a_failed_call_releases_its_claim_so_a_retry_re_executes() {
1681 let cache = cache();
1682 let runs = Cell::new(0);
1683
1684 let first = drive(&cache, Some(TOKEN), async {
1685 runs.set(runs.get() + 1);
1686 Err(DynoxideError::ValidationException("no".into()))
1687 });
1688 assert!(first.is_err());
1689 assert!(
1690 cache.lock().unwrap().is_empty(),
1691 "the claim must be released"
1692 );
1693
1694 let second = drive(&cache, Some(TOKEN), async {
1695 runs.set(runs.get() + 1);
1696 Ok(7)
1697 });
1698 assert_eq!(second.unwrap(), 7);
1699 assert_eq!(runs.get(), 2);
1700 }
1701
1702 #[test]
1703 fn a_claim_left_by_a_dropped_call_expires_rather_than_wedging_the_token() {
1704 let cache = cache();
1707 lookup_or_claim(&cache, TOKEN, hash()).unwrap();
1708
1709 assert!(matches!(
1710 lookup_or_claim_within(&cache, TOKEN, hash(), Duration::ZERO).unwrap(),
1711 TokenClaim::Marked(_)
1712 ));
1713 }
1714
1715 #[test]
1716 fn a_settled_token_stops_replaying_once_it_expires() {
1717 let cache = cache();
1718 let TokenClaim::Marked(at) = lookup_or_claim(&cache, TOKEN, hash()).unwrap() else {
1719 panic!("expected a free slot to claim");
1720 };
1721 record_complete(&cache, TOKEN, at, hash(), 7).unwrap();
1722
1723 assert!(matches!(
1725 lookup_or_claim_within(&cache, TOKEN, hash(), token_window()).unwrap(),
1726 TokenClaim::Hit(7)
1727 ));
1728 assert!(matches!(
1730 lookup_or_claim_within(&cache, TOKEN, hash(), Duration::ZERO).unwrap(),
1731 TokenClaim::Marked(_)
1732 ));
1733 }
1734
1735 #[test]
1736 fn concurrent_callers_under_one_token_execute_the_work_once() {
1737 use std::sync::Barrier;
1741 use std::sync::atomic::{AtomicUsize, Ordering};
1742
1743 const N: usize = 16;
1744 let cache = cache();
1745 let runs = AtomicUsize::new(0);
1746 let barrier = Barrier::new(N);
1747
1748 let outcomes: Vec<Result<u32>> = std::thread::scope(|scope| {
1749 let handles: Vec<_> = (0..N)
1750 .map(|_| {
1751 scope.spawn(|| {
1752 barrier.wait();
1753 drive(&cache, Some(TOKEN), async {
1754 runs.fetch_add(1, Ordering::SeqCst);
1755 Ok(7)
1756 })
1757 })
1758 })
1759 .collect();
1760 handles.into_iter().map(|h| h.join().unwrap()).collect()
1761 });
1762
1763 assert_eq!(runs.load(Ordering::SeqCst), 1, "the work must run once");
1764 for outcome in outcomes {
1767 match outcome {
1768 Ok(v) => assert_eq!(v, 7),
1769 Err(e) => assert_eq!(e.to_string(), IN_FLIGHT_MESSAGE),
1770 }
1771 }
1772 }
1773
1774 #[test]
1775 fn an_overlong_token_is_rejected_with_dynamodbs_message() {
1776 let token = "x".repeat(MAX_TOKEN_LEN + 1);
1777 let err = drive(&cache(), Some(&token), async { Ok(7) }).unwrap_err();
1778 assert_eq!(
1779 err.to_string(),
1780 format!(
1781 "1 validation error detected: Value '{token}' at 'clientRequestToken' failed to satisfy constraint: Member must have length less than or equal to {MAX_TOKEN_LEN}"
1782 )
1783 );
1784 }
1785
1786 #[test]
1787 fn a_tokenless_call_never_touches_the_cache() {
1788 let cache = cache();
1789 let runs = Cell::new(0);
1790
1791 for _ in 0..2 {
1792 drive(&cache, None, async {
1793 runs.set(runs.get() + 1);
1794 Ok(7)
1795 })
1796 .unwrap();
1797 }
1798 assert_eq!(runs.get(), 2);
1799 assert!(cache.lock().unwrap().is_empty());
1800 }
1801}