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(feature = "wasm-harness")]
65pub mod wasm_harness;
66
67#[doc(hidden)]
68pub use macros::ItemInsert;
69
70use std::collections::HashMap;
71use std::sync::{Arc, Mutex};
72use web_time::Instant;
73
74pub use errors::{DynoxideError, Result};
75pub use storage::{DatabaseInfo, TableInfoEntry, TableMetadata, TableStats};
76pub use storage_backend::BackendError;
77#[cfg(feature = "wasm-sqlite")]
78pub use storage_backend::WasmBridgeBackend;
79pub use types::{AttributeValue, ConversionError, Item};
80
81#[derive(Debug, Clone, Default)]
83pub struct ImportOptions {
84 pub record_streams: bool,
86 pub set_cached_at: bool,
88}
89
90#[derive(Debug, Clone)]
92pub struct ImportResult {
93 pub items_imported: usize,
95 pub bytes_imported: usize,
97}
98
99type TokenCache = HashMap<
101 String,
102 (
103 Instant,
104 u64,
105 actions::transact_write_items::TransactWriteItemsResponse,
106 ),
107>;
108
109#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
114pub type RusqliteBackend = storage::Storage;
115
116#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
124pub type NativeDatabase = Database<RusqliteBackend>;
125
126#[cfg(feature = "wasm-sqlite")]
133pub type WasmDatabase = Database<WasmBridgeBackend>;
134
135#[cfg(feature = "wasm-sqlite")]
143pub const WASM_PREVIEW: bool = true;
144#[cfg(not(feature = "wasm-sqlite"))]
147pub const WASM_PREVIEW: bool = false;
148
149#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
158pub struct Database<S = RusqliteBackend> {
159 inner: Arc<Mutex<S>>,
160 idempotency_tokens: Arc<Mutex<TokenCache>>,
161}
162
163#[cfg(all(
169 not(any(feature = "native-sqlite", feature = "_has-encryption")),
170 feature = "wasm-sqlite"
171))]
172use async_lock::Mutex as BackendMutex;
173#[cfg(all(
174 not(any(feature = "native-sqlite", feature = "_has-encryption")),
175 not(feature = "wasm-sqlite")
176))]
177use std::sync::Mutex as BackendMutex;
178
179#[cfg(not(any(feature = "native-sqlite", feature = "_has-encryption")))]
189pub struct Database<S> {
190 inner: Arc<BackendMutex<S>>,
191 idempotency_tokens: Arc<Mutex<TokenCache>>,
192}
193
194impl<S> Clone for Database<S> {
196 fn clone(&self) -> Self {
197 Self {
198 inner: Arc::clone(&self.inner),
199 idempotency_tokens: Arc::clone(&self.idempotency_tokens),
200 }
201 }
202}
203
204#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
205impl Database<RusqliteBackend> {
206 pub fn new(path: &str) -> Result<Self> {
208 let storage = storage::Storage::new(path)?;
209 Ok(Self {
210 inner: Arc::new(Mutex::new(storage)),
211 idempotency_tokens: Arc::new(Mutex::new(HashMap::new())),
212 })
213 }
214
215 #[cfg(feature = "_has-encryption")]
236 pub fn new_encrypted(path: &str, key: &str) -> Result<Self> {
237 if key.len() != 64 || !key.bytes().all(|b| b.is_ascii_hexdigit()) {
238 return Err(DynoxideError::ValidationException(
239 "Encryption key must be a 64-character hex string (32 bytes)".to_string(),
240 ));
241 }
242
243 let storage = storage::Storage::new_encrypted(path, key)?;
244 Ok(Self {
245 inner: Arc::new(Mutex::new(storage)),
246 idempotency_tokens: Arc::new(Mutex::new(HashMap::new())),
247 })
248 }
249
250 pub fn memory() -> Result<Self> {
252 let storage = storage::Storage::memory()?;
253 Ok(Self {
254 inner: Arc::new(Mutex::new(storage)),
255 idempotency_tokens: Arc::new(Mutex::new(HashMap::new())),
256 })
257 }
258
259 pub(crate) fn with_storage<F, T>(&self, f: F) -> Result<T>
261 where
262 F: FnOnce(&storage::Storage) -> Result<T>,
263 {
264 let guard = self
265 .inner
266 .lock()
267 .map_err(|e| DynoxideError::InternalServerError(format!("Lock poisoned: {e}")))?;
268 f(&guard)
269 }
270
271 pub(crate) fn with_storage_mut<F, T>(&self, f: F) -> Result<T>
273 where
274 F: FnOnce(&mut storage::Storage) -> Result<T>,
275 {
276 let mut guard = self
277 .inner
278 .lock()
279 .map_err(|e| DynoxideError::InternalServerError(format!("Lock poisoned: {e}")))?;
280 f(&mut guard)
281 }
282
283 pub fn create_table(
289 &self,
290 request: actions::create_table::CreateTableRequest,
291 ) -> Result<actions::create_table::CreateTableResponse> {
292 self.with_storage(|s| pollster::block_on(actions::create_table::execute(s, request)))
293 }
294
295 pub fn delete_table(
297 &self,
298 request: actions::delete_table::DeleteTableRequest,
299 ) -> Result<actions::delete_table::DeleteTableResponse> {
300 self.with_storage(|s| pollster::block_on(actions::delete_table::execute(s, request)))
301 }
302
303 pub fn describe_table(
305 &self,
306 request: actions::describe_table::DescribeTableRequest,
307 ) -> Result<actions::describe_table::DescribeTableResponse> {
308 self.with_storage(|s| pollster::block_on(actions::describe_table::execute(s, request)))
309 }
310
311 pub fn update_table(
313 &self,
314 request: actions::update_table::UpdateTableRequest,
315 ) -> Result<actions::update_table::UpdateTableResponse> {
316 self.with_storage(|s| pollster::block_on(actions::update_table::execute(s, request)))
317 }
318
319 pub fn list_tables(
321 &self,
322 request: actions::list_tables::ListTablesRequest,
323 ) -> Result<actions::list_tables::ListTablesResponse> {
324 self.with_storage(|s| pollster::block_on(actions::list_tables::execute(s, request)))
325 }
326
327 pub fn tag_resource(
333 &self,
334 request: actions::tag_resource::TagResourceRequest,
335 ) -> Result<actions::tag_resource::TagResourceResponse> {
336 self.with_storage(|s| pollster::block_on(actions::tag_resource::execute(s, request)))
337 }
338
339 pub fn untag_resource(
341 &self,
342 request: actions::untag_resource::UntagResourceRequest,
343 ) -> Result<actions::untag_resource::UntagResourceResponse> {
344 self.with_storage(|s| pollster::block_on(actions::untag_resource::execute(s, request)))
345 }
346
347 pub fn list_tags_of_resource(
349 &self,
350 request: actions::list_tags_of_resource::ListTagsOfResourceRequest,
351 ) -> Result<actions::list_tags_of_resource::ListTagsOfResourceResponse> {
352 self.with_storage(|s| {
353 pollster::block_on(actions::list_tags_of_resource::execute(s, request))
354 })
355 }
356
357 pub fn put_item(
363 &self,
364 request: actions::put_item::PutItemRequest,
365 ) -> Result<actions::put_item::PutItemResponse> {
366 self.with_storage(|s| pollster::block_on(actions::put_item::execute(s, request)))
367 }
368
369 pub fn get_item(
371 &self,
372 request: actions::get_item::GetItemRequest,
373 ) -> Result<actions::get_item::GetItemResponse> {
374 self.with_storage(|s| pollster::block_on(actions::get_item::execute(s, request)))
375 }
376
377 pub fn delete_item(
379 &self,
380 request: actions::delete_item::DeleteItemRequest,
381 ) -> Result<actions::delete_item::DeleteItemResponse> {
382 self.with_storage(|s| pollster::block_on(actions::delete_item::execute(s, request)))
383 }
384
385 pub fn update_item(
387 &self,
388 request: actions::update_item::UpdateItemRequest,
389 ) -> Result<actions::update_item::UpdateItemResponse> {
390 self.with_storage(|s| pollster::block_on(actions::update_item::execute(s, request)))
391 }
392
393 pub fn batch_get_item(
399 &self,
400 request: actions::batch_get_item::BatchGetItemRequest,
401 ) -> Result<actions::batch_get_item::BatchGetItemResponse> {
402 self.with_storage(|s| pollster::block_on(actions::batch_get_item::execute(s, request)))
403 }
404
405 pub fn batch_write_item(
407 &self,
408 request: actions::batch_write_item::BatchWriteItemRequest,
409 ) -> Result<actions::batch_write_item::BatchWriteItemResponse> {
410 self.with_storage(|s| pollster::block_on(actions::batch_write_item::execute(s, request)))
411 }
412
413 pub fn import_items(
428 &self,
429 table_name: &str,
430 items: Vec<Item>,
431 options: ImportOptions,
432 ) -> Result<ImportResult> {
433 self.with_storage(|s| {
434 pollster::block_on(actions::import_items::execute(
435 s, table_name, items, &options,
436 ))
437 })
438 }
439
440 #[cfg(feature = "import")]
446 pub(crate) fn import_items_fresh(
447 &self,
448 table_name: &str,
449 items: Vec<Item>,
450 options: ImportOptions,
451 ) -> Result<ImportResult> {
452 self.with_storage(|s| {
453 pollster::block_on(actions::import_items::execute_skip_gsi_deletes(
454 s, table_name, items, &options,
455 ))
456 })
457 }
458
459 pub fn enable_bulk_loading(&self) -> Result<()> {
468 self.with_storage(|s| s.enable_bulk_loading())
469 }
470
471 pub fn disable_bulk_loading(&self) -> Result<()> {
473 self.with_storage(|s| s.disable_bulk_loading())
474 }
475
476 pub fn query(
482 &self,
483 request: actions::query::QueryRequest,
484 ) -> Result<actions::query::QueryResponse> {
485 self.with_storage(|s| pollster::block_on(actions::query::execute(s, request)))
486 }
487
488 pub fn scan(&self, request: actions::scan::ScanRequest) -> Result<actions::scan::ScanResponse> {
490 self.with_storage(|s| pollster::block_on(actions::scan::execute(s, request)))
491 }
492
493 pub fn transact_write_items(
499 &self,
500 request: actions::transact_write_items::TransactWriteItemsRequest,
501 ) -> Result<actions::transact_write_items::TransactWriteItemsResponse> {
502 const TOKEN_EXPIRY_SECS: u64 = 600; const MAX_TOKEN_LEN: usize = 36;
504
505 if let Some(ref token) = request.client_request_token {
507 if token.len() > MAX_TOKEN_LEN {
508 return Err(DynoxideError::ValidationException(format!(
509 "1 validation error detected: Value '{}' at 'clientRequestToken' failed to satisfy constraint: Member must have length less than or equal to {}",
510 token, MAX_TOKEN_LEN
511 )));
512 }
513 }
514
515 let request_hash = if request.client_request_token.is_some() {
519 use std::hash::{Hash, Hasher};
520 let normalised = serde_json::to_value(&request.transact_items)
521 .and_then(|v| serde_json::to_vec(&v))
522 .unwrap_or_default();
523 let mut hasher = std::collections::hash_map::DefaultHasher::new();
524 normalised.hash(&mut hasher);
525 hasher.finish()
526 } else {
527 0
528 };
529
530 if let Some(ref token) = request.client_request_token {
532 let mut cache = self
533 .idempotency_tokens
534 .lock()
535 .map_err(|e| DynoxideError::InternalServerError(format!("Lock poisoned: {e}")))?;
536 cache.retain(|_, (ts, _, _)| ts.elapsed().as_secs() < TOKEN_EXPIRY_SECS);
538 if let Some((_, cached_hash, resp)) = cache.get(token) {
539 if *cached_hash != request_hash {
540 return Err(DynoxideError::IdempotentParameterMismatchException(
541 "An error occurred (IdempotentParameterMismatchException)".to_string(),
542 ));
543 }
544 return Ok(resp.clone());
545 }
546 }
547
548 let resp = self.with_storage(|s| {
549 pollster::block_on(actions::transact_write_items::execute(s, request.clone()))
550 })?;
551
552 if let Some(ref token) = request.client_request_token {
554 if let Ok(mut cache) = self.idempotency_tokens.lock() {
555 cache.insert(token.clone(), (Instant::now(), request_hash, resp.clone()));
556 }
557 }
558
559 Ok(resp)
560 }
561
562 pub fn transact_get_items(
564 &self,
565 request: actions::transact_get_items::TransactGetItemsRequest,
566 ) -> Result<actions::transact_get_items::TransactGetItemsResponse> {
567 self.with_storage(|s| pollster::block_on(actions::transact_get_items::execute(s, request)))
568 }
569
570 pub fn list_streams(
576 &self,
577 request: actions::list_streams::ListStreamsRequest,
578 ) -> Result<actions::list_streams::ListStreamsResponse> {
579 self.with_storage(|s| pollster::block_on(actions::list_streams::execute(s, request)))
580 }
581
582 pub fn describe_stream(
584 &self,
585 request: actions::describe_stream::DescribeStreamRequest,
586 ) -> Result<actions::describe_stream::DescribeStreamResponse> {
587 self.with_storage(|s| pollster::block_on(actions::describe_stream::execute(s, request)))
588 }
589
590 pub fn get_shard_iterator(
592 &self,
593 request: actions::get_shard_iterator::GetShardIteratorRequest,
594 ) -> Result<actions::get_shard_iterator::GetShardIteratorResponse> {
595 self.with_storage(|s| pollster::block_on(actions::get_shard_iterator::execute(s, request)))
596 }
597
598 pub fn get_records(
600 &self,
601 request: actions::get_records::GetRecordsRequest,
602 ) -> Result<actions::get_records::GetRecordsResponse> {
603 self.with_storage(|s| pollster::block_on(actions::get_records::execute(s, request)))
604 }
605
606 pub fn update_time_to_live(
612 &self,
613 request: actions::update_time_to_live::UpdateTimeToLiveRequest,
614 ) -> Result<actions::update_time_to_live::UpdateTimeToLiveResponse> {
615 self.with_storage(|s| pollster::block_on(actions::update_time_to_live::execute(s, request)))
616 }
617
618 pub fn describe_time_to_live(
620 &self,
621 request: actions::describe_time_to_live::DescribeTimeToLiveRequest,
622 ) -> Result<actions::describe_time_to_live::DescribeTimeToLiveResponse> {
623 self.with_storage(|s| {
624 pollster::block_on(actions::describe_time_to_live::execute(s, request))
625 })
626 }
627
628 pub fn sweep_ttl(&self) -> Result<usize> {
631 self.with_storage(|s| pollster::block_on(ttl::sweep_expired_items(s)))
632 }
633
634 pub fn execute_statement(
640 &self,
641 request: actions::execute_statement::ExecuteStatementRequest,
642 ) -> Result<actions::execute_statement::ExecuteStatementResponse> {
643 self.with_storage(|s| pollster::block_on(actions::execute_statement::execute(s, request)))
644 }
645
646 pub fn execute_transaction(
648 &self,
649 request: actions::execute_transaction::ExecuteTransactionRequest,
650 ) -> Result<actions::execute_transaction::ExecuteTransactionResponse> {
651 self.with_storage(|s| pollster::block_on(actions::execute_transaction::execute(s, request)))
652 }
653
654 pub fn batch_execute_statement(
656 &self,
657 request: actions::batch_execute_statement::BatchExecuteStatementRequest,
658 ) -> Result<actions::batch_execute_statement::BatchExecuteStatementResponse> {
659 self.with_storage(|s| {
660 pollster::block_on(actions::batch_execute_statement::execute(s, request))
661 })
662 }
663
664 pub fn touch_cached_at(
673 &self,
674 table_name: &str,
675 pk: &str,
676 sk: &str,
677 timestamp: f64,
678 ) -> Result<()> {
679 self.with_storage(|s| s.touch_cached_at(table_name, pk, sk, timestamp))
680 }
681
682 pub fn get_lru_items(
687 &self,
688 table_name: &str,
689 limit: usize,
690 ) -> Result<Vec<(String, String, i64)>> {
691 self.with_storage(|s| s.get_lru_items(table_name, limit))
692 }
693
694 pub fn db_path(&self) -> Result<Option<String>> {
700 self.with_storage(|s| Ok(s.db_path()))
701 }
702
703 pub fn db_size_bytes(&self) -> Result<u64> {
705 self.with_storage(|s| s.db_size_bytes())
706 }
707
708 pub fn table_count(&self) -> Result<usize> {
710 self.with_storage(|s| s.table_count())
711 }
712
713 pub fn table_stats(&self) -> Result<Vec<TableStats>> {
715 self.with_storage(|s| s.table_stats())
716 }
717
718 pub fn get_table_metadata(&self, table_name: &str) -> Result<Option<storage::TableMetadata>> {
720 self.with_storage(|s| s.get_table_metadata(table_name))
721 }
722
723 pub fn database_info(&self) -> Result<DatabaseInfo> {
728 self.with_storage(|s| s.database_info())
729 }
730
731 pub fn vacuum(&self) -> Result<()> {
737 self.with_storage(|s| s.vacuum())
738 }
739
740 pub fn vacuum_into(&self, path: &str) -> Result<()> {
745 self.with_storage(|s| s.vacuum_into(path))
746 }
747
748 pub fn restore_from(&self, path: &str) -> Result<()> {
754 self.with_storage_mut(|s| s.restore_from(path))
755 }
756
757 #[cfg(feature = "mcp-server")]
762 pub(crate) fn backup_to_memory(&self) -> Result<rusqlite::Connection> {
763 self.with_storage(|s| s.backup_to_memory())
764 }
765
766 #[cfg(feature = "mcp-server")]
770 pub(crate) fn restore_from_connection(&self, source: &rusqlite::Connection) -> Result<()> {
771 self.with_storage_mut(|s| s.restore_from_connection(source))
772 }
773}
774
775#[cfg(feature = "wasm-sqlite")]
789impl Database<WasmBridgeBackend> {
790 pub async fn open(name: &str) -> Result<Self> {
792 let backend = WasmBridgeBackend::open(name)
793 .await
794 .map_err(DynoxideError::from)?;
795 Ok(Self {
796 inner: Arc::new(BackendMutex::new(backend)),
797 idempotency_tokens: Arc::new(Mutex::new(HashMap::new())),
798 })
799 }
800
801 async fn backend(&self) -> async_lock::MutexGuard<'_, WasmBridgeBackend> {
806 self.inner.lock().await
807 }
808
809 pub async fn create_table(
811 &self,
812 request: actions::create_table::CreateTableRequest,
813 ) -> Result<actions::create_table::CreateTableResponse> {
814 let backend = self.backend().await;
815 actions::create_table::execute(&*backend, request).await
816 }
817
818 pub async fn delete_table(
820 &self,
821 request: actions::delete_table::DeleteTableRequest,
822 ) -> Result<actions::delete_table::DeleteTableResponse> {
823 let backend = self.backend().await;
824 actions::delete_table::execute(&*backend, request).await
825 }
826
827 pub async fn describe_table(
829 &self,
830 request: actions::describe_table::DescribeTableRequest,
831 ) -> Result<actions::describe_table::DescribeTableResponse> {
832 let backend = self.backend().await;
833 actions::describe_table::execute(&*backend, request).await
834 }
835
836 pub async fn list_tables(
838 &self,
839 request: actions::list_tables::ListTablesRequest,
840 ) -> Result<actions::list_tables::ListTablesResponse> {
841 let backend = self.backend().await;
842 actions::list_tables::execute(&*backend, request).await
843 }
844
845 pub async fn put_item(
847 &self,
848 request: actions::put_item::PutItemRequest,
849 ) -> Result<actions::put_item::PutItemResponse> {
850 let backend = self.backend().await;
851 actions::put_item::execute(&*backend, request).await
852 }
853
854 pub async fn get_item(
856 &self,
857 request: actions::get_item::GetItemRequest,
858 ) -> Result<actions::get_item::GetItemResponse> {
859 let backend = self.backend().await;
860 actions::get_item::execute(&*backend, request).await
861 }
862
863 pub async fn delete_item(
865 &self,
866 request: actions::delete_item::DeleteItemRequest,
867 ) -> Result<actions::delete_item::DeleteItemResponse> {
868 let backend = self.backend().await;
869 actions::delete_item::execute(&*backend, request).await
870 }
871
872 pub async fn query(
874 &self,
875 request: actions::query::QueryRequest,
876 ) -> Result<actions::query::QueryResponse> {
877 let backend = self.backend().await;
878 actions::query::execute(&*backend, request).await
879 }
880
881 pub async fn scan(
883 &self,
884 request: actions::scan::ScanRequest,
885 ) -> Result<actions::scan::ScanResponse> {
886 let backend = self.backend().await;
887 actions::scan::execute(&*backend, request).await
888 }
889}
890
891#[cfg(all(test, any(feature = "native-sqlite", feature = "_has-encryption")))]
892mod tests {
893 use super::*;
894
895 #[test]
896 fn test_database_memory() {
897 let db = Database::memory().unwrap();
898 let _db2 = db.clone();
900 }
901
902 #[test]
903 fn test_database_with_storage() {
904 let db = Database::memory().unwrap();
905 let tables = db.with_storage(|s| s.list_table_names()).unwrap();
906 assert!(tables.is_empty());
907 }
908
909 #[test]
910 fn test_database_thread_safe() {
911 let db = Database::memory().unwrap();
912 let db2 = db.clone();
913
914 let handle =
915 std::thread::spawn(move || db2.with_storage(|s| s.list_table_names()).unwrap());
916
917 let tables = handle.join().unwrap();
918 assert!(tables.is_empty());
919 }
920
921 #[test]
922 fn test_native_database_alias_round_trips() {
923 let db: NativeDatabase = Database::memory().unwrap();
927
928 db.create_table(actions::create_table::CreateTableRequest {
929 table_name: "tbl".to_string(),
930 key_schema: vec![types::KeySchemaElement {
931 attribute_name: "pk".to_string(),
932 key_type: types::KeyType::HASH,
933 }],
934 attribute_definitions: vec![types::AttributeDefinition {
935 attribute_name: "pk".to_string(),
936 attribute_type: types::ScalarAttributeType::S,
937 }],
938 ..Default::default()
939 })
940 .unwrap();
941
942 let mut item = HashMap::new();
943 item.insert("pk".to_string(), AttributeValue::S("a".to_string()));
944 db.put_item(actions::put_item::PutItemRequest {
945 table_name: "tbl".to_string(),
946 item,
947 ..Default::default()
948 })
949 .unwrap();
950
951 let mut key = HashMap::new();
952 key.insert("pk".to_string(), AttributeValue::S("a".to_string()));
953 let got = db
954 .get_item(actions::get_item::GetItemRequest {
955 table_name: "tbl".to_string(),
956 key,
957 ..Default::default()
958 })
959 .unwrap();
960 assert_eq!(
961 got.item.unwrap().get("pk"),
962 Some(&AttributeValue::S("a".to_string()))
963 );
964 }
965}