1#[cfg(all(feature = "native-sqlite", feature = "_has-encryption"))]
12compile_error!(
13 "Features `native-sqlite` and `encryption`/`encryption-cc` are mutually exclusive.\n\
14 If you ran `cargo install`, use:\n \
15 cargo install dynoxide-rs --no-default-features --features encrypted-server\n\
16 If using as a library dependency, set `default-features = false` \
17 and enable only one backend."
18);
19
20#[cfg(all(feature = "encryption", feature = "encryption-cc"))]
21compile_error!(
22 "Features `encryption` and `encryption-cc` are mutually exclusive. \
23 Use `encryption` for vendored OpenSSL or `encryption-cc` for Apple CommonCrypto."
24);
25
26#[cfg(all(feature = "encryption-cc", not(target_vendor = "apple")))]
27compile_error!(
28 "The `encryption-cc` feature is intended for Apple platforms only (CommonCrypto). \
29 Use the `encryption` feature for vendored OpenSSL on non-Apple platforms."
30);
31
32#[cfg(not(any(
33 feature = "native-sqlite",
34 feature = "_has-encryption",
35 feature = "wasm-sqlite"
36)))]
37compile_error!(
38 "A storage backend feature must be enabled: `native-sqlite`, `encryption`, \
39 `encryption-cc`, or `wasm-sqlite`. Default features include `native-sqlite`. \
40 If you used `default-features = false`, add one of these features."
41);
42
43pub mod actions;
44pub mod errors;
45pub mod expressions;
46#[cfg(feature = "import")]
47pub mod import;
48#[doc(hidden)]
49pub mod macros;
50#[cfg(feature = "mcp-server")]
51pub mod mcp;
52pub mod partiql;
53pub mod schema;
54#[cfg(feature = "http-server")]
55pub mod server;
56#[cfg(feature = "mcp-server")]
57pub(crate) mod snapshots;
58pub mod storage;
59pub mod storage_backend;
60pub mod streams;
61pub mod ttl;
62pub mod types;
63pub mod validation;
64#[cfg(any(feature = "http-server", feature = "wasm-sqlite", test))]
68pub(crate) mod dynamo_ops;
69#[cfg(any(feature = "wasm-sqlite", test))]
74pub mod wasm_api;
75#[cfg(feature = "wasm-harness")]
76pub mod wasm_harness;
77
78#[doc(hidden)]
79pub use macros::ItemInsert;
80
81use std::collections::HashMap;
82use std::sync::{Arc, Mutex};
83use web_time::Instant;
84
85pub use errors::{DynoxideError, Result};
86pub use storage::{DatabaseInfo, TableInfoEntry, TableMetadata, TableStats};
87pub use storage_backend::BackendError;
88#[cfg(feature = "wasm-sqlite")]
89pub use storage_backend::WasmBridgeBackend;
90pub use types::{AttributeValue, ConversionError, Item};
91
92#[derive(Debug, Clone, Default)]
94pub struct ImportOptions {
95 pub record_streams: bool,
97 pub set_cached_at: bool,
99}
100
101#[derive(Debug, Clone)]
103pub struct ImportResult {
104 pub items_imported: usize,
106 pub bytes_imported: usize,
108}
109
110type TokenCache = HashMap<
112 String,
113 (
114 Instant,
115 u64,
116 actions::transact_write_items::TransactWriteItemsResponse,
117 ),
118>;
119
120#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
125pub type RusqliteBackend = storage::Storage;
126
127#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
135pub type NativeDatabase = Database<RusqliteBackend>;
136
137#[cfg(feature = "wasm-sqlite")]
144pub type WasmDatabase = Database<WasmBridgeBackend>;
145
146#[cfg(feature = "wasm-sqlite")]
154pub const WASM_PREVIEW: bool = true;
155#[cfg(not(feature = "wasm-sqlite"))]
158pub const WASM_PREVIEW: bool = false;
159
160#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
169pub struct Database<S = RusqliteBackend> {
170 inner: Arc<Mutex<S>>,
171 idempotency_tokens: Arc<Mutex<TokenCache>>,
172}
173
174#[cfg(all(
180 not(any(feature = "native-sqlite", feature = "_has-encryption")),
181 feature = "wasm-sqlite"
182))]
183use async_lock::Mutex as BackendMutex;
184#[cfg(all(
185 not(any(feature = "native-sqlite", feature = "_has-encryption")),
186 not(feature = "wasm-sqlite")
187))]
188use std::sync::Mutex as BackendMutex;
189
190#[cfg(not(any(feature = "native-sqlite", feature = "_has-encryption")))]
200pub struct Database<S> {
201 inner: Arc<BackendMutex<S>>,
202 idempotency_tokens: Arc<Mutex<TokenCache>>,
203}
204
205impl<S> Clone for Database<S> {
207 fn clone(&self) -> Self {
208 Self {
209 inner: Arc::clone(&self.inner),
210 idempotency_tokens: Arc::clone(&self.idempotency_tokens),
211 }
212 }
213}
214
215#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
216impl Database<RusqliteBackend> {
217 pub fn new(path: &str) -> Result<Self> {
219 let storage = storage::Storage::new(path)?;
220 Ok(Self {
221 inner: Arc::new(Mutex::new(storage)),
222 idempotency_tokens: Arc::new(Mutex::new(HashMap::new())),
223 })
224 }
225
226 #[cfg(feature = "_has-encryption")]
247 pub fn new_encrypted(path: &str, key: &str) -> Result<Self> {
248 if key.len() != 64 || !key.bytes().all(|b| b.is_ascii_hexdigit()) {
249 return Err(DynoxideError::ValidationException(
250 "Encryption key must be a 64-character hex string (32 bytes)".to_string(),
251 ));
252 }
253
254 let storage = storage::Storage::new_encrypted(path, key)?;
255 Ok(Self {
256 inner: Arc::new(Mutex::new(storage)),
257 idempotency_tokens: Arc::new(Mutex::new(HashMap::new())),
258 })
259 }
260
261 pub fn memory() -> Result<Self> {
263 let storage = storage::Storage::memory()?;
264 Ok(Self {
265 inner: Arc::new(Mutex::new(storage)),
266 idempotency_tokens: Arc::new(Mutex::new(HashMap::new())),
267 })
268 }
269
270 pub(crate) fn with_storage<F, T>(&self, f: F) -> Result<T>
272 where
273 F: FnOnce(&storage::Storage) -> Result<T>,
274 {
275 let guard = self
276 .inner
277 .lock()
278 .map_err(|e| DynoxideError::InternalServerError(format!("Lock poisoned: {e}")))?;
279 f(&guard)
280 }
281
282 pub(crate) fn with_storage_mut<F, T>(&self, f: F) -> Result<T>
284 where
285 F: FnOnce(&mut storage::Storage) -> Result<T>,
286 {
287 let mut guard = self
288 .inner
289 .lock()
290 .map_err(|e| DynoxideError::InternalServerError(format!("Lock poisoned: {e}")))?;
291 f(&mut guard)
292 }
293
294 pub fn create_table(
300 &self,
301 request: actions::create_table::CreateTableRequest,
302 ) -> Result<actions::create_table::CreateTableResponse> {
303 self.with_storage(|s| pollster::block_on(actions::create_table::execute(s, request)))
304 }
305
306 pub fn delete_table(
308 &self,
309 request: actions::delete_table::DeleteTableRequest,
310 ) -> Result<actions::delete_table::DeleteTableResponse> {
311 self.with_storage(|s| pollster::block_on(actions::delete_table::execute(s, request)))
312 }
313
314 pub fn describe_table(
316 &self,
317 request: actions::describe_table::DescribeTableRequest,
318 ) -> Result<actions::describe_table::DescribeTableResponse> {
319 self.with_storage(|s| pollster::block_on(actions::describe_table::execute(s, request)))
320 }
321
322 pub fn update_table(
324 &self,
325 request: actions::update_table::UpdateTableRequest,
326 ) -> Result<actions::update_table::UpdateTableResponse> {
327 self.with_storage(|s| pollster::block_on(actions::update_table::execute(s, request)))
328 }
329
330 pub fn list_tables(
332 &self,
333 request: actions::list_tables::ListTablesRequest,
334 ) -> Result<actions::list_tables::ListTablesResponse> {
335 self.with_storage(|s| pollster::block_on(actions::list_tables::execute(s, request)))
336 }
337
338 pub fn tag_resource(
344 &self,
345 request: actions::tag_resource::TagResourceRequest,
346 ) -> Result<actions::tag_resource::TagResourceResponse> {
347 self.with_storage(|s| pollster::block_on(actions::tag_resource::execute(s, request)))
348 }
349
350 pub fn untag_resource(
352 &self,
353 request: actions::untag_resource::UntagResourceRequest,
354 ) -> Result<actions::untag_resource::UntagResourceResponse> {
355 self.with_storage(|s| pollster::block_on(actions::untag_resource::execute(s, request)))
356 }
357
358 pub fn list_tags_of_resource(
360 &self,
361 request: actions::list_tags_of_resource::ListTagsOfResourceRequest,
362 ) -> Result<actions::list_tags_of_resource::ListTagsOfResourceResponse> {
363 self.with_storage(|s| {
364 pollster::block_on(actions::list_tags_of_resource::execute(s, request))
365 })
366 }
367
368 pub fn put_item(
374 &self,
375 request: actions::put_item::PutItemRequest,
376 ) -> Result<actions::put_item::PutItemResponse> {
377 self.with_storage(|s| pollster::block_on(actions::put_item::execute(s, request)))
378 }
379
380 pub fn get_item(
382 &self,
383 request: actions::get_item::GetItemRequest,
384 ) -> Result<actions::get_item::GetItemResponse> {
385 self.with_storage(|s| pollster::block_on(actions::get_item::execute(s, request)))
386 }
387
388 pub fn delete_item(
390 &self,
391 request: actions::delete_item::DeleteItemRequest,
392 ) -> Result<actions::delete_item::DeleteItemResponse> {
393 self.with_storage(|s| pollster::block_on(actions::delete_item::execute(s, request)))
394 }
395
396 pub fn update_item(
398 &self,
399 request: actions::update_item::UpdateItemRequest,
400 ) -> Result<actions::update_item::UpdateItemResponse> {
401 self.with_storage(|s| pollster::block_on(actions::update_item::execute(s, request)))
402 }
403
404 pub fn batch_get_item(
410 &self,
411 request: actions::batch_get_item::BatchGetItemRequest,
412 ) -> Result<actions::batch_get_item::BatchGetItemResponse> {
413 self.with_storage(|s| pollster::block_on(actions::batch_get_item::execute(s, request)))
414 }
415
416 pub fn batch_write_item(
418 &self,
419 request: actions::batch_write_item::BatchWriteItemRequest,
420 ) -> Result<actions::batch_write_item::BatchWriteItemResponse> {
421 self.with_storage(|s| pollster::block_on(actions::batch_write_item::execute(s, request)))
422 }
423
424 pub fn import_items(
439 &self,
440 table_name: &str,
441 items: Vec<Item>,
442 options: ImportOptions,
443 ) -> Result<ImportResult> {
444 self.with_storage(|s| {
445 pollster::block_on(actions::import_items::execute(
446 s, table_name, items, &options,
447 ))
448 })
449 }
450
451 #[cfg(feature = "import")]
457 pub(crate) fn import_items_fresh(
458 &self,
459 table_name: &str,
460 items: Vec<Item>,
461 options: ImportOptions,
462 ) -> Result<ImportResult> {
463 self.with_storage(|s| {
464 pollster::block_on(actions::import_items::execute_skip_gsi_deletes(
465 s, table_name, items, &options,
466 ))
467 })
468 }
469
470 pub fn enable_bulk_loading(&self) -> Result<()> {
479 self.with_storage(|s| s.enable_bulk_loading())
480 }
481
482 pub fn disable_bulk_loading(&self) -> Result<()> {
484 self.with_storage(|s| s.disable_bulk_loading())
485 }
486
487 pub fn query(
493 &self,
494 request: actions::query::QueryRequest,
495 ) -> Result<actions::query::QueryResponse> {
496 self.with_storage(|s| pollster::block_on(actions::query::execute(s, request)))
497 }
498
499 pub fn scan(&self, request: actions::scan::ScanRequest) -> Result<actions::scan::ScanResponse> {
501 self.with_storage(|s| pollster::block_on(actions::scan::execute(s, request)))
502 }
503
504 pub fn transact_write_items(
510 &self,
511 request: actions::transact_write_items::TransactWriteItemsRequest,
512 ) -> Result<actions::transact_write_items::TransactWriteItemsResponse> {
513 const TOKEN_EXPIRY_SECS: u64 = 600; const MAX_TOKEN_LEN: usize = 36;
515
516 if let Some(ref token) = request.client_request_token {
518 if token.len() > MAX_TOKEN_LEN {
519 return Err(DynoxideError::ValidationException(format!(
520 "1 validation error detected: Value '{}' at 'clientRequestToken' failed to satisfy constraint: Member must have length less than or equal to {}",
521 token, MAX_TOKEN_LEN
522 )));
523 }
524 }
525
526 let request_hash = if request.client_request_token.is_some() {
530 use std::hash::{Hash, Hasher};
531 let normalised = serde_json::to_value(&request.transact_items)
532 .and_then(|v| serde_json::to_vec(&v))
533 .unwrap_or_default();
534 let mut hasher = std::collections::hash_map::DefaultHasher::new();
535 normalised.hash(&mut hasher);
536 hasher.finish()
537 } else {
538 0
539 };
540
541 if let Some(ref token) = request.client_request_token {
543 let mut cache = self
544 .idempotency_tokens
545 .lock()
546 .map_err(|e| DynoxideError::InternalServerError(format!("Lock poisoned: {e}")))?;
547 cache.retain(|_, (ts, _, _)| ts.elapsed().as_secs() < TOKEN_EXPIRY_SECS);
549 if let Some((_, cached_hash, resp)) = cache.get(token) {
550 if *cached_hash != request_hash {
551 return Err(DynoxideError::IdempotentParameterMismatchException(
552 "An error occurred (IdempotentParameterMismatchException)".to_string(),
553 ));
554 }
555 return Ok(resp.clone());
556 }
557 }
558
559 let resp = self.with_storage(|s| {
560 pollster::block_on(actions::transact_write_items::execute(s, request.clone()))
561 })?;
562
563 if let Some(ref token) = request.client_request_token {
565 if let Ok(mut cache) = self.idempotency_tokens.lock() {
566 cache.insert(token.clone(), (Instant::now(), request_hash, resp.clone()));
567 }
568 }
569
570 Ok(resp)
571 }
572
573 pub fn transact_get_items(
575 &self,
576 request: actions::transact_get_items::TransactGetItemsRequest,
577 ) -> Result<actions::transact_get_items::TransactGetItemsResponse> {
578 self.with_storage(|s| pollster::block_on(actions::transact_get_items::execute(s, request)))
579 }
580
581 pub fn list_streams(
587 &self,
588 request: actions::list_streams::ListStreamsRequest,
589 ) -> Result<actions::list_streams::ListStreamsResponse> {
590 self.with_storage(|s| pollster::block_on(actions::list_streams::execute(s, request)))
591 }
592
593 pub fn describe_stream(
595 &self,
596 request: actions::describe_stream::DescribeStreamRequest,
597 ) -> Result<actions::describe_stream::DescribeStreamResponse> {
598 self.with_storage(|s| pollster::block_on(actions::describe_stream::execute(s, request)))
599 }
600
601 pub fn get_shard_iterator(
603 &self,
604 request: actions::get_shard_iterator::GetShardIteratorRequest,
605 ) -> Result<actions::get_shard_iterator::GetShardIteratorResponse> {
606 self.with_storage(|s| pollster::block_on(actions::get_shard_iterator::execute(s, request)))
607 }
608
609 pub fn get_records(
611 &self,
612 request: actions::get_records::GetRecordsRequest,
613 ) -> Result<actions::get_records::GetRecordsResponse> {
614 self.with_storage(|s| pollster::block_on(actions::get_records::execute(s, request)))
615 }
616
617 pub fn update_time_to_live(
623 &self,
624 request: actions::update_time_to_live::UpdateTimeToLiveRequest,
625 ) -> Result<actions::update_time_to_live::UpdateTimeToLiveResponse> {
626 self.with_storage(|s| pollster::block_on(actions::update_time_to_live::execute(s, request)))
627 }
628
629 pub fn describe_time_to_live(
631 &self,
632 request: actions::describe_time_to_live::DescribeTimeToLiveRequest,
633 ) -> Result<actions::describe_time_to_live::DescribeTimeToLiveResponse> {
634 self.with_storage(|s| {
635 pollster::block_on(actions::describe_time_to_live::execute(s, request))
636 })
637 }
638
639 pub fn sweep_ttl(&self) -> Result<usize> {
642 self.with_storage(|s| pollster::block_on(ttl::sweep_expired_items(s)))
643 }
644
645 pub fn execute_statement(
651 &self,
652 request: actions::execute_statement::ExecuteStatementRequest,
653 ) -> Result<actions::execute_statement::ExecuteStatementResponse> {
654 self.with_storage(|s| pollster::block_on(actions::execute_statement::execute(s, request)))
655 }
656
657 pub fn execute_transaction(
659 &self,
660 request: actions::execute_transaction::ExecuteTransactionRequest,
661 ) -> Result<actions::execute_transaction::ExecuteTransactionResponse> {
662 self.with_storage(|s| pollster::block_on(actions::execute_transaction::execute(s, request)))
663 }
664
665 pub fn batch_execute_statement(
667 &self,
668 request: actions::batch_execute_statement::BatchExecuteStatementRequest,
669 ) -> Result<actions::batch_execute_statement::BatchExecuteStatementResponse> {
670 self.with_storage(|s| {
671 pollster::block_on(actions::batch_execute_statement::execute(s, request))
672 })
673 }
674
675 pub fn touch_cached_at(
684 &self,
685 table_name: &str,
686 pk: &str,
687 sk: &str,
688 timestamp: f64,
689 ) -> Result<()> {
690 self.with_storage(|s| s.touch_cached_at(table_name, pk, sk, timestamp))
691 }
692
693 pub fn get_lru_items(
698 &self,
699 table_name: &str,
700 limit: usize,
701 ) -> Result<Vec<(String, String, i64)>> {
702 self.with_storage(|s| s.get_lru_items(table_name, limit))
703 }
704
705 pub fn db_path(&self) -> Result<Option<String>> {
711 self.with_storage(|s| Ok(s.db_path()))
712 }
713
714 pub fn db_size_bytes(&self) -> Result<u64> {
716 self.with_storage(|s| s.db_size_bytes())
717 }
718
719 pub fn table_count(&self) -> Result<usize> {
721 self.with_storage(|s| s.table_count())
722 }
723
724 pub fn table_stats(&self) -> Result<Vec<TableStats>> {
726 self.with_storage(|s| s.table_stats())
727 }
728
729 pub fn get_table_metadata(&self, table_name: &str) -> Result<Option<storage::TableMetadata>> {
731 self.with_storage(|s| s.get_table_metadata(table_name))
732 }
733
734 pub fn database_info(&self) -> Result<DatabaseInfo> {
739 self.with_storage(|s| s.database_info())
740 }
741
742 pub fn vacuum(&self) -> Result<()> {
748 self.with_storage(|s| s.vacuum())
749 }
750
751 pub fn vacuum_into(&self, path: &str) -> Result<()> {
756 self.with_storage(|s| s.vacuum_into(path))
757 }
758
759 pub fn restore_from(&self, path: &str) -> Result<()> {
765 self.with_storage_mut(|s| s.restore_from(path))
766 }
767
768 #[cfg(feature = "mcp-server")]
773 pub(crate) fn backup_to_memory(&self) -> Result<rusqlite::Connection> {
774 self.with_storage(|s| s.backup_to_memory())
775 }
776
777 #[cfg(feature = "mcp-server")]
781 pub(crate) fn restore_from_connection(&self, source: &rusqlite::Connection) -> Result<()> {
782 self.with_storage_mut(|s| s.restore_from_connection(source))
783 }
784}
785
786#[cfg(feature = "wasm-sqlite")]
800impl Database<WasmBridgeBackend> {
801 pub async fn open(name: &str) -> Result<Self> {
804 Self::open_with(name, false).await
805 }
806
807 pub async fn open_with(name: &str, ephemeral: bool) -> Result<Self> {
810 let backend = WasmBridgeBackend::open_with(name, ephemeral)
811 .await
812 .map_err(DynoxideError::from)?;
813 Ok(Self {
814 inner: Arc::new(BackendMutex::new(backend)),
815 idempotency_tokens: Arc::new(Mutex::new(HashMap::new())),
816 })
817 }
818
819 pub async fn persistence_mode(&self) -> String {
821 self.backend().await.persistence_mode().to_string()
822 }
823
824 pub async fn close(&self) -> Result<()> {
828 self.backend()
829 .await
830 .close()
831 .await
832 .map_err(DynoxideError::from)
833 }
834
835 pub(crate) async fn backend(&self) -> async_lock::MutexGuard<'_, WasmBridgeBackend> {
844 self.inner.lock().await
845 }
846
847 pub async fn create_table(
849 &self,
850 request: actions::create_table::CreateTableRequest,
851 ) -> Result<actions::create_table::CreateTableResponse> {
852 let backend = self.backend().await;
853 actions::create_table::execute(&*backend, request).await
854 }
855
856 pub async fn delete_table(
858 &self,
859 request: actions::delete_table::DeleteTableRequest,
860 ) -> Result<actions::delete_table::DeleteTableResponse> {
861 let backend = self.backend().await;
862 actions::delete_table::execute(&*backend, request).await
863 }
864
865 pub async fn describe_table(
867 &self,
868 request: actions::describe_table::DescribeTableRequest,
869 ) -> Result<actions::describe_table::DescribeTableResponse> {
870 let backend = self.backend().await;
871 actions::describe_table::execute(&*backend, request).await
872 }
873
874 pub async fn list_tables(
876 &self,
877 request: actions::list_tables::ListTablesRequest,
878 ) -> Result<actions::list_tables::ListTablesResponse> {
879 let backend = self.backend().await;
880 actions::list_tables::execute(&*backend, request).await
881 }
882
883 pub async fn put_item(
885 &self,
886 request: actions::put_item::PutItemRequest,
887 ) -> Result<actions::put_item::PutItemResponse> {
888 let backend = self.backend().await;
889 actions::put_item::execute(&*backend, request).await
890 }
891
892 pub async fn get_item(
894 &self,
895 request: actions::get_item::GetItemRequest,
896 ) -> Result<actions::get_item::GetItemResponse> {
897 let backend = self.backend().await;
898 actions::get_item::execute(&*backend, request).await
899 }
900
901 pub async fn delete_item(
903 &self,
904 request: actions::delete_item::DeleteItemRequest,
905 ) -> Result<actions::delete_item::DeleteItemResponse> {
906 let backend = self.backend().await;
907 actions::delete_item::execute(&*backend, request).await
908 }
909
910 pub async fn query(
912 &self,
913 request: actions::query::QueryRequest,
914 ) -> Result<actions::query::QueryResponse> {
915 let backend = self.backend().await;
916 actions::query::execute(&*backend, request).await
917 }
918
919 pub async fn scan(
921 &self,
922 request: actions::scan::ScanRequest,
923 ) -> Result<actions::scan::ScanResponse> {
924 let backend = self.backend().await;
925 actions::scan::execute(&*backend, request).await
926 }
927}
928
929#[cfg(all(test, any(feature = "native-sqlite", feature = "_has-encryption")))]
930mod tests {
931 use super::*;
932
933 #[test]
934 fn test_database_memory() {
935 let db = Database::memory().unwrap();
936 let _db2 = db.clone();
938 }
939
940 #[test]
941 fn test_database_with_storage() {
942 let db = Database::memory().unwrap();
943 let tables = db.with_storage(|s| s.list_table_names()).unwrap();
944 assert!(tables.is_empty());
945 }
946
947 #[test]
948 fn test_database_thread_safe() {
949 let db = Database::memory().unwrap();
950 let db2 = db.clone();
951
952 let handle =
953 std::thread::spawn(move || db2.with_storage(|s| s.list_table_names()).unwrap());
954
955 let tables = handle.join().unwrap();
956 assert!(tables.is_empty());
957 }
958
959 #[test]
960 fn test_native_database_alias_round_trips() {
961 let db: NativeDatabase = Database::memory().unwrap();
965
966 db.create_table(actions::create_table::CreateTableRequest {
967 table_name: "tbl".to_string(),
968 key_schema: vec![types::KeySchemaElement {
969 attribute_name: "pk".to_string(),
970 key_type: types::KeyType::HASH,
971 }],
972 attribute_definitions: vec![types::AttributeDefinition {
973 attribute_name: "pk".to_string(),
974 attribute_type: types::ScalarAttributeType::S,
975 }],
976 ..Default::default()
977 })
978 .unwrap();
979
980 let mut item = HashMap::new();
981 item.insert("pk".to_string(), AttributeValue::S("a".to_string()));
982 db.put_item(actions::put_item::PutItemRequest {
983 table_name: "tbl".to_string(),
984 item,
985 ..Default::default()
986 })
987 .unwrap();
988
989 let mut key = HashMap::new();
990 key.insert("pk".to_string(), AttributeValue::S("a".to_string()));
991 let got = db
992 .get_item(actions::get_item::GetItemRequest {
993 table_name: "tbl".to_string(),
994 key,
995 ..Default::default()
996 })
997 .unwrap();
998 assert_eq!(
999 got.item.unwrap().get("pk"),
1000 Some(&AttributeValue::S("a".to_string()))
1001 );
1002 }
1003}