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(feature = "native-sqlite", feature = "_has-encryption")))]
33compile_error!(
34 "Either `native-sqlite`, `encryption`, or `encryption-cc` feature must be enabled. \
35 Default features include `native-sqlite`. If you used \
36 `default-features = false`, add one of these features."
37);
38
39pub mod actions;
40pub mod errors;
41pub mod expressions;
42#[cfg(feature = "import")]
43pub mod import;
44#[doc(hidden)]
45pub mod macros;
46#[cfg(feature = "mcp-server")]
47pub mod mcp;
48pub mod partiql;
49pub mod schema;
50#[cfg(feature = "http-server")]
51pub mod server;
52#[cfg(feature = "mcp-server")]
53pub(crate) mod snapshots;
54pub mod storage;
55pub mod streams;
56pub mod ttl;
57pub mod types;
58pub mod validation;
59
60#[doc(hidden)]
61pub use macros::ItemInsert;
62
63use std::collections::HashMap;
64use std::sync::{Arc, Mutex};
65use std::time::Instant;
66
67pub use errors::{DynoxideError, Result};
68pub use storage::{DatabaseInfo, TableInfoEntry, TableMetadata, TableStats};
69pub use types::{AttributeValue, ConversionError, Item};
70
71#[derive(Debug, Clone, Default)]
73pub struct ImportOptions {
74 pub record_streams: bool,
76 pub set_cached_at: bool,
78}
79
80#[derive(Debug, Clone)]
82pub struct ImportResult {
83 pub items_imported: usize,
85 pub bytes_imported: usize,
87}
88
89type TokenCache = HashMap<
91 String,
92 (
93 Instant,
94 u64,
95 actions::transact_write_items::TransactWriteItemsResponse,
96 ),
97>;
98
99#[derive(Clone)]
104pub struct Database {
105 inner: Arc<Mutex<storage::Storage>>,
106 idempotency_tokens: Arc<Mutex<TokenCache>>,
107}
108
109impl Database {
110 pub fn new(path: &str) -> Result<Self> {
112 let storage = storage::Storage::new(path)?;
113 Ok(Self {
114 inner: Arc::new(Mutex::new(storage)),
115 idempotency_tokens: Arc::new(Mutex::new(HashMap::new())),
116 })
117 }
118
119 #[cfg(feature = "_has-encryption")]
140 pub fn new_encrypted(path: &str, key: &str) -> Result<Self> {
141 if key.len() != 64 || !key.bytes().all(|b| b.is_ascii_hexdigit()) {
142 return Err(DynoxideError::ValidationException(
143 "Encryption key must be a 64-character hex string (32 bytes)".to_string(),
144 ));
145 }
146
147 let storage = storage::Storage::new_encrypted(path, key)?;
148 Ok(Self {
149 inner: Arc::new(Mutex::new(storage)),
150 idempotency_tokens: Arc::new(Mutex::new(HashMap::new())),
151 })
152 }
153
154 pub fn memory() -> Result<Self> {
156 let storage = storage::Storage::memory()?;
157 Ok(Self {
158 inner: Arc::new(Mutex::new(storage)),
159 idempotency_tokens: Arc::new(Mutex::new(HashMap::new())),
160 })
161 }
162
163 pub(crate) fn with_storage<F, T>(&self, f: F) -> Result<T>
165 where
166 F: FnOnce(&storage::Storage) -> Result<T>,
167 {
168 let guard = self
169 .inner
170 .lock()
171 .map_err(|e| DynoxideError::InternalServerError(format!("Lock poisoned: {e}")))?;
172 f(&guard)
173 }
174
175 pub(crate) fn with_storage_mut<F, T>(&self, f: F) -> Result<T>
177 where
178 F: FnOnce(&mut storage::Storage) -> Result<T>,
179 {
180 let mut guard = self
181 .inner
182 .lock()
183 .map_err(|e| DynoxideError::InternalServerError(format!("Lock poisoned: {e}")))?;
184 f(&mut guard)
185 }
186
187 pub fn create_table(
193 &self,
194 request: actions::create_table::CreateTableRequest,
195 ) -> Result<actions::create_table::CreateTableResponse> {
196 self.with_storage(|s| actions::create_table::execute(s, request))
197 }
198
199 pub fn delete_table(
201 &self,
202 request: actions::delete_table::DeleteTableRequest,
203 ) -> Result<actions::delete_table::DeleteTableResponse> {
204 self.with_storage(|s| actions::delete_table::execute(s, request))
205 }
206
207 pub fn describe_table(
209 &self,
210 request: actions::describe_table::DescribeTableRequest,
211 ) -> Result<actions::describe_table::DescribeTableResponse> {
212 self.with_storage(|s| actions::describe_table::execute(s, request))
213 }
214
215 pub fn update_table(
217 &self,
218 request: actions::update_table::UpdateTableRequest,
219 ) -> Result<actions::update_table::UpdateTableResponse> {
220 self.with_storage(|s| actions::update_table::execute(s, request))
221 }
222
223 pub fn list_tables(
225 &self,
226 request: actions::list_tables::ListTablesRequest,
227 ) -> Result<actions::list_tables::ListTablesResponse> {
228 self.with_storage(|s| actions::list_tables::execute(s, request))
229 }
230
231 pub fn tag_resource(
237 &self,
238 request: actions::tag_resource::TagResourceRequest,
239 ) -> Result<actions::tag_resource::TagResourceResponse> {
240 self.with_storage(|s| actions::tag_resource::execute(s, request))
241 }
242
243 pub fn untag_resource(
245 &self,
246 request: actions::untag_resource::UntagResourceRequest,
247 ) -> Result<actions::untag_resource::UntagResourceResponse> {
248 self.with_storage(|s| actions::untag_resource::execute(s, request))
249 }
250
251 pub fn list_tags_of_resource(
253 &self,
254 request: actions::list_tags_of_resource::ListTagsOfResourceRequest,
255 ) -> Result<actions::list_tags_of_resource::ListTagsOfResourceResponse> {
256 self.with_storage(|s| actions::list_tags_of_resource::execute(s, request))
257 }
258
259 pub fn put_item(
265 &self,
266 request: actions::put_item::PutItemRequest,
267 ) -> Result<actions::put_item::PutItemResponse> {
268 self.with_storage(|s| actions::put_item::execute(s, request))
269 }
270
271 pub fn get_item(
273 &self,
274 request: actions::get_item::GetItemRequest,
275 ) -> Result<actions::get_item::GetItemResponse> {
276 self.with_storage(|s| actions::get_item::execute(s, request))
277 }
278
279 pub fn delete_item(
281 &self,
282 request: actions::delete_item::DeleteItemRequest,
283 ) -> Result<actions::delete_item::DeleteItemResponse> {
284 self.with_storage(|s| actions::delete_item::execute(s, request))
285 }
286
287 pub fn update_item(
289 &self,
290 request: actions::update_item::UpdateItemRequest,
291 ) -> Result<actions::update_item::UpdateItemResponse> {
292 self.with_storage(|s| actions::update_item::execute(s, request))
293 }
294
295 pub fn batch_get_item(
301 &self,
302 request: actions::batch_get_item::BatchGetItemRequest,
303 ) -> Result<actions::batch_get_item::BatchGetItemResponse> {
304 self.with_storage(|s| actions::batch_get_item::execute(s, request))
305 }
306
307 pub fn batch_write_item(
309 &self,
310 request: actions::batch_write_item::BatchWriteItemRequest,
311 ) -> Result<actions::batch_write_item::BatchWriteItemResponse> {
312 self.with_storage(|s| actions::batch_write_item::execute(s, request))
313 }
314
315 pub fn import_items(
330 &self,
331 table_name: &str,
332 items: Vec<Item>,
333 options: ImportOptions,
334 ) -> Result<ImportResult> {
335 self.with_storage(|s| actions::import_items::execute(s, table_name, items, &options))
336 }
337
338 #[cfg(feature = "import")]
344 pub(crate) fn import_items_fresh(
345 &self,
346 table_name: &str,
347 items: Vec<Item>,
348 options: ImportOptions,
349 ) -> Result<ImportResult> {
350 self.with_storage(|s| {
351 actions::import_items::execute_skip_gsi_deletes(s, table_name, items, &options)
352 })
353 }
354
355 pub fn enable_bulk_loading(&self) -> Result<()> {
364 self.with_storage(|s| s.enable_bulk_loading())
365 }
366
367 pub fn disable_bulk_loading(&self) -> Result<()> {
369 self.with_storage(|s| s.disable_bulk_loading())
370 }
371
372 pub fn query(
378 &self,
379 request: actions::query::QueryRequest,
380 ) -> Result<actions::query::QueryResponse> {
381 self.with_storage(|s| actions::query::execute(s, request))
382 }
383
384 pub fn scan(&self, request: actions::scan::ScanRequest) -> Result<actions::scan::ScanResponse> {
386 self.with_storage(|s| actions::scan::execute(s, request))
387 }
388
389 pub fn transact_write_items(
395 &self,
396 request: actions::transact_write_items::TransactWriteItemsRequest,
397 ) -> Result<actions::transact_write_items::TransactWriteItemsResponse> {
398 const TOKEN_EXPIRY_SECS: u64 = 600; const MAX_TOKEN_LEN: usize = 36;
400
401 if let Some(ref token) = request.client_request_token {
403 if token.len() > MAX_TOKEN_LEN {
404 return Err(DynoxideError::ValidationException(format!(
405 "1 validation error detected: Value '{}' at 'clientRequestToken' failed to satisfy constraint: Member must have length less than or equal to {}",
406 token, MAX_TOKEN_LEN
407 )));
408 }
409 }
410
411 let request_hash = if request.client_request_token.is_some() {
415 use std::hash::{Hash, Hasher};
416 let normalised = serde_json::to_value(&request.transact_items)
417 .and_then(|v| serde_json::to_vec(&v))
418 .unwrap_or_default();
419 let mut hasher = std::collections::hash_map::DefaultHasher::new();
420 normalised.hash(&mut hasher);
421 hasher.finish()
422 } else {
423 0
424 };
425
426 if let Some(ref token) = request.client_request_token {
428 let mut cache = self
429 .idempotency_tokens
430 .lock()
431 .map_err(|e| DynoxideError::InternalServerError(format!("Lock poisoned: {e}")))?;
432 cache.retain(|_, (ts, _, _)| ts.elapsed().as_secs() < TOKEN_EXPIRY_SECS);
434 if let Some((_, cached_hash, resp)) = cache.get(token) {
435 if *cached_hash != request_hash {
436 return Err(DynoxideError::IdempotentParameterMismatchException(
437 "An error occurred (IdempotentParameterMismatchException)".to_string(),
438 ));
439 }
440 return Ok(resp.clone());
441 }
442 }
443
444 let resp =
445 self.with_storage(|s| actions::transact_write_items::execute(s, request.clone()))?;
446
447 if let Some(ref token) = request.client_request_token {
449 if let Ok(mut cache) = self.idempotency_tokens.lock() {
450 cache.insert(token.clone(), (Instant::now(), request_hash, resp.clone()));
451 }
452 }
453
454 Ok(resp)
455 }
456
457 pub fn transact_get_items(
459 &self,
460 request: actions::transact_get_items::TransactGetItemsRequest,
461 ) -> Result<actions::transact_get_items::TransactGetItemsResponse> {
462 self.with_storage(|s| actions::transact_get_items::execute(s, request))
463 }
464
465 pub fn list_streams(
471 &self,
472 request: actions::list_streams::ListStreamsRequest,
473 ) -> Result<actions::list_streams::ListStreamsResponse> {
474 self.with_storage(|s| actions::list_streams::execute(s, request))
475 }
476
477 pub fn describe_stream(
479 &self,
480 request: actions::describe_stream::DescribeStreamRequest,
481 ) -> Result<actions::describe_stream::DescribeStreamResponse> {
482 self.with_storage(|s| actions::describe_stream::execute(s, request))
483 }
484
485 pub fn get_shard_iterator(
487 &self,
488 request: actions::get_shard_iterator::GetShardIteratorRequest,
489 ) -> Result<actions::get_shard_iterator::GetShardIteratorResponse> {
490 self.with_storage(|s| actions::get_shard_iterator::execute(s, request))
491 }
492
493 pub fn get_records(
495 &self,
496 request: actions::get_records::GetRecordsRequest,
497 ) -> Result<actions::get_records::GetRecordsResponse> {
498 self.with_storage(|s| actions::get_records::execute(s, request))
499 }
500
501 pub fn update_time_to_live(
507 &self,
508 request: actions::update_time_to_live::UpdateTimeToLiveRequest,
509 ) -> Result<actions::update_time_to_live::UpdateTimeToLiveResponse> {
510 self.with_storage(|s| actions::update_time_to_live::execute(s, request))
511 }
512
513 pub fn describe_time_to_live(
515 &self,
516 request: actions::describe_time_to_live::DescribeTimeToLiveRequest,
517 ) -> Result<actions::describe_time_to_live::DescribeTimeToLiveResponse> {
518 self.with_storage(|s| actions::describe_time_to_live::execute(s, request))
519 }
520
521 pub fn sweep_ttl(&self) -> Result<usize> {
524 self.with_storage(ttl::sweep_expired_items)
525 }
526
527 pub fn execute_statement(
533 &self,
534 request: actions::execute_statement::ExecuteStatementRequest,
535 ) -> Result<actions::execute_statement::ExecuteStatementResponse> {
536 self.with_storage(|s| actions::execute_statement::execute(s, request))
537 }
538
539 pub fn execute_transaction(
541 &self,
542 request: actions::execute_transaction::ExecuteTransactionRequest,
543 ) -> Result<actions::execute_transaction::ExecuteTransactionResponse> {
544 self.with_storage(|s| actions::execute_transaction::execute(s, request))
545 }
546
547 pub fn batch_execute_statement(
549 &self,
550 request: actions::batch_execute_statement::BatchExecuteStatementRequest,
551 ) -> Result<actions::batch_execute_statement::BatchExecuteStatementResponse> {
552 self.with_storage(|s| actions::batch_execute_statement::execute(s, request))
553 }
554
555 pub fn touch_cached_at(
564 &self,
565 table_name: &str,
566 pk: &str,
567 sk: &str,
568 timestamp: f64,
569 ) -> Result<()> {
570 self.with_storage(|s| s.touch_cached_at(table_name, pk, sk, timestamp))
571 }
572
573 pub fn get_lru_items(
578 &self,
579 table_name: &str,
580 limit: usize,
581 ) -> Result<Vec<(String, String, i64)>> {
582 self.with_storage(|s| s.get_lru_items(table_name, limit))
583 }
584
585 pub fn db_path(&self) -> Result<Option<String>> {
591 self.with_storage(|s| Ok(s.db_path()))
592 }
593
594 pub fn db_size_bytes(&self) -> Result<u64> {
596 self.with_storage(|s| s.db_size_bytes())
597 }
598
599 pub fn table_count(&self) -> Result<usize> {
601 self.with_storage(|s| s.table_count())
602 }
603
604 pub fn table_stats(&self) -> Result<Vec<TableStats>> {
606 self.with_storage(|s| s.table_stats())
607 }
608
609 pub fn get_table_metadata(&self, table_name: &str) -> Result<Option<storage::TableMetadata>> {
611 self.with_storage(|s| s.get_table_metadata(table_name))
612 }
613
614 pub fn database_info(&self) -> Result<DatabaseInfo> {
619 self.with_storage(|s| s.database_info())
620 }
621
622 pub fn vacuum(&self) -> Result<()> {
628 self.with_storage(|s| s.vacuum())
629 }
630
631 pub fn vacuum_into(&self, path: &str) -> Result<()> {
636 self.with_storage(|s| s.vacuum_into(path))
637 }
638
639 pub fn restore_from(&self, path: &str) -> Result<()> {
645 self.with_storage_mut(|s| s.restore_from(path))
646 }
647
648 #[cfg(feature = "mcp-server")]
653 pub(crate) fn backup_to_memory(&self) -> Result<rusqlite::Connection> {
654 self.with_storage(|s| s.backup_to_memory())
655 }
656
657 #[cfg(feature = "mcp-server")]
661 pub(crate) fn restore_from_connection(&self, source: &rusqlite::Connection) -> Result<()> {
662 self.with_storage_mut(|s| s.restore_from_connection(source))
663 }
664}
665
666#[cfg(test)]
667mod tests {
668 use super::*;
669
670 #[test]
671 fn test_database_memory() {
672 let db = Database::memory().unwrap();
673 let _db2 = db.clone();
675 }
676
677 #[test]
678 fn test_database_with_storage() {
679 let db = Database::memory().unwrap();
680 let tables = db.with_storage(|s| s.list_table_names()).unwrap();
681 assert!(tables.is_empty());
682 }
683
684 #[test]
685 fn test_database_thread_safe() {
686 let db = Database::memory().unwrap();
687 let db2 = db.clone();
688
689 let handle =
690 std::thread::spawn(move || db2.with_storage(|s| s.list_table_names()).unwrap());
691
692 let tables = handle.join().unwrap();
693 assert!(tables.is_empty());
694 }
695}