1#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2use crate::errors::{DynoxideError, Result};
3#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
4use crate::storage_backend::clock::{Clock, SystemClock};
5#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
6use crate::storage_backend::sql_builders::{self, escape_table_name};
7use crate::types::AttributeValue;
8#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
9use rusqlite::{Connection, params};
10#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
11use std::{cell::RefCell, collections::HashMap, sync::Arc};
12
13#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
16use crate::storage_backend::sql_builders::SCHEMA_VERSION;
17
18#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
26const OPEN_BUSY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
27
28#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
33const REQUEST_BUSY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
34
35#[cfg(test)]
36thread_local! {
37 static OPENED_WITH_BUSY_TIMEOUT_MS: std::cell::Cell<Option<i64>> =
40 const { std::cell::Cell::new(None) };
41}
42
43pub(crate) const HASH_BUCKETS: u32 = 4096;
46
47pub fn compute_hash_prefix(pk_value: &AttributeValue) -> String {
61 let key_bytes = match pk_value {
62 AttributeValue::S(s) => s.as_bytes().to_vec(),
63 AttributeValue::N(n) => num_to_buffer(n),
64 AttributeValue::B(b) => b.clone(),
65 _ => vec![], };
67
68 let digest = md5::compute([b"Outliers" as &[u8], &key_bytes].concat());
69 format!("{:032x}", digest)[..6].to_string()
70}
71
72pub fn hash_bucket(hash_prefix: &str) -> u32 {
74 let prefix_3 = &hash_prefix[..3.min(hash_prefix.len())];
75 u32::from_str_radix(prefix_3, 16).unwrap_or(0)
76}
77
78fn num_to_buffer(num_str: &str) -> Vec<u8> {
84 let trimmed = num_str.trim();
85 if trimmed.is_empty() {
86 return vec![0x80];
87 }
88
89 use bigdecimal::BigDecimal;
90 use std::str::FromStr;
91
92 let bd = match BigDecimal::from_str(trimmed) {
93 Ok(v) => v,
94 Err(_) => return vec![0x80],
95 };
96
97 if bd.sign() == bigdecimal::num_bigint::Sign::NoSign {
98 return vec![0x80];
99 }
100
101 let is_negative = bd.sign() == bigdecimal::num_bigint::Sign::Minus;
102 let bd_abs = if is_negative { -&bd } else { bd.clone() };
103
104 let (mantissa, exponent) = extract_mantissa_and_exponent(&bd_abs);
105 if mantissa.is_empty() {
106 return vec![0x80];
107 }
108
109 let append_zero: i64 = if exponent % 2 != 0 { 1 } else { 0 };
112 let byte_len_no_exp = ((mantissa.len() as i64 + append_zero + 1) / 2) as usize;
113
114 let mut byte_array: Vec<u8>;
115 if byte_len_no_exp < 20 && is_negative {
116 byte_array = vec![0u8; byte_len_no_exp + 2];
117 byte_array[byte_len_no_exp + 1] = 102;
118 } else {
119 byte_array = vec![0u8; byte_len_no_exp + 1];
120 }
121
122 let exp_sum = exponent + append_zero;
126 let exp_byte_val = floor_div(exp_sum, 2) - 64;
127 if is_negative {
128 byte_array[0] = (exp_byte_val ^ !0i64) as u8;
131 } else {
132 byte_array[0] = exp_byte_val as u8;
133 }
134
135 let mut mi: i64 = 0; let mlen = mantissa.len() as i64;
139 let mut appended_zero = false;
140
141 while mi < mlen {
142 let bai = ((mi + append_zero) / 2 + 1) as usize; if append_zero != 0 && mi == 0 && !appended_zero {
144 byte_array[bai] = 0;
145 appended_zero = true;
146 mi -= 1; } else if (mi + append_zero) % 2 == 0 {
148 byte_array[bai] = mantissa[mi as usize] * 10;
149 } else {
150 byte_array[bai] += mantissa[mi as usize];
151 }
152
153 if ((mi + append_zero) % 2 != 0) || (mi == mlen - 1) {
155 if is_negative {
156 byte_array[bai] = 101u8.wrapping_sub(byte_array[bai]);
157 } else {
158 byte_array[bai] = byte_array[bai].wrapping_add(1);
159 }
160 }
161
162 mi += 1; }
164
165 byte_array
166}
167
168fn floor_div(a: i64, b: i64) -> i64 {
170 let d = a / b;
171 let r = a % b;
172 if (r != 0) && ((r ^ b) < 0) { d - 1 } else { d }
173}
174
175fn extract_mantissa_and_exponent(bd: &bigdecimal::BigDecimal) -> (Vec<u8>, i64) {
182 let normalized = bd.normalized();
184
185 let (bigint, scale) = normalized.as_bigint_and_exponent();
188 let digits_str = bigint.to_string();
189 let digits_str = digits_str.trim_start_matches('-');
190
191 let digits: Vec<u8> = digits_str
192 .chars()
193 .map(|c| c.to_digit(10).unwrap() as u8)
194 .collect();
195
196 let exponent = digits.len() as i64 - scale;
199
200 (digits, exponent)
201}
202
203pub fn hash_in_segment(hash_prefix: &str, segment: u32, total_segments: u32) -> bool {
210 let bucket = hash_bucket(hash_prefix);
211 let start = ceiling_div(HASH_BUCKETS * segment, total_segments);
212 let end = ceiling_div(HASH_BUCKETS * (segment + 1), total_segments) - 1;
213 bucket >= start && bucket <= end
214}
215
216pub(crate) fn ceiling_div(a: u32, b: u32) -> u32 {
217 a.div_ceil(b)
218}
219
220#[derive(Debug, Default)]
222pub struct ScanParams<'a> {
223 pub limit: Option<usize>,
224 pub exclusive_start_pk: Option<&'a str>,
225 pub exclusive_start_sk: Option<&'a str>,
226 pub segment: Option<u32>,
227 pub total_segments: Option<u32>,
228 pub exclusive_start_base_pk: Option<&'a str>,
230 pub exclusive_start_base_sk: Option<&'a str>,
232}
233
234#[derive(Debug, Default)]
236pub struct CreateTableMetadata<'a> {
237 pub table_name: &'a str,
238 pub key_schema: &'a str,
239 pub attribute_definitions: &'a str,
240 pub gsi_definitions: Option<&'a str>,
241 pub lsi_definitions: Option<&'a str>,
242 pub provisioned_throughput: Option<&'a str>,
243 pub created_at: i64,
244 pub sse_specification: Option<&'a str>,
245 pub table_class: Option<&'a str>,
246 pub deletion_protection_enabled: bool,
247 pub billing_mode: Option<&'a str>,
248 pub on_demand_throughput: Option<&'a str>,
249 pub vector_index_definitions: Option<&'a str>,
250}
251
252#[derive(Debug, Default)]
254pub struct QueryParams<'a> {
255 pub sk_condition: Option<&'a str>,
256 pub sk_params: &'a [&'a str],
257 pub forward: bool,
258 pub limit: Option<usize>,
259 pub exclusive_start_sk: Option<&'a str>,
260 pub exclusive_start_base_pk: Option<&'a str>,
262 pub exclusive_start_base_sk: Option<&'a str>,
264}
265
266#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
274pub struct Storage {
275 conn: Connection,
276 metadata_cache: RefCell<HashMap<String, TableMetadata>>,
279 clock: Arc<dyn Clock>,
281 #[cfg(test)]
285 gsi_insert_fault: RefCell<Option<u32>>,
286 #[cfg(test)]
290 item_size_fault: RefCell<Option<u32>>,
291}
292
293#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
294impl Storage {
295 pub fn new(path: &str) -> Result<Self> {
297 let conn = Connection::open(path)?;
298 conn.busy_timeout(OPEN_BUSY_TIMEOUT)?;
299 let mut storage = Self {
300 conn,
301 metadata_cache: RefCell::new(HashMap::new()),
302 #[cfg(test)]
303 gsi_insert_fault: RefCell::new(None),
304 #[cfg(test)]
305 item_size_fault: RefCell::new(None),
306 clock: Arc::new(SystemClock),
307 };
308 storage.initialize().map_err(Self::maybe_encrypted_error)?;
309 storage.conn.busy_timeout(REQUEST_BUSY_TIMEOUT)?;
310 Ok(storage)
311 }
312
313 #[cfg(test)]
318 pub(crate) fn fail_gsi_insert_after(&self, successes: u32) {
319 *self.gsi_insert_fault.borrow_mut() = Some(successes);
320 }
321
322 #[cfg(test)]
324 pub(crate) fn check_gsi_insert_fault(&self) -> Result<()> {
325 Self::count_down(&self.gsi_insert_fault, "injected GSI insert failure")
326 }
327
328 #[cfg(test)]
329 fn count_down(fault: &RefCell<Option<u32>>, message: &str) -> Result<()> {
330 let mut fault = fault.borrow_mut();
331 match fault.as_mut() {
332 Some(0) => Err(crate::errors::DynoxideError::InternalServerError(
333 message.to_string(),
334 )),
335 Some(remaining) => {
336 *remaining -= 1;
337 Ok(())
338 }
339 None => Ok(()),
340 }
341 }
342
343 pub fn with_clock(mut self, clock: Arc<dyn Clock>) -> Self {
349 self.clock = clock;
350 self
351 }
352
353 pub(crate) fn clock(&self) -> &dyn Clock {
355 self.clock.as_ref()
356 }
357
358 fn maybe_encrypted_error(err: DynoxideError) -> DynoxideError {
361 if let DynoxideError::SqliteError(ref sqlite_err) = err
362 && let Some(rusqlite::ErrorCode::NotADatabase) = sqlite_err.sqlite_error_code()
363 {
364 return DynoxideError::InternalServerError(
365 "Database file is encrypted or not a valid SQLite database. \
366 If encrypted, enable the `encryption` or `encryption-cc` feature \
367 and use Database::new_encrypted() with the correct key."
368 .to_string(),
369 );
370 }
371 err
372 }
373
374 #[cfg(feature = "_has-encryption")]
380 pub fn new_encrypted(path: &str, key: &str) -> Result<Self> {
381 use zeroize::Zeroize;
382
383 let conn = Connection::open(path)?;
384 conn.busy_timeout(OPEN_BUSY_TIMEOUT)?;
385 let mut pragma_val = format!("x'{key}'");
390 conn.pragma_update(None, "key", &pragma_val)?;
391 pragma_val.zeroize();
392 conn.execute_batch("SELECT count(*) FROM sqlite_master;")?;
394 let mut storage = Self {
395 conn,
396 metadata_cache: RefCell::new(HashMap::new()),
397 #[cfg(test)]
398 gsi_insert_fault: RefCell::new(None),
399 #[cfg(test)]
400 item_size_fault: RefCell::new(None),
401 clock: Arc::new(SystemClock),
402 };
403 storage.initialize()?;
404 storage.conn.busy_timeout(REQUEST_BUSY_TIMEOUT)?;
405 Ok(storage)
406 }
407
408 pub fn memory() -> Result<Self> {
410 let conn = Connection::open_in_memory()?;
411 let mut storage = Self {
412 conn,
413 metadata_cache: RefCell::new(HashMap::new()),
414 #[cfg(test)]
415 gsi_insert_fault: RefCell::new(None),
416 #[cfg(test)]
417 item_size_fault: RefCell::new(None),
418 clock: Arc::new(SystemClock),
419 };
420 storage.initialize()?;
421 Ok(storage)
422 }
423
424 fn initialize(&mut self) -> Result<()> {
426 refuse_newer_schema(&self.conn)?;
429 self.conn.pragma_update(None, "journal_mode", "WAL")?;
431 Self::register_functions(&self.conn)?;
432 self.migrate_to_current()?;
433 #[cfg(test)]
434 OPENED_WITH_BUSY_TIMEOUT_MS.set(Some(self.conn.query_row(
435 "PRAGMA busy_timeout",
436 [],
437 |r| r.get(0),
438 )?));
439 Ok(())
440 }
441
442 fn register_functions(conn: &Connection) -> Result<()> {
444 conn.create_scalar_function(
447 "fnv1a_hash",
448 1,
449 rusqlite::functions::FunctionFlags::SQLITE_DETERMINISTIC
450 | rusqlite::functions::FunctionFlags::SQLITE_UTF8,
451 |ctx: &rusqlite::functions::Context| -> rusqlite::Result<i64> {
452 let pk_ref = ctx.get_raw(0);
453 let pk_bytes = match pk_ref {
454 rusqlite::types::ValueRef::Text(bytes) => bytes,
455 _ => {
456 return Err(rusqlite::Error::InvalidFunctionParameterType(
457 0,
458 rusqlite::types::Type::Text,
459 ));
460 }
461 };
462 let mut hash: u32 = 2166136261;
463 for &byte in pk_bytes {
464 hash ^= byte as u32;
465 hash = hash.wrapping_mul(16777619);
466 }
467 Ok(hash as i64)
468 },
469 )?;
470 Ok(())
471 }
472
473 fn migrate_to_current(&self) -> Result<()> {
477 let version = self.create_metadata_tables()?.unwrap_or(1);
485
486 if version < 2 {
487 self.migrate_v1_to_v2()?;
488 }
489 if version < 3 {
490 self.migrate_v2_to_v3()?;
491 }
492 if version < 4 {
493 self.migrate_v3_to_v4()?;
494 }
495 if version < 5 {
496 self.migrate_v4_to_v5()?;
497 }
498 if version < 6 {
499 self.migrate_v5_to_v6()?;
500 }
501 if version < 7 {
502 self.migrate_v6_to_v7()?;
503 }
504 if version < 8 {
505 self.migrate_v7_to_v8()?;
506 }
507 if version < 9 {
508 self.migrate_v8_to_v9()?;
509 }
510 pollster::block_on(crate::actions::schema_migrations::run_open_migrations(
514 self,
515 crate::storage_backend::schema::open_migrations(Some(version)),
516 ))
517 }
518
519 fn create_metadata_tables(&self) -> Result<Option<u32>> {
524 let transaction = rusqlite::Transaction::new_unchecked(
525 &self.conn,
526 rusqlite::TransactionBehavior::Immediate,
527 )?;
528 let new_database = !table_exists(&self.conn, "_tables")?;
529 self.conn.execute_batch(sql_builders::INIT_SCHEMA)?;
531 let _ = self
534 .conn
535 .execute_batch("ALTER TABLE _stream_records ADD COLUMN user_identity TEXT");
536 if new_database {
537 let (sql, params) = sql_builders::init_schema_version(SCHEMA_VERSION);
538 self.conn
539 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
540 }
541 let recorded = self.recorded_schema_version()?;
542 transaction.commit()?;
543 Ok(recorded)
544 }
545
546 fn migrate_v1_to_v2(&self) -> Result<()> {
548 let mut stmt = self.conn.prepare("SELECT table_name FROM _tables")?;
549 let table_names: Vec<String> = stmt
550 .query_map([], |row| row.get(0))?
551 .collect::<std::result::Result<Vec<_>, _>>()?;
552
553 for table_name in &table_names {
554 let escaped = format!("\"{}\"", table_name.replace('"', "\"\""));
555 let _ = self.conn.execute(
556 &format!("ALTER TABLE {escaped} ADD COLUMN cached_at REAL"),
557 [],
558 );
559 }
560
561 self.conn.execute(
562 "INSERT OR REPLACE INTO _config (key, value) VALUES ('schema_version', '2')",
563 [],
564 )?;
565
566 Ok(())
567 }
568
569 fn migrate_v2_to_v3(&self) -> Result<()> {
571 let _ = self
572 .conn
573 .execute("ALTER TABLE _tables ADD COLUMN tags TEXT", []);
574
575 self.conn.execute(
576 "INSERT OR REPLACE INTO _config (key, value) VALUES ('schema_version', '3')",
577 [],
578 )?;
579
580 Ok(())
581 }
582
583 fn migrate_v3_to_v4(&self) -> Result<()> {
585 let _ = self
586 .conn
587 .execute("ALTER TABLE _tables ADD COLUMN sse_specification TEXT", []);
588 let _ = self
589 .conn
590 .execute("ALTER TABLE _tables ADD COLUMN table_class TEXT", []);
591 let _ = self.conn.execute(
592 "ALTER TABLE _tables ADD COLUMN deletion_protection_enabled INTEGER DEFAULT 0",
593 [],
594 );
595
596 self.conn.execute(
597 "INSERT OR REPLACE INTO _config (key, value) VALUES ('schema_version', '4')",
598 [],
599 )?;
600
601 Ok(())
602 }
603
604 fn migrate_v4_to_v5(&self) -> Result<()> {
606 let mut stmt = self
607 .conn
608 .prepare("SELECT table_name, gsi_definitions, lsi_definitions FROM _tables")?;
609 let tables: Vec<(String, Option<String>, Option<String>)> = stmt
610 .query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))?
611 .collect::<std::result::Result<Vec<_>, _>>()?;
612
613 for (table_name, gsi_json, lsi_json) in &tables {
614 if let Some(json) = gsi_json
615 && let Ok(gsis) = serde_json::from_str::<Vec<serde_json::Value>>(json)
616 {
617 for gsi in &gsis {
618 if let Some(idx) = gsi.get("IndexName").and_then(|v| v.as_str()) {
619 let gsi_table = escape_table_name(&format!("{table_name}::gsi::{idx}"));
620 let idx_name =
621 escape_table_name(&format!("{table_name}::gsi::{idx}::base_key"));
622 let _ = self.conn.execute_batch(&format!(
623 "CREATE INDEX IF NOT EXISTS \"{idx_name}\" ON \"{gsi_table}\" (table_pk, table_sk)"
624 ));
625 }
626 }
627 }
628 if let Some(json) = lsi_json
629 && let Ok(lsis) = serde_json::from_str::<Vec<serde_json::Value>>(json)
630 {
631 for lsi in &lsis {
632 if let Some(idx) = lsi.get("IndexName").and_then(|v| v.as_str()) {
633 let lsi_table = escape_table_name(&format!("{table_name}::lsi::{idx}"));
634 let idx_name =
635 escape_table_name(&format!("{table_name}::lsi::{idx}::base_key"));
636 let _ = self.conn.execute_batch(&format!(
637 "CREATE INDEX IF NOT EXISTS \"{idx_name}\" ON \"{lsi_table}\" (base_pk, base_sk)"
638 ));
639 }
640 }
641 }
642 }
643
644 self.conn.execute(
645 "INSERT OR REPLACE INTO _config (key, value) VALUES ('schema_version', '5')",
646 [],
647 )?;
648
649 Ok(())
650 }
651
652 fn migrate_v5_to_v6(&self) -> Result<()> {
654 let mut stmt = self.conn.prepare("SELECT table_name FROM _tables")?;
655 let table_names: Vec<String> = stmt
656 .query_map([], |row| row.get(0))?
657 .collect::<std::result::Result<Vec<_>, _>>()?;
658
659 for table_name in &table_names {
660 let escaped = escape_table_name(table_name);
661 let _ = self.conn.execute(
662 &format!(
663 "ALTER TABLE \"{escaped}\" ADD COLUMN hash_prefix TEXT NOT NULL DEFAULT ''"
664 ),
665 [],
666 );
667 }
668
669 self.conn.execute(
670 "INSERT OR REPLACE INTO _config (key, value) VALUES ('schema_version', '6')",
671 [],
672 )?;
673
674 Ok(())
675 }
676
677 fn migrate_v6_to_v7(&self) -> Result<()> {
679 let _ = self.conn.execute(
680 "ALTER TABLE _tables ADD COLUMN on_demand_throughput TEXT",
681 [],
682 );
683
684 self.conn.execute(
685 "INSERT OR REPLACE INTO _config (key, value) VALUES ('schema_version', '7')",
686 [],
687 )?;
688
689 Ok(())
690 }
691
692 fn migrate_v7_to_v8(&self) -> Result<()> {
703 let _ = self
704 .conn
705 .execute("ALTER TABLE _tables ADD COLUMN table_id TEXT", []);
706
707 let names: Vec<String> = {
708 let mut stmt = self
709 .conn
710 .prepare("SELECT table_name FROM _tables WHERE table_id IS NULL")?;
711 let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
712 rows.collect::<rusqlite::Result<Vec<String>>>()?
713 };
714 for name in names {
715 let id = uuid::Uuid::new_v4().to_string();
716 self.conn.execute(
717 "UPDATE _tables SET table_id = ?1 WHERE table_name = ?2",
718 params![id, name],
719 )?;
720 }
721
722 self.conn.execute(
723 "INSERT OR REPLACE INTO _config (key, value) VALUES ('schema_version', '8')",
724 [],
725 )?;
726
727 Ok(())
728 }
729
730 fn migrate_v8_to_v9(&self) -> Result<()> {
738 if let Err(e) = self
739 .conn
740 .execute(sql_builders::ADD_VECTOR_INDEX_DEFINITIONS_COLUMN, [])
741 && !sql_builders::is_duplicate_column_error(&e.to_string())
742 {
743 return Err(e.into());
744 }
745
746 self.conn.execute(
747 "INSERT OR REPLACE INTO _config (key, value) VALUES ('schema_version', '9')",
748 [],
749 )?;
750
751 Ok(())
752 }
753
754 pub fn conn(&self) -> &Connection {
756 &self.conn
757 }
758
759 pub fn conn_mut(&mut self) -> &mut Connection {
761 &mut self.conn
762 }
763
764 pub fn insert_table_metadata(&self, m: &CreateTableMetadata) -> Result<()> {
770 let table_name = m.table_name;
771 let (sql, params) = sql_builders::insert_table_metadata(m);
772 self.conn
773 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
774 self.metadata_cache.borrow_mut().remove(table_name);
775 Ok(())
776 }
777
778 pub fn get_table_metadata(&self, table_name: &str) -> Result<Option<TableMetadata>> {
784 if let Some(cached) = self.metadata_cache.borrow().get(table_name) {
786 return Ok(Some(cached.clone()));
787 }
788
789 let (sql, params) = sql_builders::get_table_metadata(table_name);
790 let mut stmt = self.conn.prepare(&sql)?;
791
792 let result = stmt.query_row(rusqlite::params_from_iter(params.iter()), row_to_metadata);
793
794 match result {
795 Ok(meta) => {
796 self.metadata_cache
797 .borrow_mut()
798 .insert(table_name.to_string(), meta.clone());
799 Ok(Some(meta))
800 }
801 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
802 Err(e) => Err(DynoxideError::from(e)),
803 }
804 }
805
806 pub fn delete_table_metadata(&self, table_name: &str) -> Result<bool> {
808 let (sql, params) = sql_builders::delete_table_metadata(table_name);
809 let affected = self
810 .conn
811 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
812 self.metadata_cache.borrow_mut().remove(table_name);
813 Ok(affected > 0)
814 }
815
816 pub fn update_table_metadata(
818 &self,
819 table_name: &str,
820 attribute_definitions: &str,
821 gsi_definitions: Option<&str>,
822 ) -> Result<()> {
823 let (sql, params) =
824 sql_builders::update_table_metadata(table_name, attribute_definitions, gsi_definitions);
825 self.conn
826 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
827 self.metadata_cache.borrow_mut().remove(table_name);
828 Ok(())
829 }
830
831 pub fn update_vector_index_definitions(
833 &self,
834 table_name: &str,
835 vector_index_definitions: Option<&str>,
836 ) -> Result<()> {
837 let (sql, params) =
838 sql_builders::update_vector_index_definitions(table_name, vector_index_definitions);
839 self.conn
840 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
841 self.metadata_cache.borrow_mut().remove(table_name);
842 Ok(())
843 }
844
845 pub fn update_provisioned_throughput(
847 &self,
848 table_name: &str,
849 provisioned_throughput: &str,
850 ) -> Result<()> {
851 let (sql, params) =
852 sql_builders::update_provisioned_throughput(table_name, provisioned_throughput);
853 self.conn
854 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
855 self.metadata_cache.borrow_mut().remove(table_name);
856 Ok(())
857 }
858
859 pub fn clear_provisioned_throughput(&self, table_name: &str) -> Result<()> {
861 let (sql, params) = sql_builders::clear_provisioned_throughput(table_name);
862 self.conn
863 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
864 self.metadata_cache.borrow_mut().remove(table_name);
865 Ok(())
866 }
867
868 pub fn clear_on_demand_throughput(&self, table_name: &str) -> Result<()> {
870 let (sql, params) = sql_builders::clear_on_demand_throughput(table_name);
871 self.conn
872 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
873 self.metadata_cache.borrow_mut().remove(table_name);
874 Ok(())
875 }
876
877 pub fn update_billing_mode(&self, table_name: &str, billing_mode: &str) -> Result<()> {
879 let (sql, params) = sql_builders::update_billing_mode(table_name, billing_mode);
880 self.conn
881 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
882 self.metadata_cache.borrow_mut().remove(table_name);
883 Ok(())
884 }
885
886 pub fn update_table_class(&self, table_name: &str, table_class: &str) -> Result<()> {
888 let (sql, params) = sql_builders::update_table_class(table_name, table_class);
889 self.conn
890 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
891 self.metadata_cache.borrow_mut().remove(table_name);
892 Ok(())
893 }
894
895 pub fn update_on_demand_throughput(
897 &self,
898 table_name: &str,
899 on_demand_throughput: &str,
900 ) -> Result<()> {
901 let (sql, params) =
902 sql_builders::update_on_demand_throughput(table_name, on_demand_throughput);
903 self.conn
904 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
905 self.metadata_cache.borrow_mut().remove(table_name);
906 Ok(())
907 }
908
909 pub fn get_tags(&self, table_name: &str) -> Result<Vec<crate::types::Tag>> {
915 let tags_json: Option<String> = self.conn.query_row(
916 "SELECT tags FROM _tables WHERE table_name = ?1",
917 params![table_name],
918 |row| row.get(0),
919 )?;
920
921 match tags_json {
922 Some(json) => serde_json::from_str(&json)
923 .map_err(|e| DynoxideError::InternalServerError(format!("Bad tags JSON: {e}"))),
924 None => Ok(Vec::new()),
925 }
926 }
927
928 pub fn set_tags(&self, table_name: &str, new_tags: &[crate::types::Tag]) -> Result<()> {
930 use std::collections::BTreeMap;
931
932 let existing = self.get_tags(table_name)?;
933 let mut tag_map: BTreeMap<String, String> =
934 existing.into_iter().map(|t| (t.key, t.value)).collect();
935
936 for tag in new_tags {
937 tag_map.insert(tag.key.clone(), tag.value.clone());
938 }
939
940 if tag_map.len() > 50 {
941 return Err(DynoxideError::ValidationException(
942 "One or more parameter values were invalid: \
943 Too many tags: tag limit is 50"
944 .to_string(),
945 ));
946 }
947
948 let merged: Vec<crate::types::Tag> = tag_map
949 .into_iter()
950 .map(|(k, v)| crate::types::Tag { key: k, value: v })
951 .collect();
952
953 let json = serde_json::to_string(&merged)
954 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
955
956 self.conn.execute(
957 "UPDATE _tables SET tags = ?1 WHERE table_name = ?2",
958 params![json, table_name],
959 )?;
960 Ok(())
961 }
962
963 pub fn update_deletion_protection(&self, table_name: &str, enabled: bool) -> Result<()> {
965 let (sql, params) = sql_builders::update_deletion_protection(table_name, enabled);
966 self.conn
967 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
968 self.metadata_cache.borrow_mut().remove(table_name);
969 Ok(())
970 }
971
972 pub fn remove_tags(&self, table_name: &str, keys: &[String]) -> Result<()> {
974 let mut tags = self.get_tags(table_name)?;
975 tags.retain(|t| !keys.contains(&t.key));
976
977 let json = if tags.is_empty() {
978 None
979 } else {
980 Some(
981 serde_json::to_string(&tags)
982 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?,
983 )
984 };
985
986 self.conn.execute(
987 "UPDATE _tables SET tags = ?1 WHERE table_name = ?2",
988 params![json, table_name],
989 )?;
990 Ok(())
991 }
992
993 pub fn list_table_names(&self) -> Result<Vec<String>> {
995 let (sql, params) = sql_builders::list_table_names();
996 let mut stmt = self.conn.prepare(&sql)?;
997 let names = stmt
998 .query_map(rusqlite::params_from_iter(params.iter()), |row| row.get(0))?
999 .collect::<std::result::Result<Vec<String>, _>>()?;
1000 Ok(names)
1001 }
1002
1003 pub fn table_exists(&self, table_name: &str) -> Result<bool> {
1005 let (sql, params) = sql_builders::table_exists(table_name);
1006 let count: i32 =
1007 self.conn
1008 .query_row(&sql, rusqlite::params_from_iter(params.iter()), |row| {
1009 row.get(0)
1010 })?;
1011 Ok(count > 0)
1012 }
1013
1014 #[allow(dead_code)]
1016 pub(crate) fn invalidate_metadata_cache(&self, table_name: &str) {
1017 self.metadata_cache.borrow_mut().remove(table_name);
1018 }
1019
1020 pub fn create_data_table(&self, table_name: &str) -> Result<()> {
1026 let (sql, params) = sql_builders::create_data_table(table_name);
1027 self.conn
1028 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
1029 Ok(())
1030 }
1031
1032 pub fn drop_data_table(&self, table_name: &str) -> Result<()> {
1034 let (sql, params) = sql_builders::drop_data_table(table_name);
1035 self.conn
1036 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
1037 Ok(())
1038 }
1039
1040 pub fn create_gsi_table(&self, table_name: &str, index_name: &str) -> Result<()> {
1042 let (sql, _) = sql_builders::create_gsi_table(table_name, index_name);
1043 self.conn.execute_batch(&sql)?;
1044 Ok(())
1045 }
1046
1047 pub fn drop_gsi_table(&self, table_name: &str, index_name: &str) -> Result<()> {
1049 let (sql, params) = sql_builders::drop_gsi_table(table_name, index_name);
1050 self.conn
1051 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
1052 Ok(())
1053 }
1054
1055 #[allow(clippy::too_many_arguments)]
1061 pub fn insert_gsi_item(
1062 &self,
1063 table_name: &str,
1064 index_name: &str,
1065 gsi_pk: &str,
1066 gsi_sk: &str,
1067 table_pk: &str,
1068 table_sk: &str,
1069 item_json: &str,
1070 ) -> Result<()> {
1071 let sql = sql_builders::gsi_insert_sql(table_name, index_name);
1072 let params = sql_builders::gsi_insert_params(gsi_pk, gsi_sk, table_pk, table_sk, item_json);
1073 self.conn
1074 .prepare_cached(&sql)?
1075 .execute(rusqlite::params_from_iter(params.iter()))?;
1076 Ok(())
1077 }
1078
1079 pub fn insert_gsi_items(
1083 &self,
1084 table_name: &str,
1085 index_name: &str,
1086 rows: &[crate::storage_backend::GsiItemRow],
1087 ) -> Result<()> {
1088 let sql = sql_builders::gsi_insert_sql(table_name, index_name);
1089 let mut stmt = self.conn.prepare_cached(&sql)?;
1090 for row in rows {
1091 let params = sql_builders::gsi_insert_params(
1092 &row.gsi_pk,
1093 &row.gsi_sk,
1094 &row.table_pk,
1095 &row.table_sk,
1096 &row.item_json,
1097 );
1098 stmt.execute(rusqlite::params_from_iter(params.iter()))?;
1099 }
1100 Ok(())
1101 }
1102
1103 pub fn delete_gsi_item(
1105 &self,
1106 table_name: &str,
1107 index_name: &str,
1108 table_pk: &str,
1109 table_sk: &str,
1110 ) -> Result<()> {
1111 let (sql, params) =
1112 sql_builders::delete_gsi_item(table_name, index_name, table_pk, table_sk);
1113 self.conn
1114 .prepare_cached(&sql)?
1115 .execute(rusqlite::params_from_iter(params.iter()))?;
1116 Ok(())
1117 }
1118
1119 pub fn query_gsi_items(
1121 &self,
1122 table_name: &str,
1123 index_name: &str,
1124 gsi_pk: &str,
1125 params: &QueryParams,
1126 ) -> Result<Vec<(String, String, String)>> {
1127 let (sql, params_vec) =
1128 sql_builders::query_gsi_items(table_name, index_name, gsi_pk, params);
1129 let mut stmt = self.conn.prepare(&sql)?;
1130 let rows = stmt
1131 .query_map(rusqlite::params_from_iter(params_vec.iter()), |row| {
1132 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
1133 })?
1134 .collect::<std::result::Result<Vec<_>, _>>()?;
1135
1136 Ok(rows)
1137 }
1138
1139 pub fn scan_gsi_items(
1141 &self,
1142 table_name: &str,
1143 index_name: &str,
1144 params: &ScanParams,
1145 ) -> Result<Vec<(String, String, String)>> {
1146 let (sql, params_vec) = sql_builders::scan_gsi_items(table_name, index_name, params);
1147 let mut stmt = self.conn.prepare(&sql)?;
1148 let rows = stmt
1149 .query_map(rusqlite::params_from_iter(params_vec.iter()), |row| {
1150 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
1151 })?
1152 .collect::<std::result::Result<Vec<_>, _>>()?;
1153
1154 Ok(rows)
1155 }
1156
1157 pub fn create_lsi_table(&self, table_name: &str, index_name: &str) -> Result<()> {
1163 let (sql, _) = sql_builders::create_lsi_table(table_name, index_name);
1164 self.conn.execute_batch(&sql)?;
1165 Ok(())
1166 }
1167
1168 pub fn create_vector_table(&self, table_name: &str, index_name: &str) -> Result<()> {
1170 let (sql, _) = sql_builders::create_vector_table(table_name, index_name);
1171 self.conn.execute_batch(&sql)?;
1172 Ok(())
1173 }
1174
1175 pub fn insert_vector_items(
1178 &self,
1179 table_name: &str,
1180 index_name: &str,
1181 rows: &[crate::storage_backend::VectorItemRow],
1182 ) -> Result<()> {
1183 let sql = sql_builders::vector_insert_sql(table_name, index_name);
1184 let mut stmt = self.conn.prepare_cached(&sql)?;
1185 for row in rows {
1186 let params = sql_builders::vector_insert_params(
1187 &row.table_pk,
1188 &row.table_sk,
1189 &row.hash_value,
1190 &row.vector_json,
1191 &row.filter_json,
1192 &row.item_json,
1193 row.entry_bytes,
1194 );
1195 stmt.execute(rusqlite::params_from_iter(params.iter()))?;
1196 }
1197 Ok(())
1198 }
1199
1200 pub fn delete_vector_item(
1203 &self,
1204 table_name: &str,
1205 index_name: &str,
1206 table_pk: &str,
1207 table_sk: &str,
1208 ) -> Result<()> {
1209 let (sql, params) =
1210 sql_builders::delete_vector_item(table_name, index_name, table_pk, table_sk);
1211 self.conn
1212 .prepare_cached(&sql)?
1213 .execute(rusqlite::params_from_iter(params.iter()))?;
1214 Ok(())
1215 }
1216
1217 pub fn query_vector_candidates(
1221 &self,
1222 table_name: &str,
1223 index_name: &str,
1224 hash_value: Option<&str>,
1225 ) -> Result<Vec<crate::storage_backend::VectorCandidateRow>> {
1226 let (sql, params) =
1227 sql_builders::query_vector_candidates(table_name, index_name, hash_value);
1228 let mut stmt = self.conn.prepare(&sql)?;
1229 let rows = stmt
1230 .query_map(rusqlite::params_from_iter(params.iter()), |row| {
1231 Ok(crate::storage_backend::VectorCandidateRow {
1232 table_pk: row.get(0)?,
1233 table_sk: row.get(1)?,
1234 vector_json: row.get(2)?,
1235 filter_json: row.get(3)?,
1236 entry_bytes: row.get(4)?,
1237 })
1238 })?
1239 .collect::<std::result::Result<Vec<_>, _>>()?;
1240 Ok(rows)
1241 }
1242
1243 pub fn vector_items_for_keys(
1245 &self,
1246 table_name: &str,
1247 index_name: &str,
1248 keys: &[(String, String)],
1249 ) -> Result<Vec<(String, String, String)>> {
1250 if keys.is_empty() {
1251 return Ok(Vec::new());
1252 }
1253 let (sql, params) = sql_builders::vector_items_for_keys(table_name, index_name, keys);
1254 let mut stmt = self.conn.prepare(&sql)?;
1255 let rows = stmt
1256 .query_map(rusqlite::params_from_iter(params.iter()), |row| {
1257 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
1258 })?
1259 .collect::<std::result::Result<Vec<_>, _>>()?;
1260 Ok(rows)
1261 }
1262
1263 pub fn drop_vector_table(&self, table_name: &str, index_name: &str) -> Result<()> {
1265 let (sql, params) = sql_builders::drop_vector_table(table_name, index_name);
1266 self.conn
1267 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
1268 Ok(())
1269 }
1270
1271 pub fn drop_lsi_table(&self, table_name: &str, index_name: &str) -> Result<()> {
1273 let (sql, params) = sql_builders::drop_lsi_table(table_name, index_name);
1274 self.conn
1275 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
1276 Ok(())
1277 }
1278
1279 #[allow(clippy::too_many_arguments)]
1285 pub fn insert_lsi_item(
1286 &self,
1287 table_name: &str,
1288 index_name: &str,
1289 pk: &str,
1290 sk: &str,
1291 base_pk: &str,
1292 base_sk: &str,
1293 item_json: &str,
1294 ) -> Result<()> {
1295 let sql = sql_builders::lsi_insert_sql(table_name, index_name);
1296 let params = sql_builders::lsi_insert_params(pk, sk, base_pk, base_sk, item_json);
1297 self.conn
1298 .prepare_cached(&sql)?
1299 .execute(rusqlite::params_from_iter(params.iter()))?;
1300 Ok(())
1301 }
1302
1303 pub fn delete_lsi_item(
1305 &self,
1306 table_name: &str,
1307 index_name: &str,
1308 base_pk: &str,
1309 base_sk: &str,
1310 ) -> Result<()> {
1311 let (sql, params) = sql_builders::delete_lsi_item(table_name, index_name, base_pk, base_sk);
1312 self.conn
1313 .prepare_cached(&sql)?
1314 .execute(rusqlite::params_from_iter(params.iter()))?;
1315 Ok(())
1316 }
1317
1318 pub fn query_lsi_items(
1320 &self,
1321 table_name: &str,
1322 index_name: &str,
1323 pk: &str,
1324 params: &QueryParams,
1325 ) -> Result<Vec<(String, String, String)>> {
1326 let (sql, params_vec) = sql_builders::query_lsi_items(table_name, index_name, pk, params);
1327 let mut stmt = self.conn.prepare(&sql)?;
1328 let rows = stmt
1329 .query_map(rusqlite::params_from_iter(params_vec.iter()), |row| {
1330 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
1331 })?
1332 .collect::<std::result::Result<Vec<_>, _>>()?;
1333
1334 Ok(rows)
1335 }
1336
1337 pub fn scan_lsi_items(
1339 &self,
1340 table_name: &str,
1341 index_name: &str,
1342 params: &ScanParams,
1343 ) -> Result<Vec<(String, String, String)>> {
1344 let (sql, params_vec) = sql_builders::scan_lsi_items(table_name, index_name, params);
1345 let mut stmt = self.conn.prepare(&sql)?;
1346 let rows = stmt
1347 .query_map(rusqlite::params_from_iter(params_vec.iter()), |row| {
1348 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
1349 })?
1350 .collect::<std::result::Result<Vec<_>, _>>()?;
1351
1352 Ok(rows)
1353 }
1354
1355 pub fn begin_transaction(&self) -> Result<()> {
1361 self.conn.execute_batch(sql_builders::BEGIN)?;
1362 Ok(())
1363 }
1364
1365 pub fn commit(&self) -> Result<()> {
1367 self.conn.execute_batch(sql_builders::COMMIT)?;
1368 Ok(())
1369 }
1370
1371 pub fn rollback(&self) -> Result<()> {
1373 self.conn.execute_batch(sql_builders::ROLLBACK)?;
1374 Ok(())
1375 }
1376
1377 pub(crate) fn recorded_schema_version(&self) -> Result<Option<u32>> {
1380 use rusqlite::OptionalExtension;
1381 let value: Option<String> = self
1382 .conn
1383 .query_row(sql_builders::GET_SCHEMA_VERSION, [], |r| r.get(0))
1384 .optional()?;
1385 Ok(crate::storage_backend::schema::parse_recorded(
1386 value.as_deref(),
1387 ))
1388 }
1389
1390 pub(crate) fn record_schema_version(&self, version: u32) -> Result<()> {
1393 let (sql, params) = sql_builders::set_schema_version(version);
1394 self.conn
1395 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
1396 Ok(())
1397 }
1398
1399 pub fn enable_bulk_loading(&self) -> Result<()> {
1405 self.conn.execute_batch(
1406 "PRAGMA synchronous = OFF;
1407 PRAGMA cache_size = -64000;
1408 PRAGMA temp_store = MEMORY;
1409 PRAGMA mmap_size = 268435456;",
1410 )?;
1411 Ok(())
1412 }
1413
1414 pub fn disable_bulk_loading(&self) -> Result<()> {
1416 self.conn.execute_batch(
1417 "PRAGMA synchronous = NORMAL;
1418 PRAGMA cache_size = -2000;
1419 PRAGMA temp_store = DEFAULT;
1420 PRAGMA mmap_size = 0;",
1421 )?;
1422 Ok(())
1423 }
1424
1425 pub fn put_item(
1431 &self,
1432 table_name: &str,
1433 pk: &str,
1434 sk: &str,
1435 item_json: &str,
1436 item_size: usize,
1437 ) -> Result<Option<String>> {
1438 self.put_item_with_hash(table_name, pk, sk, item_json, item_size, "")
1439 }
1440
1441 pub fn put_item_with_hash(
1443 &self,
1444 table_name: &str,
1445 pk: &str,
1446 sk: &str,
1447 item_json: &str,
1448 item_size: usize,
1449 hash_prefix: &str,
1450 ) -> Result<Option<String>> {
1451 let old_item = self.get_item(table_name, pk, sk)?;
1453
1454 let (sql, params) =
1455 sql_builders::put_item_with_hash(table_name, pk, sk, item_json, item_size, hash_prefix);
1456 self.conn
1457 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
1458
1459 Ok(old_item)
1460 }
1461
1462 pub fn put_base_items(
1467 &self,
1468 table_name: &str,
1469 rows: &[crate::storage_backend::BaseItemRow],
1470 ) -> Result<()> {
1471 let escaped = escape_table_name(table_name);
1472 let sql = format!(
1473 "INSERT OR REPLACE INTO \"{escaped}\" (pk, sk, item_json, item_size, cached_at, hash_prefix) \
1474 VALUES (?1, ?2, ?3, ?4, ?5, ?6)"
1475 );
1476 let mut stmt = self.conn.prepare_cached(&sql)?;
1477 for row in rows {
1478 stmt.execute(params![
1479 row.pk,
1480 row.sk,
1481 row.item_json,
1482 row.item_size as i64,
1483 row.cached_at,
1484 row.hash_prefix
1485 ])?;
1486 }
1487 Ok(())
1488 }
1489
1490 pub fn get_item(&self, table_name: &str, pk: &str, sk: &str) -> Result<Option<String>> {
1492 let (sql, params) = sql_builders::get_item(table_name, pk, sk);
1493 let result = self
1494 .conn
1495 .query_row(&sql, rusqlite::params_from_iter(params.iter()), |row| {
1496 row.get(0)
1497 });
1498
1499 match result {
1500 Ok(json) => Ok(Some(json)),
1501 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
1502 Err(e) => Err(DynoxideError::from(e)),
1503 }
1504 }
1505
1506 pub fn get_partition_size(&self, table_name: &str, pk: &str) -> Result<i64> {
1508 let (sql, params) = sql_builders::get_partition_size(table_name, pk);
1509 let size: i64 =
1510 self.conn
1511 .query_row(&sql, rusqlite::params_from_iter(params.iter()), |row| {
1512 row.get(0)
1513 })?;
1514 Ok(size)
1515 }
1516
1517 pub fn get_lsi_partition_size(
1520 &self,
1521 table_name: &str,
1522 index_name: &str,
1523 pk: &str,
1524 ) -> Result<i64> {
1525 let (sql, params) = sql_builders::get_lsi_partition_size(table_name, index_name, pk);
1526 let size: i64 =
1527 self.conn
1528 .query_row(&sql, rusqlite::params_from_iter(params.iter()), |row| {
1529 row.get(0)
1530 })?;
1531 Ok(size)
1532 }
1533
1534 pub fn delete_item(&self, table_name: &str, pk: &str, sk: &str) -> Result<Option<String>> {
1536 let old_item = self.get_item(table_name, pk, sk)?;
1537
1538 let (sql, params) = sql_builders::delete_item(table_name, pk, sk);
1539 self.conn
1540 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
1541
1542 Ok(old_item)
1543 }
1544
1545 pub fn query_items(
1550 &self,
1551 table_name: &str,
1552 pk: &str,
1553 params: &QueryParams,
1554 ) -> Result<Vec<(String, String, String)>> {
1555 let (sql, params_vec) = sql_builders::query_items(table_name, pk, params);
1556 let mut stmt = self.conn.prepare(&sql)?;
1557 let rows = stmt
1558 .query_map(rusqlite::params_from_iter(params_vec.iter()), |row| {
1559 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
1560 })?
1561 .collect::<std::result::Result<Vec<_>, _>>()?;
1562
1563 Ok(rows)
1564 }
1565
1566 pub fn scan_items(
1571 &self,
1572 table_name: &str,
1573 params: &ScanParams,
1574 ) -> Result<Vec<(String, String, String)>> {
1575 let (sql, params_vec) = sql_builders::scan_items(table_name, params);
1576 let mut stmt = self.conn.prepare(&sql)?;
1577 let rows = stmt
1578 .query_map(rusqlite::params_from_iter(params_vec.iter()), |row| {
1579 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
1580 })?
1581 .collect::<std::result::Result<Vec<_>, _>>()?;
1582
1583 Ok(rows)
1584 }
1585
1586 pub fn count_items(&self, table_name: &str) -> Result<i64> {
1588 let (sql, params) = sql_builders::count_items(table_name);
1589 let count: i64 =
1590 self.conn
1591 .query_row(&sql, rusqlite::params_from_iter(params.iter()), |row| {
1592 row.get(0)
1593 })?;
1594 Ok(count)
1595 }
1596
1597 pub fn item_count_and_size(&self, table_name: &str) -> Result<(i64, i64)> {
1599 let (sql, params) = sql_builders::item_count_and_size(table_name);
1600 let stats: (i64, i64) =
1601 self.conn
1602 .query_row(&sql, rusqlite::params_from_iter(params.iter()), |row| {
1603 Ok((row.get(0)?, row.get(1)?))
1604 })?;
1605 Ok(stats)
1606 }
1607
1608 pub fn gsi_item_count_and_size(
1611 &self,
1612 table_name: &str,
1613 index_name: &str,
1614 whole_items: bool,
1615 ) -> Result<(i64, i64)> {
1616 if whole_items {
1617 self.count_and_size(sql_builders::gsi_count_and_size_from_base(
1618 table_name, index_name,
1619 ))
1620 } else {
1621 self.count_and_size_entries(sql_builders::gsi_entries_item_json(table_name, index_name))
1622 }
1623 }
1624
1625 pub fn lsi_item_count_and_size(
1628 &self,
1629 table_name: &str,
1630 index_name: &str,
1631 whole_items: bool,
1632 ) -> Result<(i64, i64)> {
1633 if whole_items {
1634 self.count_and_size(sql_builders::lsi_count_and_size_from_base(
1635 table_name, index_name,
1636 ))
1637 } else {
1638 self.count_and_size_entries(sql_builders::lsi_entries_item_json(table_name, index_name))
1639 }
1640 }
1641
1642 pub fn vector_item_count_and_size(
1645 &self,
1646 table_name: &str,
1647 index_name: &str,
1648 ) -> Result<(i64, i64)> {
1649 self.count_and_size(sql_builders::vector_count_and_size(table_name, index_name))
1650 }
1651
1652 pub fn set_item_sizes(
1655 &self,
1656 table_name: &str,
1657 sizes: &[(String, String, usize)],
1658 ) -> Result<()> {
1659 for (pk, sk, size) in sizes {
1660 #[cfg(test)]
1661 Self::count_down(&self.item_size_fault, "injected item size write failure")?;
1662 let (sql, params) = sql_builders::set_item_size(table_name, pk, sk, *size);
1663 self.conn
1664 .prepare_cached(&sql)?
1665 .execute(rusqlite::params_from_iter(params.iter()))?;
1666 }
1667 Ok(())
1668 }
1669
1670 fn count_and_size(
1671 &self,
1672 (sql, params): (String, Vec<sql_builders::SqlParam<'static>>),
1673 ) -> Result<(i64, i64)> {
1674 Ok(self
1675 .conn
1676 .query_row(&sql, rusqlite::params_from_iter(params.iter()), |row| {
1677 Ok((row.get(0)?, row.get(1)?))
1678 })?)
1679 }
1680
1681 fn count_and_size_entries(
1682 &self,
1683 (sql, params): (String, Vec<sql_builders::SqlParam<'static>>),
1684 ) -> Result<(i64, i64)> {
1685 let mut stmt = self.conn.prepare(&sql)?;
1686 let mut rows = stmt.query(rusqlite::params_from_iter(params.iter()))?;
1687 let (mut count, mut bytes) = (0i64, 0i64);
1688 while let Some(row) = rows.next()? {
1689 let json: String = row.get(0)?;
1690 count += 1;
1691 bytes += crate::storage_backend::projected_entry_size(&json)
1692 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
1693 }
1694 Ok((count, bytes))
1695 }
1696
1697 pub fn db_path(&self) -> Option<String> {
1703 self.conn
1704 .path()
1705 .filter(|p| !p.is_empty())
1706 .map(|p| p.to_owned())
1707 }
1708
1709 pub fn db_size_bytes(&self) -> Result<u64> {
1711 let size: i64 = self.conn.query_row(
1712 "SELECT page_count * page_size FROM pragma_page_count(), pragma_page_size()",
1713 [],
1714 |row| row.get(0),
1715 )?;
1716 Ok(size as u64)
1717 }
1718
1719 pub fn table_count(&self) -> Result<usize> {
1721 let count: i64 = self
1722 .conn
1723 .query_row("SELECT COUNT(*) FROM _tables", [], |row| row.get(0))?;
1724 Ok(count as usize)
1725 }
1726
1727 pub fn table_stats(&self) -> Result<Vec<TableStats>> {
1731 let table_names = self.list_table_names()?;
1732 let mut stats = Vec::with_capacity(table_names.len());
1733 for name in table_names {
1734 let sql = format!(
1735 "SELECT COUNT(*), COALESCE(SUM(item_size), 0) FROM \"{}\"",
1736 escape_table_name(&name)
1737 );
1738 let (item_count, size_bytes): (i64, i64) = self
1739 .conn
1740 .query_row(&sql, [], |row| Ok((row.get(0)?, row.get(1)?)))?;
1741 stats.push(TableStats {
1742 table_name: name,
1743 item_count,
1744 size_bytes: size_bytes as u64,
1745 });
1746 }
1747 Ok(stats)
1748 }
1749
1750 pub fn database_info(&self) -> Result<DatabaseInfo> {
1755 let path = self.db_path();
1756 let size_bytes = self.db_size_bytes()?;
1757 let table_count = self.table_count()?;
1758 let stats = self.table_stats()?;
1759
1760 let mut table_details = Vec::with_capacity(stats.len());
1761 for s in stats {
1762 let metadata = self.get_table_metadata(&s.table_name)?;
1763 table_details.push(TableInfoEntry { stats: s, metadata });
1764 }
1765
1766 Ok(DatabaseInfo {
1767 path,
1768 size_bytes,
1769 table_count,
1770 tables: table_details,
1771 })
1772 }
1773
1774 pub fn vacuum_into(&self, path: &str) -> Result<()> {
1781 if path.contains('\0') {
1782 return Err(DynoxideError::ValidationException(
1783 "path contains null byte".to_string(),
1784 ));
1785 }
1786 self.conn
1787 .execute_batch(&format!("VACUUM INTO '{}'", path.replace('\'', "''")))?;
1788 Ok(())
1789 }
1790
1791 pub fn vacuum(&self) -> Result<()> {
1793 self.conn.execute_batch("VACUUM")?;
1794 Ok(())
1795 }
1796
1797 pub fn restore_from(&mut self, path: &str) -> Result<()> {
1806 let source = Connection::open_with_flags(
1810 path,
1811 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY
1812 | rusqlite::OpenFlags::SQLITE_OPEN_URI
1813 | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
1814 )?;
1815 self.restore_from_connection(&source)
1816 }
1817
1818 pub fn backup_to_memory(&self) -> Result<Connection> {
1823 let mut dest = Connection::open_in_memory()?;
1824 {
1825 let backup = rusqlite::backup::Backup::new(&self.conn, &mut dest)?;
1826 run_backup(&backup)?;
1827 }
1828 Ok(dest)
1829 }
1830
1831 pub fn restore_from_connection(&mut self, source: &Connection) -> Result<()> {
1846 ensure_snapshot(source)?;
1847 refuse_newer_schema(source)?;
1848 let staged = self.staged_copy(source)?;
1849 {
1850 let backup = rusqlite::backup::Backup::new(&staged.conn, &mut self.conn)?;
1851 run_backup(&backup)?;
1852 }
1853 self.metadata_cache.borrow_mut().clear();
1854 Ok(())
1855 }
1856
1857 fn staged_copy(&self, source: &Connection) -> Result<Storage> {
1861 let mut staged = Storage {
1862 conn: Connection::open("")?,
1867 metadata_cache: RefCell::new(HashMap::new()),
1868 #[cfg(test)]
1871 gsi_insert_fault: RefCell::new(self.gsi_insert_fault.take()),
1872 #[cfg(test)]
1873 item_size_fault: RefCell::new(self.item_size_fault.take()),
1874 clock: Arc::clone(&self.clock),
1875 };
1876 let copied = match rusqlite::backup::Backup::new(source, &mut staged.conn) {
1877 Ok(backup) => run_backup(&backup),
1878 Err(e) => Err(e.into()),
1879 };
1880 let migrated = copied
1881 .and_then(|()| Self::register_functions(&staged.conn))
1882 .and_then(|()| staged.migrate_to_current());
1883 #[cfg(test)]
1884 {
1885 *self.gsi_insert_fault.borrow_mut() = staged.gsi_insert_fault.take();
1886 *self.item_size_fault.borrow_mut() = staged.item_size_fault.take();
1887 }
1888 migrated.map(|()| staged)
1889 }
1890
1891 pub fn connection_size_bytes(conn: &Connection) -> Result<u64> {
1893 let size: i64 = conn.query_row(
1894 "SELECT page_count * page_size FROM pragma_page_count(), pragma_page_size()",
1895 [],
1896 |row| row.get(0),
1897 )?;
1898 Ok(size as u64)
1899 }
1900
1901 pub fn enable_stream(&self, table_name: &str, view_type: &str, label: &str) -> Result<()> {
1907 self.conn.execute(
1908 "UPDATE _tables SET stream_enabled = 1, stream_view_type = ?1, stream_label = ?2 WHERE table_name = ?3",
1909 params![view_type, label, table_name],
1910 )?;
1911 self.metadata_cache.borrow_mut().remove(table_name);
1912 Ok(())
1913 }
1914
1915 pub fn disable_stream(&self, table_name: &str) -> Result<()> {
1917 self.conn.execute(
1918 "UPDATE _tables SET stream_enabled = 0 WHERE table_name = ?1",
1919 params![table_name],
1920 )?;
1921 self.metadata_cache.borrow_mut().remove(table_name);
1922 Ok(())
1923 }
1924
1925 #[allow(clippy::too_many_arguments)]
1927 pub fn insert_stream_record(
1928 &self,
1929 table_name: &str,
1930 event_name: &str,
1931 keys_json: &str,
1932 new_image: Option<&str>,
1933 old_image: Option<&str>,
1934 sequence_number: &str,
1935 shard_id: &str,
1936 created_at: i64,
1937 ) -> Result<()> {
1938 self.insert_stream_record_with_identity(
1939 table_name,
1940 event_name,
1941 keys_json,
1942 new_image,
1943 old_image,
1944 sequence_number,
1945 shard_id,
1946 created_at,
1947 None,
1948 )
1949 }
1950
1951 #[allow(clippy::too_many_arguments)]
1953 pub fn insert_stream_record_with_identity(
1954 &self,
1955 table_name: &str,
1956 event_name: &str,
1957 keys_json: &str,
1958 new_image: Option<&str>,
1959 old_image: Option<&str>,
1960 sequence_number: &str,
1961 shard_id: &str,
1962 created_at: i64,
1963 user_identity: Option<&str>,
1964 ) -> Result<()> {
1965 self.conn.execute(
1966 "INSERT INTO _stream_records (table_name, event_name, keys_json, new_image, old_image, sequence_number, shard_id, created_at, user_identity)
1967 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
1968 params![table_name, event_name, keys_json, new_image, old_image, sequence_number, shard_id, created_at, user_identity],
1969 )?;
1970 Ok(())
1971 }
1972
1973 pub fn next_stream_sequence_number(&self, table_name: &str) -> Result<i64> {
1975 let result: std::result::Result<i64, _> = self.conn.query_row(
1976 "SELECT COALESCE(MAX(CAST(sequence_number AS INTEGER)), 0) + 1 FROM _stream_records WHERE table_name = ?1",
1977 params![table_name],
1978 |row| row.get(0),
1979 );
1980 match result {
1981 Ok(n) => Ok(n),
1982 Err(_) => Ok(1),
1983 }
1984 }
1985
1986 pub fn get_stream_records(
1988 &self,
1989 table_name: &str,
1990 shard_id: &str,
1991 after_sequence: i64,
1992 limit: usize,
1993 ) -> Result<Vec<StreamRecord>> {
1994 let mut stmt = self.conn.prepare(
1995 "SELECT event_name, keys_json, new_image, old_image, sequence_number, created_at, user_identity
1996 FROM _stream_records
1997 WHERE table_name = ?1 AND shard_id = ?2 AND CAST(sequence_number AS INTEGER) > ?3
1998 ORDER BY CAST(sequence_number AS INTEGER) ASC
1999 LIMIT ?4",
2000 )?;
2001 let rows = stmt
2002 .query_map(
2003 params![table_name, shard_id, after_sequence, limit as i64],
2004 |row| {
2005 Ok(StreamRecord {
2006 event_name: row.get(0)?,
2007 keys_json: row.get(1)?,
2008 new_image: row.get(2)?,
2009 old_image: row.get(3)?,
2010 sequence_number: row.get(4)?,
2011 created_at: row.get(5)?,
2012 user_identity: row.get(6)?,
2013 })
2014 },
2015 )?
2016 .collect::<std::result::Result<Vec<_>, _>>()?;
2017 Ok(rows)
2018 }
2019
2020 pub fn list_stream_enabled_tables(&self) -> Result<Vec<TableMetadata>> {
2022 let sql = format!(
2023 "SELECT {} FROM _tables WHERE stream_enabled = 1 ORDER BY table_name",
2024 sql_builders::TABLE_METADATA_COLUMNS
2025 );
2026 let mut stmt = self.conn.prepare(&sql)?;
2027 let rows = stmt
2028 .query_map([], row_to_metadata)?
2029 .collect::<std::result::Result<Vec<_>, _>>()?;
2030 Ok(rows)
2031 }
2032
2033 pub fn update_ttl_config(
2039 &self,
2040 table_name: &str,
2041 attribute_name: Option<&str>,
2042 enabled: bool,
2043 ) -> Result<()> {
2044 self.conn.execute(
2045 "UPDATE _tables SET ttl_attribute = ?1, ttl_enabled = ?2 WHERE table_name = ?3",
2046 params![attribute_name, enabled as i32, table_name],
2047 )?;
2048 self.metadata_cache.borrow_mut().remove(table_name);
2049 Ok(())
2050 }
2051
2052 pub fn list_ttl_enabled_tables(&self) -> Result<Vec<TableMetadata>> {
2054 let sql = format!(
2055 "SELECT {} FROM _tables WHERE ttl_enabled = 1 ORDER BY table_name",
2056 sql_builders::TABLE_METADATA_COLUMNS
2057 );
2058 let mut stmt = self.conn.prepare(&sql)?;
2059 let rows = stmt
2060 .query_map([], row_to_metadata)?
2061 .collect::<std::result::Result<Vec<_>, _>>()?;
2062 Ok(rows)
2063 }
2064
2065 pub fn get_shard_sequence_range(
2067 &self,
2068 table_name: &str,
2069 shard_id: &str,
2070 ) -> Result<(Option<String>, Option<String>)> {
2071 let result: std::result::Result<(Option<String>, Option<String>), _> = self.conn.query_row(
2072 "SELECT MIN(sequence_number), MAX(sequence_number) FROM _stream_records WHERE table_name = ?1 AND shard_id = ?2",
2073 params![table_name, shard_id],
2074 |row| Ok((row.get(0)?, row.get(1)?)),
2075 );
2076 match result {
2077 Ok(range) => Ok(range),
2078 Err(_) => Ok((None, None)),
2079 }
2080 }
2081
2082 pub fn touch_cached_at(
2088 &self,
2089 table_name: &str,
2090 pk: &str,
2091 sk: &str,
2092 timestamp: f64,
2093 ) -> Result<()> {
2094 let sql = format!(
2095 "UPDATE \"{}\" SET cached_at = ?1 WHERE pk = ?2 AND sk = ?3",
2096 escape_table_name(table_name)
2097 );
2098 self.conn.execute(&sql, params![timestamp, pk, sk])?;
2099 Ok(())
2100 }
2101
2102 pub fn get_lru_items(
2107 &self,
2108 table_name: &str,
2109 limit: usize,
2110 ) -> Result<Vec<(String, String, i64)>> {
2111 let sql = format!(
2112 "SELECT pk, sk, item_size FROM \"{}\" WHERE cached_at IS NOT NULL ORDER BY cached_at ASC LIMIT ?1",
2113 escape_table_name(table_name)
2114 );
2115 let mut stmt = self.conn.prepare(&sql)?;
2116 let rows = stmt
2117 .query_map(params![limit as i64], |row| {
2118 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
2119 })?
2120 .collect::<std::result::Result<Vec<_>, _>>()?;
2121 Ok(rows)
2122 }
2123}
2124
2125#[derive(Debug, Clone)]
2127pub struct StreamRecord {
2128 pub event_name: String,
2129 pub keys_json: String,
2130 pub new_image: Option<String>,
2131 pub old_image: Option<String>,
2132 pub sequence_number: String,
2133 pub created_at: i64,
2134 pub user_identity: Option<String>,
2135}
2136
2137#[derive(Debug, Clone)]
2139pub struct TableStats {
2140 pub table_name: String,
2141 pub item_count: i64,
2142 pub size_bytes: u64,
2143}
2144
2145#[derive(Debug, Clone)]
2147pub struct DatabaseInfo {
2148 pub path: Option<String>,
2149 pub size_bytes: u64,
2150 pub table_count: usize,
2151 pub tables: Vec<TableInfoEntry>,
2152}
2153
2154#[derive(Debug, Clone)]
2156pub struct TableInfoEntry {
2157 pub stats: TableStats,
2158 pub metadata: Option<TableMetadata>,
2159}
2160
2161#[derive(Debug, Clone)]
2167pub struct TableMetadata {
2168 pub table_name: String,
2169 pub key_schema: String,
2170 pub attribute_definitions: String,
2171 pub gsi_definitions: Option<String>,
2172 pub lsi_definitions: Option<String>,
2173 pub stream_enabled: bool,
2174 pub stream_view_type: Option<String>,
2175 pub stream_label: Option<String>,
2176 pub ttl_attribute: Option<String>,
2177 pub ttl_enabled: bool,
2178 pub created_at: i64,
2179 pub table_status: String,
2180 pub billing_mode: Option<String>,
2181 pub provisioned_throughput: Option<String>,
2182 pub sse_specification: Option<String>,
2183 pub table_class: Option<String>,
2184 pub deletion_protection_enabled: bool,
2185 pub on_demand_throughput: Option<String>,
2186 pub table_id: Option<String>,
2189 pub vector_index_definitions: Option<String>,
2192}
2193
2194#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2196fn row_to_metadata(row: &rusqlite::Row) -> rusqlite::Result<TableMetadata> {
2197 Ok(TableMetadata {
2198 table_name: row.get(0)?,
2199 key_schema: row.get(1)?,
2200 attribute_definitions: row.get(2)?,
2201 gsi_definitions: row.get(3)?,
2202 lsi_definitions: row.get(4)?,
2203 stream_enabled: row.get::<_, i32>(5)? != 0,
2204 stream_view_type: row.get(6)?,
2205 stream_label: row.get(7)?,
2206 ttl_attribute: row.get(8)?,
2207 ttl_enabled: row.get::<_, i32>(9)? != 0,
2208 created_at: row.get(10)?,
2209 table_status: row.get(11)?,
2210 billing_mode: row.get(12)?,
2211 provisioned_throughput: row.get(13)?,
2212 sse_specification: row.get(14)?,
2213 table_class: row.get(15)?,
2214 deletion_protection_enabled: row.get::<_, i32>(16).unwrap_or(0) != 0,
2215 on_demand_throughput: row.get(17)?,
2216 table_id: row.get(18)?,
2217 vector_index_definitions: row.get(19)?,
2218 })
2219}
2220
2221#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2224const BACKUP_LOCK_PAUSE: std::time::Duration = std::time::Duration::from_millis(10);
2225
2226#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2231const BACKUP_LOCK_PATIENCE: std::time::Duration = REQUEST_BUSY_TIMEOUT;
2232
2233#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2236const BACKUP_RESTART_LIMIT: u32 = 50;
2237
2238#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2240#[derive(Clone, Copy, Debug, PartialEq)]
2241enum BackupStep {
2242 Done,
2243 More {
2245 remaining: i32,
2246 },
2247 Busy,
2249}
2250
2251#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2253#[derive(Clone, Copy, Debug, PartialEq)]
2254enum BackupNext {
2255 Finish,
2256 Step,
2257 Pause,
2258 GiveUpLocked,
2259 GiveUpRestarting,
2260}
2261
2262#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2265#[derive(Default)]
2266struct BackupWatch {
2267 busy_since: Option<std::time::Instant>,
2268 last_remaining: Option<i32>,
2269 restarts: u32,
2270}
2271
2272#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2273impl BackupWatch {
2274 fn observe(
2285 &mut self,
2286 step: BackupStep,
2287 started: std::time::Instant,
2288 now: std::time::Instant,
2289 patience: std::time::Duration,
2290 ) -> BackupNext {
2291 match step {
2292 BackupStep::Done => BackupNext::Finish,
2293 BackupStep::More { remaining } => {
2294 self.busy_since = None;
2295 let restarted = self.last_remaining.is_some_and(|last| remaining >= last);
2296 self.last_remaining = Some(remaining);
2297 if !restarted {
2298 return BackupNext::Step;
2299 }
2300 self.restarts += 1;
2301 if self.restarts > BACKUP_RESTART_LIMIT {
2302 BackupNext::GiveUpRestarting
2303 } else {
2304 BackupNext::Pause
2305 }
2306 }
2307 BackupStep::Busy => {
2308 let since = *self.busy_since.get_or_insert(started);
2309 if now.saturating_duration_since(since) >= patience {
2310 BackupNext::GiveUpLocked
2311 } else {
2312 BackupNext::Pause
2313 }
2314 }
2315 }
2316 }
2317}
2318
2319#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2329fn run_backup(backup: &rusqlite::backup::Backup<'_, '_>) -> Result<()> {
2330 let mut watch = BackupWatch::default();
2331 loop {
2332 let started = std::time::Instant::now();
2333 let step = backup_step(backup)?;
2334 match watch.observe(
2335 step,
2336 started,
2337 std::time::Instant::now(),
2338 BACKUP_LOCK_PATIENCE,
2339 ) {
2340 BackupNext::Finish => return Ok(()),
2341 BackupNext::Step => {}
2342 BackupNext::Pause => std::thread::sleep(BACKUP_LOCK_PAUSE),
2343 BackupNext::GiveUpLocked => {
2344 return Err(rusqlite::Error::SqliteFailure(
2345 rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_BUSY),
2346 Some("database is locked".to_string()),
2347 )
2348 .into());
2349 }
2350 BackupNext::GiveUpRestarting => {
2351 return Err(rusqlite::Error::SqliteFailure(
2352 rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_BUSY),
2353 Some(format!(
2354 "the database being copied kept changing; gave up after {BACKUP_RESTART_LIMIT} restarts"
2355 )),
2356 )
2357 .into());
2358 }
2359 }
2360 }
2361}
2362
2363#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2365fn backup_step(backup: &rusqlite::backup::Backup<'_, '_>) -> Result<BackupStep> {
2366 use rusqlite::backup::StepResult;
2367 Ok(match backup.step(100)? {
2368 StepResult::Done => BackupStep::Done,
2369 StepResult::More => BackupStep::More {
2370 remaining: backup.progress().remaining,
2371 },
2372 _ => BackupStep::Busy,
2374 })
2375}
2376
2377#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2383fn ensure_snapshot(source: &Connection) -> Result<()> {
2384 if table_exists(source, "_tables")? {
2385 Ok(())
2386 } else {
2387 Err(DynoxideError::ValidationException(
2388 "not a Dynoxide snapshot: it has no table metadata".to_string(),
2389 ))
2390 }
2391}
2392
2393#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2395fn table_exists(conn: &Connection, name: &str) -> Result<bool> {
2396 Ok(conn.query_row(sql_builders::TABLE_EXISTS, [name], |row| row.get(0))?)
2397}
2398
2399#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2404fn refuse_newer_schema(conn: &Connection) -> Result<()> {
2405 use rusqlite::OptionalExtension;
2406 if !table_exists(conn, "_config")? {
2407 return Ok(());
2408 }
2409 let value: Option<String> = conn
2410 .query_row(sql_builders::GET_SCHEMA_VERSION, [], |row| row.get(0))
2411 .optional()?;
2412 crate::storage_backend::schema::refuse_newer(crate::storage_backend::schema::parse_recorded(
2413 value.as_deref(),
2414 ))
2415 .map_err(DynoxideError::InternalServerError)
2416}
2417
2418#[cfg(all(test, any(feature = "native-sqlite", feature = "_has-encryption")))]
2419mod tests {
2420 use super::*;
2421
2422 fn test_storage() -> Storage {
2423 Storage::memory().expect("Failed to create in-memory storage")
2424 }
2425
2426 #[test]
2427 fn test_initialize_creates_metadata_tables() {
2428 let storage = test_storage();
2429 let version: String = storage
2431 .conn()
2432 .query_row(
2433 "SELECT value FROM _config WHERE key = 'schema_version'",
2434 [],
2435 |row| row.get(0),
2436 )
2437 .unwrap();
2438 assert_eq!(version, SCHEMA_VERSION.to_string());
2439 }
2440
2441 #[test]
2442 fn test_migrate_v6_to_v7_adds_on_demand_throughput_column() {
2443 let tmp = tempfile::NamedTempFile::new().unwrap();
2447 let path = tmp.path().to_str().unwrap().to_string();
2448
2449 {
2452 let conn = Connection::open(&path).unwrap();
2453 conn.execute_batch(
2454 "CREATE TABLE _config (key TEXT PRIMARY KEY, value TEXT NOT NULL);
2455 CREATE TABLE _tables (
2456 table_name TEXT PRIMARY KEY,
2457 key_schema TEXT NOT NULL,
2458 attribute_definitions TEXT NOT NULL,
2459 gsi_definitions TEXT,
2460 lsi_definitions TEXT,
2461 stream_enabled INTEGER DEFAULT 0,
2462 stream_view_type TEXT,
2463 stream_label TEXT,
2464 ttl_attribute TEXT,
2465 ttl_enabled INTEGER DEFAULT 0,
2466 created_at INTEGER NOT NULL,
2467 table_status TEXT NOT NULL DEFAULT 'ACTIVE',
2468 billing_mode TEXT DEFAULT 'PAY_PER_REQUEST',
2469 provisioned_throughput TEXT,
2470 tags TEXT,
2471 sse_specification TEXT,
2472 table_class TEXT,
2473 deletion_protection_enabled INTEGER DEFAULT 0
2474 );",
2475 )
2476 .unwrap();
2477 conn.execute(
2478 "INSERT INTO _config (key, value) VALUES ('schema_version', '6')",
2479 [],
2480 )
2481 .unwrap();
2482 conn.execute(
2483 "INSERT INTO _tables (table_name, key_schema, attribute_definitions, created_at) \
2484 VALUES ('LegacyTable', ?1, ?2, 0)",
2485 params![
2486 r#"[{"AttributeName":"pk","KeyType":"HASH"}]"#,
2487 r#"[{"AttributeName":"pk","AttributeType":"S"}]"#,
2488 ],
2489 )
2490 .unwrap();
2491 }
2492
2493 let storage = Storage::new(&path).unwrap();
2496 let version: String = storage
2497 .conn()
2498 .query_row(
2499 "SELECT value FROM _config WHERE key = 'schema_version'",
2500 [],
2501 |r| r.get(0),
2502 )
2503 .unwrap();
2504 assert_eq!(version, SCHEMA_VERSION.to_string());
2505
2506 let meta = storage.get_table_metadata("LegacyTable").unwrap().unwrap();
2509 assert_eq!(meta.table_name, "LegacyTable");
2510 assert!(meta.on_demand_throughput.is_none());
2511
2512 let col: Option<String> = storage
2514 .conn()
2515 .query_row(
2516 "SELECT on_demand_throughput FROM _tables WHERE table_name = 'LegacyTable'",
2517 [],
2518 |r| r.get(0),
2519 )
2520 .unwrap();
2521 assert!(col.is_none());
2522 }
2523
2524 #[test]
2528 fn test_migrate_v7_to_v8_backfills_table_id() {
2529 let tmp = tempfile::NamedTempFile::new().unwrap();
2530 let path = tmp.path().to_str().unwrap().to_string();
2531
2532 {
2535 let conn = Connection::open(&path).unwrap();
2536 conn.execute_batch(
2537 "CREATE TABLE _config (key TEXT PRIMARY KEY, value TEXT NOT NULL);
2538 CREATE TABLE _tables (
2539 table_name TEXT PRIMARY KEY,
2540 key_schema TEXT NOT NULL,
2541 attribute_definitions TEXT NOT NULL,
2542 gsi_definitions TEXT,
2543 lsi_definitions TEXT,
2544 stream_enabled INTEGER DEFAULT 0,
2545 stream_view_type TEXT,
2546 stream_label TEXT,
2547 ttl_attribute TEXT,
2548 ttl_enabled INTEGER DEFAULT 0,
2549 created_at INTEGER NOT NULL,
2550 table_status TEXT NOT NULL DEFAULT 'ACTIVE',
2551 billing_mode TEXT DEFAULT 'PAY_PER_REQUEST',
2552 provisioned_throughput TEXT,
2553 tags TEXT,
2554 sse_specification TEXT,
2555 table_class TEXT,
2556 deletion_protection_enabled INTEGER DEFAULT 0,
2557 on_demand_throughput TEXT
2558 );",
2559 )
2560 .unwrap();
2561 conn.execute(
2562 "INSERT INTO _config (key, value) VALUES ('schema_version', '7')",
2563 [],
2564 )
2565 .unwrap();
2566 conn.execute(
2567 "INSERT INTO _tables (table_name, key_schema, attribute_definitions, created_at) \
2568 VALUES ('LegacyTable', ?1, ?2, 0)",
2569 params![
2570 r#"[{"AttributeName":"pk","KeyType":"HASH"}]"#,
2571 r#"[{"AttributeName":"pk","AttributeType":"S"}]"#,
2572 ],
2573 )
2574 .unwrap();
2575 }
2576
2577 let storage = Storage::new(&path).unwrap();
2579 let version: String = storage
2580 .conn()
2581 .query_row(
2582 "SELECT value FROM _config WHERE key = 'schema_version'",
2583 [],
2584 |r| r.get(0),
2585 )
2586 .unwrap();
2587 assert_eq!(version, SCHEMA_VERSION.to_string());
2588
2589 let meta = storage.get_table_metadata("LegacyTable").unwrap().unwrap();
2592 let id = meta.table_id.expect("legacy table should be backfilled");
2593 assert!(!id.is_empty());
2594
2595 drop(storage);
2597 let storage2 = Storage::new(&path).unwrap();
2598 let meta2 = storage2.get_table_metadata("LegacyTable").unwrap().unwrap();
2599 assert_eq!(meta2.table_id.as_deref(), Some(id.as_str()));
2600 }
2601
2602 #[test]
2606 fn test_migrate_v8_to_v9_adds_vector_index_definitions_column() {
2607 let tmp = tempfile::NamedTempFile::new().unwrap();
2608 let path = tmp.path().to_str().unwrap().to_string();
2609
2610 {
2613 let conn = Connection::open(&path).unwrap();
2614 conn.execute_batch(
2615 "CREATE TABLE _config (key TEXT PRIMARY KEY, value TEXT NOT NULL);
2616 CREATE TABLE _tables (
2617 table_name TEXT PRIMARY KEY,
2618 key_schema TEXT NOT NULL,
2619 attribute_definitions TEXT NOT NULL,
2620 gsi_definitions TEXT,
2621 lsi_definitions TEXT,
2622 stream_enabled INTEGER DEFAULT 0,
2623 stream_view_type TEXT,
2624 stream_label TEXT,
2625 ttl_attribute TEXT,
2626 ttl_enabled INTEGER DEFAULT 0,
2627 created_at INTEGER NOT NULL,
2628 table_status TEXT NOT NULL DEFAULT 'ACTIVE',
2629 billing_mode TEXT DEFAULT 'PAY_PER_REQUEST',
2630 provisioned_throughput TEXT,
2631 tags TEXT,
2632 sse_specification TEXT,
2633 table_class TEXT,
2634 deletion_protection_enabled INTEGER DEFAULT 0,
2635 on_demand_throughput TEXT,
2636 table_id TEXT
2637 );",
2638 )
2639 .unwrap();
2640 conn.execute(
2641 "INSERT INTO _config (key, value) VALUES ('schema_version', '8')",
2642 [],
2643 )
2644 .unwrap();
2645 conn.execute(
2646 "INSERT INTO _tables (table_name, key_schema, attribute_definitions, created_at, table_id) \
2647 VALUES ('LegacyTable', ?1, ?2, 0, 'fixed-id')",
2648 params![
2649 r#"[{"AttributeName":"pk","KeyType":"HASH"}]"#,
2650 r#"[{"AttributeName":"pk","AttributeType":"S"}]"#,
2651 ],
2652 )
2653 .unwrap();
2654 }
2655
2656 let storage = Storage::new(&path).unwrap();
2658 let version: String = storage
2659 .conn()
2660 .query_row(
2661 "SELECT value FROM _config WHERE key = 'schema_version'",
2662 [],
2663 |r| r.get(0),
2664 )
2665 .unwrap();
2666 assert_eq!(version, SCHEMA_VERSION.to_string());
2667
2668 let meta = storage.get_table_metadata("LegacyTable").unwrap().unwrap();
2671 assert_eq!(meta.table_name, "LegacyTable");
2672 assert!(meta.vector_index_definitions.is_none());
2673
2674 let col: Option<String> = storage
2676 .conn()
2677 .query_row(
2678 "SELECT vector_index_definitions FROM _tables WHERE table_name = 'LegacyTable'",
2679 [],
2680 |r| r.get(0),
2681 )
2682 .unwrap();
2683 assert!(col.is_none());
2684 }
2685
2686 #[test]
2687 fn test_migrate_v9_to_v10_rebuilds_a_multi_attribute_gsi() {
2688 let tmp = tempfile::NamedTempFile::new().unwrap();
2689 let path = tmp.path().to_str().unwrap().to_string();
2690
2691 {
2693 let db = crate::Database::new(&path).unwrap();
2694 db.create_table(
2695 serde_json::from_value(serde_json::json!({
2696 "TableName": "Legacy",
2697 "BillingMode": "PAY_PER_REQUEST",
2698 "AttributeDefinitions": [
2699 {"AttributeName": "pk", "AttributeType": "S"},
2700 {"AttributeName": "h", "AttributeType": "S"},
2701 {"AttributeName": "a", "AttributeType": "S"},
2702 {"AttributeName": "b", "AttributeType": "S"}
2703 ],
2704 "KeySchema": [{"AttributeName": "pk", "KeyType": "HASH"}],
2705 "GlobalSecondaryIndexes": [{
2706 "IndexName": "byKey",
2707 "KeySchema": [
2708 {"AttributeName": "h", "KeyType": "HASH"},
2709 {"AttributeName": "a", "KeyType": "RANGE"},
2710 {"AttributeName": "b", "KeyType": "RANGE"}
2711 ],
2712 "Projection": {"ProjectionType": "ALL"}
2713 }]
2714 }))
2715 .unwrap(),
2716 )
2717 .unwrap();
2718 for item in [
2719 serde_json::json!({"pk": {"S": "1"}, "h": {"S": "x"}, "a": {"S": "m"}, "b": {"S": "2"}}),
2720 serde_json::json!({"pk": {"S": "2"}, "h": {"S": "x"}, "a": {"S": "m"}, "b": {"S": "1"}}),
2721 serde_json::json!({"pk": {"S": "3"}, "h": {"S": "x"}, "a": {"S": "m"}}),
2722 ] {
2723 db.put_item(
2724 serde_json::from_value(
2725 serde_json::json!({"TableName": "Legacy", "Item": item}),
2726 )
2727 .unwrap(),
2728 )
2729 .unwrap();
2730 }
2731 }
2732
2733 {
2736 let conn = Connection::open(&path).unwrap();
2737 conn.execute_batch(
2738 "DELETE FROM \"Legacy::gsi::byKey\";
2739 INSERT INTO \"Legacy::gsi::byKey\" (gsi_pk, gsi_sk, table_pk, table_sk, item_json)
2740 SELECT 'S:x', 'S:m', pk, sk, item_json FROM \"Legacy\";
2741 UPDATE _config SET value = '9' WHERE key = 'schema_version';",
2742 )
2743 .unwrap();
2744 }
2745
2746 let storage = Storage::new(&path).unwrap();
2747 let version: String = storage
2748 .conn()
2749 .query_row(
2750 "SELECT value FROM _config WHERE key = 'schema_version'",
2751 [],
2752 |r| r.get(0),
2753 )
2754 .unwrap();
2755 assert_eq!(version, SCHEMA_VERSION.to_string());
2756 let rows: Vec<(String, String)> = {
2757 let mut stmt = storage
2758 .conn()
2759 .prepare(
2760 "SELECT table_pk, gsi_sk FROM \"Legacy::gsi::byKey\" ORDER BY gsi_sk, table_pk",
2761 )
2762 .unwrap();
2763 stmt.query_map([], |r| Ok((r.get(0)?, r.get(1)?)))
2764 .unwrap()
2765 .collect::<rusqlite::Result<_>>()
2766 .unwrap()
2767 };
2768 let expected_key = |b: &str| {
2769 crate::actions::gsi::composite_key_string(&[
2770 &AttributeValue::S("m".into()),
2771 &AttributeValue::S(b.into()),
2772 ])
2773 .unwrap()
2774 };
2775 assert_eq!(
2776 rows,
2777 vec![
2778 ("S:2".to_string(), expected_key("1")),
2779 ("S:1".to_string(), expected_key("2")),
2780 ]
2781 );
2782 }
2783
2784 fn v9_snapshot_path() -> (tempfile::TempDir, String) {
2788 let dir = tempfile::tempdir().unwrap();
2789 let path = dir.path().join("legacy.db").to_str().unwrap().to_string();
2790 (dir, path)
2791 }
2792
2793 fn write_v9_database(path: &str, extra: usize) {
2808 {
2809 let db = crate::Database::new(path).unwrap();
2810 db.create_table(
2811 serde_json::from_value(serde_json::json!({
2812 "TableName": "Legacy",
2813 "BillingMode": "PAY_PER_REQUEST",
2814 "AttributeDefinitions": [
2815 {"AttributeName": "pk", "AttributeType": "S"},
2816 {"AttributeName": "h", "AttributeType": "S"},
2817 {"AttributeName": "h2", "AttributeType": "S"},
2818 {"AttributeName": "a", "AttributeType": "S"},
2819 {"AttributeName": "b", "AttributeType": "S"}
2820 ],
2821 "KeySchema": [{"AttributeName": "pk", "KeyType": "HASH"}],
2822 "GlobalSecondaryIndexes": [
2823 {
2824 "IndexName": "byKey",
2825 "KeySchema": [
2826 {"AttributeName": "h", "KeyType": "HASH"},
2827 {"AttributeName": "a", "KeyType": "RANGE"},
2828 {"AttributeName": "b", "KeyType": "RANGE"}
2829 ],
2830 "Projection": {"ProjectionType": "ALL"}
2831 },
2832 {
2833 "IndexName": "byTwoHash",
2834 "KeySchema": [
2835 {"AttributeName": "h", "KeyType": "HASH"},
2836 {"AttributeName": "h2", "KeyType": "HASH"},
2837 {"AttributeName": "a", "KeyType": "RANGE"}
2838 ],
2839 "Projection": {"ProjectionType": "ALL"}
2840 },
2841 {
2842 "IndexName": "bySingle",
2843 "KeySchema": [
2844 {"AttributeName": "h", "KeyType": "HASH"},
2845 {"AttributeName": "a", "KeyType": "RANGE"}
2846 ],
2847 "Projection": {"ProjectionType": "KEYS_ONLY"}
2848 }
2849 ]
2850 }))
2851 .unwrap(),
2852 )
2853 .unwrap();
2854 let named = [
2855 serde_json::json!({"pk": {"S": "1"}, "h": {"S": "x"}, "h2": {"S": "y"}, "a": {"S": "m"}, "b": {"S": "2"}}),
2856 serde_json::json!({"pk": {"S": "2"}, "h": {"S": "x"}, "h2": {"S": "y"}, "a": {"S": "m"}, "b": {"S": "1"}}),
2857 serde_json::json!({"pk": {"S": "3"}, "h": {"S": "x"}, "h2": {"S": "y"}, "a": {"S": "m"}}),
2858 ];
2859 let extras = (0..extra).map(|i| {
2860 serde_json::json!({
2861 "pk": {"S": format!("e{i:05}")},
2862 "h": {"S": "x"}, "h2": {"S": "y"}, "a": {"S": "m"},
2863 "b": {"S": format!("{i:05}")}
2864 })
2865 });
2866 for item in named.into_iter().chain(extras) {
2867 db.put_item(
2868 serde_json::from_value(
2869 serde_json::json!({"TableName": "Legacy", "Item": item}),
2870 )
2871 .unwrap(),
2872 )
2873 .unwrap();
2874 }
2875 db.create_table(
2876 serde_json::from_value(serde_json::json!({
2877 "TableName": "Vec",
2878 "BillingMode": "PAY_PER_REQUEST",
2879 "AttributeDefinitions": [{"AttributeName": "pk", "AttributeType": "S"}],
2880 "KeySchema": [{"AttributeName": "pk", "KeyType": "HASH"}],
2881 "VectorIndexes": [{
2882 "IndexName": "vix",
2883 "VectorAttribute": {"AttributeName": "e"},
2884 "Dimensions": 3,
2885 "DistanceFunction": "COSINE",
2886 "Projection": {"ProjectionType": "KEYS_ONLY"}
2887 }]
2888 }))
2889 .unwrap(),
2890 )
2891 .unwrap();
2892 db.put_item(
2893 serde_json::from_value(serde_json::json!({
2894 "TableName": "Vec",
2895 "Item": {
2896 "pk": {"S": "v"},
2897 "e": {"L": [{"N": "0.125"}, {"N": "0.125"}, {"N": "0.125"}]}
2898 }
2899 }))
2900 .unwrap(),
2901 )
2902 .unwrap();
2903 }
2904 let conn = Connection::open(path).unwrap();
2905 conn.execute_batch(
2906 "UPDATE \"Vec\" SET item_size = item_size + 7;
2907 DELETE FROM \"Legacy::gsi::byKey\";
2908 INSERT INTO \"Legacy::gsi::byKey\" (gsi_pk, gsi_sk, table_pk, table_sk, item_json)
2909 SELECT 'S:x', 'S:m', pk, sk, item_json FROM \"Legacy\";
2910 DELETE FROM \"Legacy::gsi::byTwoHash\";
2911 INSERT INTO \"Legacy::gsi::byTwoHash\" (gsi_pk, gsi_sk, table_pk, table_sk, item_json)
2912 SELECT 'S:x', 'S:m', pk, sk, item_json FROM \"Legacy\";
2913 UPDATE _config SET value = '9' WHERE key = 'schema_version';",
2914 )
2915 .unwrap();
2916 }
2917
2918 fn gsi_rows(conn: &Connection, index: &str) -> Vec<[String; 5]> {
2920 let mut stmt = conn
2921 .prepare(&format!(
2922 "SELECT gsi_pk, gsi_sk, table_pk, table_sk, item_json \
2923 FROM \"Legacy::gsi::{index}\" ORDER BY table_pk, table_sk"
2924 ))
2925 .unwrap();
2926 stmt.query_map([], |r| {
2927 Ok([r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?, r.get(4)?])
2928 })
2929 .unwrap()
2930 .collect::<rusqlite::Result<_>>()
2931 .unwrap()
2932 }
2933
2934 fn recorded_version(conn: &Connection) -> String {
2935 conn.query_row(
2936 "SELECT value FROM _config WHERE key = 'schema_version'",
2937 [],
2938 |r| r.get(0),
2939 )
2940 .unwrap()
2941 }
2942
2943 fn open_with_faults(
2946 path: &str,
2947 gsi_insert: Option<u32>,
2948 item_size: Option<u32>,
2949 ) -> Result<Storage> {
2950 let mut storage = Storage {
2951 conn: Connection::open(path)?,
2952 metadata_cache: RefCell::new(HashMap::new()),
2953 gsi_insert_fault: RefCell::new(gsi_insert),
2954 item_size_fault: RefCell::new(item_size),
2955 clock: Arc::new(SystemClock),
2956 };
2957 storage.initialize()?;
2958 Ok(storage)
2959 }
2960
2961 #[test]
2962 fn an_interrupted_gsi_rebuild_leaves_the_database_as_it_was() {
2963 let (_dir, path) = v9_snapshot_path();
2964 write_v9_database(&path, 0);
2965 let before = {
2966 let conn = Connection::open(&path).unwrap();
2967 (gsi_rows(&conn, "byKey"), gsi_rows(&conn, "byTwoHash"))
2968 };
2969
2970 assert!(open_with_faults(&path, Some(0), None).is_err());
2973
2974 let conn = Connection::open(&path).unwrap();
2975 assert_eq!(recorded_version(&conn), "9");
2976 assert_eq!(
2977 (gsi_rows(&conn, "byKey"), gsi_rows(&conn, "byTwoHash")),
2978 before,
2979 "an interrupted rebuild must leave every index as it found it"
2980 );
2981 drop(conn);
2982
2983 let storage = Storage::new(&path).unwrap();
2985 assert_eq!(recorded_version(storage.conn()), SCHEMA_VERSION.to_string());
2986 }
2987
2988 #[test]
2989 fn an_interrupted_size_recompute_leaves_the_database_as_it_was() {
2990 let (_dir, path) = v9_snapshot_path();
2991 {
2992 let db = crate::Database::new(&path).unwrap();
2993 db.create_table(
2994 serde_json::from_value(serde_json::json!({
2995 "TableName": "Vec",
2996 "BillingMode": "PAY_PER_REQUEST",
2997 "AttributeDefinitions": [{"AttributeName": "pk", "AttributeType": "S"}],
2998 "KeySchema": [{"AttributeName": "pk", "KeyType": "HASH"}],
2999 "VectorIndexes": [{
3000 "IndexName": "vix",
3001 "VectorAttribute": {"AttributeName": "e"},
3002 "Dimensions": 3,
3003 "DistanceFunction": "COSINE",
3004 "Projection": {"ProjectionType": "KEYS_ONLY"}
3005 }]
3006 }))
3007 .unwrap(),
3008 )
3009 .unwrap();
3010 for pk in ["a", "b", "c"] {
3011 db.put_item(
3012 serde_json::from_value(serde_json::json!({
3013 "TableName": "Vec",
3014 "Item": {
3015 "pk": {"S": pk},
3016 "e": {"L": [{"N": "0.125"}, {"N": "0.125"}, {"N": "0.125"}]}
3017 }
3018 }))
3019 .unwrap(),
3020 )
3021 .unwrap();
3022 }
3023 }
3024 let sizes = |conn: &Connection| -> Vec<(String, i64)> {
3026 let mut stmt = conn
3027 .prepare("SELECT pk, item_size FROM \"Vec\" ORDER BY pk")
3028 .unwrap();
3029 stmt.query_map([], |r| Ok((r.get(0)?, r.get(1)?)))
3030 .unwrap()
3031 .collect::<rusqlite::Result<_>>()
3032 .unwrap()
3033 };
3034 let before = {
3035 let conn = Connection::open(&path).unwrap();
3036 conn.execute_batch(
3037 "UPDATE \"Vec\" SET item_size = item_size + 7;
3038 UPDATE _config SET value = '10' WHERE key = 'schema_version';",
3039 )
3040 .unwrap();
3041 sizes(&conn)
3042 };
3043
3044 assert!(open_with_faults(&path, None, Some(1)).is_err());
3046
3047 let conn = Connection::open(&path).unwrap();
3048 assert_eq!(recorded_version(&conn), "10");
3049 assert_eq!(
3050 sizes(&conn),
3051 before,
3052 "an interrupted recompute must leave every stored size as it found it"
3053 );
3054 }
3055
3056 fn vec_size(conn: &Connection) -> i64 {
3057 conn.query_row("SELECT item_size FROM \"Vec\" WHERE pk = 'S:v'", [], |r| {
3058 r.get(0)
3059 })
3060 .unwrap()
3061 }
3062
3063 #[test]
3064 fn a_second_open_runs_no_migration() {
3065 let (_dir, path) = v9_snapshot_path();
3066 write_v9_database(&path, 0);
3067 drop(Storage::new(&path).unwrap());
3068 let storage = open_with_faults(&path, Some(0), Some(0)).unwrap();
3071 assert_eq!(recorded_version(storage.conn()), SCHEMA_VERSION.to_string());
3072 }
3073
3074 #[test]
3075 fn a_recompute_interrupted_after_the_rebuild_leaves_version_10() {
3076 let (_dir, path) = v9_snapshot_path();
3077 write_v9_database(&path, 0);
3078 let stale_size = vec_size(&Connection::open(&path).unwrap());
3079
3080 assert!(open_with_faults(&path, None, Some(0)).is_err());
3083
3084 let conn = Connection::open(&path).unwrap();
3085 assert_eq!(
3086 recorded_version(&conn),
3087 "10",
3088 "the rebuild's step is kept and the recompute's is not"
3089 );
3090 assert_eq!(vec_size(&conn), stale_size, "the recompute is rolled back");
3091 let composite = crate::actions::gsi::composite_key_string(&[
3092 &AttributeValue::S("m".into()),
3093 &AttributeValue::S("2".into()),
3094 ])
3095 .unwrap();
3096 assert!(
3097 gsi_rows(&conn, "byKey")
3098 .iter()
3099 .any(|[_, gsi_sk, _, _, _]| *gsi_sk == composite),
3100 "the rebuilt index is kept"
3101 );
3102 drop(conn);
3103
3104 let storage = open_with_faults(&path, Some(0), None).unwrap();
3106 assert_eq!(recorded_version(storage.conn()), SCHEMA_VERSION.to_string());
3107 assert_eq!(vec_size(storage.conn()), stale_size - 7);
3108 }
3109
3110 #[test]
3111 fn the_rebuild_keeps_single_attribute_indexes_and_keys_every_hash_attribute() {
3112 let (_dir, path) = v9_snapshot_path();
3113 write_v9_database(&path, 0);
3114 let single_before = gsi_rows(&Connection::open(&path).unwrap(), "bySingle");
3115
3116 let storage = Storage::new(&path).unwrap();
3117 assert_eq!(
3118 gsi_rows(storage.conn(), "bySingle"),
3119 single_before,
3120 "a single-attribute index is stored the same way and is left alone"
3121 );
3122 let two_hash = crate::actions::gsi::composite_key_string(&[
3123 &AttributeValue::S("x".into()),
3124 &AttributeValue::S("y".into()),
3125 ])
3126 .unwrap();
3127 let keys: Vec<(String, String)> = gsi_rows(storage.conn(), "byTwoHash")
3128 .into_iter()
3129 .map(|[gsi_pk, _, table_pk, _, _]| (gsi_pk, table_pk))
3130 .collect();
3131 assert_eq!(
3132 keys,
3133 ["S:1", "S:2", "S:3"]
3134 .map(|pk| (two_hash.clone(), pk.to_string()))
3135 .to_vec()
3136 );
3137 }
3138
3139 #[test]
3140 fn the_rebuild_runs_past_the_first_backfill_batch() {
3141 let tmp = tempfile::NamedTempFile::new().unwrap();
3142 let path = tmp.path().to_str().unwrap().to_string();
3143 write_v9_database(&path, 1_005);
3145
3146 let storage = Storage::new(&path).unwrap();
3147 assert_eq!(gsi_rows(storage.conn(), "byKey").len(), 2 + 1_005);
3150 assert_eq!(gsi_rows(storage.conn(), "byTwoHash").len(), 3 + 1_005);
3151 }
3152
3153 #[test]
3154 fn a_restored_snapshot_from_an_earlier_version_is_brought_up_to_date() {
3155 let (_dir, path) = v9_snapshot_path();
3156 write_v9_database(&path, 0);
3157
3158 let mut storage = Storage::memory().unwrap();
3159 storage.restore_from(&path).unwrap();
3160
3161 assert_eq!(recorded_version(storage.conn()), SCHEMA_VERSION.to_string());
3162 let sort_keys: Vec<String> = gsi_rows(storage.conn(), "byKey")
3163 .into_iter()
3164 .map(|[_, gsi_sk, _, _, _]| gsi_sk)
3165 .collect();
3166 let composite = |b: &str| {
3167 crate::actions::gsi::composite_key_string(&[
3168 &AttributeValue::S("m".into()),
3169 &AttributeValue::S(b.into()),
3170 ])
3171 .unwrap()
3172 };
3173 assert_eq!(sort_keys, vec![composite("2"), composite("1")]);
3174 }
3175
3176 fn mark_live(storage: &Storage) {
3179 storage
3180 .conn()
3181 .execute_batch(
3182 "CREATE TABLE live_marker (v TEXT); INSERT INTO live_marker VALUES ('live');",
3183 )
3184 .unwrap();
3185 }
3186
3187 fn still_live(storage: &Storage) -> bool {
3188 storage
3189 .conn()
3190 .query_row("SELECT v FROM live_marker", [], |r| r.get::<_, String>(0))
3191 .map(|v| v == "live")
3192 .unwrap_or(false)
3193 }
3194
3195 #[test]
3196 fn restoring_from_a_missing_file_errors_and_leaves_the_database_alone() {
3197 let dir = tempfile::tempdir().unwrap();
3198 let missing = dir.path().join("no-such-snapshot.db");
3199 let missing = missing.to_str().unwrap();
3200 let mut storage = Storage::memory().unwrap();
3201 mark_live(&storage);
3202
3203 assert!(storage.restore_from(missing).is_err());
3204 assert!(
3205 !std::path::Path::new(missing).exists(),
3206 "a mistyped path must not be created"
3207 );
3208 assert!(still_live(&storage), "the database must be left as it was");
3209 }
3210
3211 #[test]
3212 fn a_restore_whose_migration_fails_leaves_the_database_alone() {
3213 let (_dir, path) = v9_snapshot_path();
3214 write_v9_database(&path, 0);
3215
3216 let mut storage = Storage::memory().unwrap();
3217 mark_live(&storage);
3218 storage.fail_gsi_insert_after(0);
3220 let err = storage.restore_from(&path).unwrap_err().to_string();
3221 assert!(
3222 err.contains("injected GSI insert failure"),
3223 "the restore must fail in the migration, not elsewhere: {err}"
3224 );
3225
3226 assert!(still_live(&storage), "the database must be left as it was");
3227 let legacy: i64 = storage
3228 .conn()
3229 .query_row(
3230 "SELECT count(*) FROM sqlite_master WHERE name = 'Legacy'",
3231 [],
3232 |r| r.get(0),
3233 )
3234 .unwrap();
3235 assert_eq!(legacy, 0, "none of the snapshot may be left behind");
3236 assert_eq!(recorded_version(storage.conn()), SCHEMA_VERSION.to_string());
3237 }
3238
3239 #[test]
3240 fn restoring_from_something_that_is_not_a_snapshot_errors_and_leaves_the_database_alone() {
3241 let dir = tempfile::tempdir().unwrap();
3242 let empty = dir.path().join("zero-bytes.db");
3243 std::fs::write(&empty, b"").unwrap();
3244 let foreign = dir.path().join("foreign.db");
3245 Connection::open(&foreign)
3246 .unwrap()
3247 .execute_batch("CREATE TABLE notes (body TEXT); INSERT INTO notes VALUES ('x');")
3248 .unwrap();
3249
3250 for source in [
3251 empty.to_str().unwrap(),
3252 foreign.to_str().unwrap(),
3253 ":memory:",
3254 ] {
3255 let mut storage = Storage::memory().unwrap();
3256 mark_live(&storage);
3257 let err = storage.restore_from(source).unwrap_err().to_string();
3258 assert!(
3259 err.contains("not a Dynoxide snapshot"),
3260 "{source}: refused for the wrong reason: {err}"
3261 );
3262 assert!(
3263 still_live(&storage),
3264 "{source}: the database must be left as it was"
3265 );
3266 }
3267 }
3268
3269 #[test]
3270 fn a_backup_gives_up_on_a_source_that_stays_locked() {
3271 let dir = tempfile::tempdir().unwrap();
3272 let path = dir.path().join("locked.db");
3273 let path = path.to_str().unwrap().to_string();
3274 {
3275 let conn = Connection::open(&path).unwrap();
3276 conn.execute_batch(
3277 "PRAGMA journal_mode = DELETE; CREATE TABLE t (v); INSERT INTO t VALUES (1);",
3278 )
3279 .unwrap();
3280 }
3281 let holder = Connection::open(&path).unwrap();
3283 holder
3284 .execute_batch("BEGIN EXCLUSIVE; INSERT INTO t VALUES (2);")
3285 .unwrap();
3286
3287 let (done, outcome) = std::sync::mpsc::channel();
3290 let copier_path = path.clone();
3291 std::thread::spawn(move || {
3292 let source = Connection::open(&copier_path).unwrap();
3293 let mut dest = Connection::open_in_memory().unwrap();
3294 let result = rusqlite::backup::Backup::new(&source, &mut dest)
3295 .map_err(DynoxideError::from)
3296 .and_then(|backup| run_backup(&backup));
3297 let _ = done.send(result.map_err(|e| e.to_string()));
3298 });
3299
3300 let result = outcome
3304 .recv_timeout(std::time::Duration::from_secs(15))
3305 .expect("a backup of a locked source must give up within its patience");
3306 let err = result.expect_err("the source stayed locked, so the copy must fail");
3307 assert!(err.contains("locked") || err.contains("busy"), "{err}");
3308 drop(holder);
3309 }
3310
3311 fn briefly_locked_source(hold: std::time::Duration) -> (tempfile::TempDir, String) {
3313 let dir = tempfile::tempdir().unwrap();
3314 let path = dir.path().join("briefly-locked.db");
3315 let path = path.to_str().unwrap().to_string();
3316 {
3317 let conn = Connection::open(&path).unwrap();
3318 conn.execute_batch(
3319 "PRAGMA journal_mode = DELETE; CREATE TABLE t (v); INSERT INTO t VALUES (1);",
3320 )
3321 .unwrap();
3322 }
3323 let (locked, is_locked) = std::sync::mpsc::channel();
3324 let holder_path = path.clone();
3325 std::thread::spawn(move || {
3326 let holder = Connection::open(&holder_path).unwrap();
3327 holder
3328 .execute_batch("BEGIN EXCLUSIVE; INSERT INTO t VALUES (2);")
3329 .unwrap();
3330 let _ = locked.send(());
3331 std::thread::sleep(hold);
3332 holder.execute_batch("COMMIT").unwrap();
3333 });
3334 is_locked.recv().unwrap();
3335 (dir, path)
3336 }
3337
3338 fn copy_rows(source: &Connection) -> Result<i64> {
3339 let mut dest = Connection::open_in_memory().unwrap();
3340 {
3341 let backup = rusqlite::backup::Backup::new(source, &mut dest)?;
3342 run_backup(&backup)?;
3343 }
3344 Ok(dest.query_row("SELECT COUNT(*) FROM t", [], |row| row.get(0))?)
3345 }
3346
3347 #[test]
3348 fn a_backup_waits_out_a_short_lock() {
3349 let (_dir, path) = briefly_locked_source(std::time::Duration::from_millis(50));
3350 let source = Connection::open(&path).unwrap();
3352 assert_eq!(
3353 copy_rows(&source).unwrap(),
3354 2,
3355 "the copy sees the committed row"
3356 );
3357 }
3358
3359 #[test]
3360 fn a_backup_retries_a_lock_that_clears() {
3361 let (_dir, path) = briefly_locked_source(std::time::Duration::from_millis(50));
3362 let source = Connection::open(&path).unwrap();
3364 source.busy_timeout(std::time::Duration::ZERO).unwrap();
3365 assert_eq!(
3366 copy_rows(&source).unwrap(),
3367 2,
3368 "the copy sees the committed row"
3369 );
3370 }
3371
3372 #[test]
3373 fn a_backup_watch_times_a_busy_spell_from_the_step_that_found_it() {
3374 let patience = std::time::Duration::from_secs(5);
3375 let t0 = std::time::Instant::now();
3376 let at = |ms: u64| t0 + std::time::Duration::from_millis(ms);
3377 let mut watch = BackupWatch::default();
3378 assert_eq!(
3380 watch.observe(BackupStep::Busy, at(0), at(4_900), patience),
3381 BackupNext::Pause
3382 );
3383 assert_eq!(
3385 watch.observe(BackupStep::Busy, at(4_910), at(5_000), patience),
3386 BackupNext::GiveUpLocked
3387 );
3388 let mut watch = BackupWatch::default();
3390 assert_eq!(
3391 watch.observe(BackupStep::Busy, at(0), at(4_000), patience),
3392 BackupNext::Pause
3393 );
3394 assert_eq!(
3395 watch.observe(
3396 BackupStep::More { remaining: 10 },
3397 at(4_010),
3398 at(4_020),
3399 patience
3400 ),
3401 BackupNext::Step
3402 );
3403 assert_eq!(
3404 watch.observe(BackupStep::Busy, at(4_030), at(8_000), patience),
3405 BackupNext::Pause
3406 );
3407 assert_eq!(
3408 watch.observe(BackupStep::Done, at(8_010), at(8_020), patience),
3409 BackupNext::Finish
3410 );
3411 }
3412
3413 #[test]
3414 fn a_backup_watch_gives_up_on_a_copy_that_keeps_starting_over() {
3415 let patience = std::time::Duration::from_secs(5);
3416 let t = std::time::Instant::now();
3417 let mut watch = BackupWatch::default();
3418 assert_eq!(
3419 watch.observe(BackupStep::More { remaining: 100 }, t, t, patience),
3420 BackupNext::Step
3421 );
3422 assert_eq!(
3424 watch.observe(BackupStep::More { remaining: 50 }, t, t, patience),
3425 BackupNext::Step
3426 );
3427 for _ in 0..BACKUP_RESTART_LIMIT {
3430 assert_eq!(
3431 watch.observe(BackupStep::More { remaining: 100 }, t, t, patience),
3432 BackupNext::Pause
3433 );
3434 assert_eq!(
3435 watch.observe(BackupStep::More { remaining: 50 }, t, t, patience),
3436 BackupNext::Step
3437 );
3438 }
3439 assert_eq!(
3440 watch.observe(BackupStep::More { remaining: 100 }, t, t, patience),
3441 BackupNext::GiveUpRestarting
3442 );
3443 }
3444
3445 #[test]
3446 fn a_backup_watch_counts_a_copy_that_starts_over_at_the_same_place() {
3447 let patience = std::time::Duration::from_secs(5);
3452 let t = std::time::Instant::now();
3453 let mut watch = BackupWatch::default();
3454 assert_eq!(
3455 watch.observe(BackupStep::More { remaining: 302 }, t, t, patience),
3456 BackupNext::Step
3457 );
3458 for restart in 1..=BACKUP_RESTART_LIMIT {
3461 assert_eq!(
3462 watch.observe(BackupStep::More { remaining: 302 }, t, t, patience),
3463 BackupNext::Pause,
3464 "restart {restart}"
3465 );
3466 }
3467 assert_eq!(
3468 watch.observe(BackupStep::More { remaining: 302 }, t, t, patience),
3469 BackupNext::GiveUpRestarting
3470 );
3471 }
3472
3473 #[test]
3474 fn a_backup_of_a_source_changed_between_steps_gives_up() {
3475 let dir = tempfile::tempdir().unwrap();
3476 let path = dir.path().join("changing.db");
3477 let path = path.to_str().unwrap().to_string();
3478 let writer = Connection::open(&path).unwrap();
3479 writer
3480 .execute_batch(
3481 "PRAGMA journal_mode = DELETE; CREATE TABLE t (v); \
3482 WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < 400) \
3483 INSERT INTO t SELECT zeroblob(3000) FROM n;",
3484 )
3485 .unwrap();
3486 let source = Connection::open(&path).unwrap();
3487 let mut dest = Connection::open_in_memory().unwrap();
3488 let backup = rusqlite::backup::Backup::new(&source, &mut dest).unwrap();
3489 let patience = std::time::Duration::from_secs(5);
3490 let mut watch = BackupWatch::default();
3491 let mut outcome = None;
3492 for step in 0..(2 * BACKUP_RESTART_LIMIT + 10) {
3497 let t = std::time::Instant::now();
3498 match watch.observe(backup_step(&backup).unwrap(), t, t, patience) {
3499 BackupNext::Step | BackupNext::Pause => {}
3500 other => {
3501 outcome = Some((step, other));
3502 break;
3503 }
3504 }
3505 writer
3506 .execute("UPDATE t SET v = randomblob(3000) WHERE rowid = 1", [])
3507 .unwrap();
3508 }
3509 let (step, next) = outcome.expect("a copy that keeps starting over must give up");
3510 assert_eq!(next, BackupNext::GiveUpRestarting);
3511 assert!(
3512 step <= BACKUP_RESTART_LIMIT + 1,
3513 "gave up after {step} steps, not at the restart limit"
3514 );
3515 }
3516
3517 #[test]
3518 fn a_restore_leaves_its_source_file_unchanged() {
3519 let (_dir, path) = v9_snapshot_path();
3520 write_v9_database(&path, 0);
3521 let before = std::fs::read(&path).unwrap();
3522
3523 let mut storage = Storage::memory().unwrap();
3524 storage.restore_from(&path).unwrap();
3525 drop(storage);
3526
3527 assert_eq!(std::fs::read(&path).unwrap(), before);
3528 assert_eq!(recorded_version(&Connection::open(&path).unwrap()), "9");
3529 }
3530
3531 fn busy_timeout_ms(conn: &Connection) -> i64 {
3532 conn.query_row("PRAGMA busy_timeout", [], |r| r.get(0))
3533 .unwrap()
3534 }
3535
3536 fn open_and_after(storage: &Storage) -> (Option<i64>, i64) {
3539 (
3540 OPENED_WITH_BUSY_TIMEOUT_MS.take(),
3541 busy_timeout_ms(storage.conn()),
3542 )
3543 }
3544
3545 #[test]
3550 fn a_database_file_waits_thirty_seconds_for_a_lock_while_opening_and_five_after() {
3551 let (_dir, path) = v9_snapshot_path();
3552 let storage = Storage::new(&path).unwrap();
3553 assert_eq!(open_and_after(&storage), (Some(30_000), 5_000));
3554 let memory = Storage::memory().unwrap();
3557 assert_eq!(open_and_after(&memory), (Some(5_000), 5_000));
3558 }
3559
3560 #[cfg(feature = "_has-encryption")]
3561 #[test]
3562 fn an_encrypted_database_file_waits_thirty_seconds_for_a_lock_while_opening_and_five_after() {
3563 const KEY: &str = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
3564 let (_dir, path) = v9_snapshot_path();
3565 let storage = Storage::new_encrypted(&path, KEY).unwrap();
3566 assert_eq!(open_and_after(&storage), (Some(30_000), 5_000));
3567 }
3568
3569 #[test]
3575 fn a_second_open_waits_for_a_migration_another_holds_then_finds_it_done() {
3576 let (_dir, path) = v9_snapshot_path();
3577 write_v9_database(&path, 0);
3578 let stale_index = gsi_rows(&Connection::open(&path).unwrap(), "byKey");
3579
3580 let first = Connection::open(&path).unwrap();
3585 first.execute_batch("BEGIN IMMEDIATE").unwrap();
3586 first
3587 .execute_batch(&format!(
3588 "UPDATE \"Vec\" SET item_size = 1;
3589 UPDATE _config SET value = '{SCHEMA_VERSION}' WHERE key = 'schema_version';"
3590 ))
3591 .unwrap();
3592
3593 let (started, starting) = std::sync::mpsc::channel();
3594 let second_path = path.clone();
3595 let second = std::thread::spawn(move || {
3596 started.send(()).unwrap();
3597 let storage = Storage::new(&second_path)?;
3598 Ok::<_, DynoxideError>((
3599 recorded_version(storage.conn()),
3600 vec_size(storage.conn()),
3601 gsi_rows(storage.conn(), "byKey"),
3602 ))
3603 });
3604 starting.recv().unwrap();
3605 std::thread::sleep(std::time::Duration::from_millis(200));
3606 first.execute_batch("COMMIT").unwrap();
3607
3608 let (version, size, index) = second.join().unwrap().unwrap();
3609 assert_eq!(version, SCHEMA_VERSION.to_string());
3610 assert_eq!(
3611 size, 1,
3612 "the second open must not recompute the sizes again"
3613 );
3614 assert_eq!(
3615 index, stale_index,
3616 "the second open must not rebuild the index the first left"
3617 );
3618 }
3619
3620 #[test]
3625 fn a_database_that_records_no_version_is_brought_up_to_date() {
3626 let (_dir, path) = v9_snapshot_path();
3627 write_v9_database(&path, 0);
3628 let table_id = |conn: &Connection| -> String {
3629 conn.query_row(
3630 "SELECT table_id FROM _tables WHERE table_name = 'Legacy'",
3631 [],
3632 |r| r.get(0),
3633 )
3634 .unwrap()
3635 };
3636 let (stale_size, id_before) = {
3637 let conn = Connection::open(&path).unwrap();
3638 conn.execute("DELETE FROM _config WHERE key = 'schema_version'", [])
3639 .unwrap();
3640 (vec_size(&conn), table_id(&conn))
3641 };
3642
3643 let storage = Storage::new(&path).unwrap();
3644 assert_eq!(recorded_version(storage.conn()), SCHEMA_VERSION.to_string());
3645 assert_eq!(
3646 vec_size(storage.conn()),
3647 stale_size - 7,
3648 "vector table sizes are recomputed"
3649 );
3650 let composite = crate::actions::gsi::composite_key_string(&[
3651 &AttributeValue::S("m".into()),
3652 &AttributeValue::S("2".into()),
3653 ])
3654 .unwrap();
3655 assert!(
3656 gsi_rows(storage.conn(), "byKey")
3657 .iter()
3658 .any(|[_, gsi_sk, _, _, _]| *gsi_sk == composite),
3659 "multi-attribute indexes are rebuilt"
3660 );
3661 assert_eq!(table_id(storage.conn()), id_before, "a table keeps its id");
3662 drop(storage);
3663
3664 let db = crate::Database::new(&path).unwrap();
3665 let item = db
3666 .get_item(
3667 serde_json::from_value(serde_json::json!({
3668 "TableName": "Legacy",
3669 "Key": {"pk": {"S": "1"}}
3670 }))
3671 .unwrap(),
3672 )
3673 .unwrap()
3674 .item;
3675 assert!(item.is_some(), "the items read back");
3676 }
3677
3678 fn stamp_newer(conn: &Connection) {
3682 conn.execute(
3683 "UPDATE _config SET value = ?1 WHERE key = 'schema_version'",
3684 [(SCHEMA_VERSION + 1).to_string()],
3685 )
3686 .unwrap();
3687 conn.query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |_| Ok(()))
3688 .unwrap();
3689 }
3690
3691 fn write_newer_database(path: &str) {
3693 drop(crate::Database::new(path).unwrap());
3694 stamp_newer(&Connection::open(path).unwrap());
3695 }
3696
3697 fn assert_refused_as_newer(err: DynoxideError) {
3698 let message = err.to_string();
3699 assert_eq!(
3700 message,
3701 crate::storage_backend::schema::newer_than_this_build(SCHEMA_VERSION + 1)
3702 );
3703 assert!(
3704 message.contains(&format!("schema version {}", SCHEMA_VERSION + 1))
3705 && message.contains(&format!("up to schema version {SCHEMA_VERSION}")),
3706 "{message}"
3707 );
3708 }
3709
3710 #[test]
3711 fn a_database_a_newer_build_wrote_is_refused_and_left_as_it_was() {
3712 let (_dir, path) = v9_snapshot_path();
3713 write_newer_database(&path);
3714 let before = std::fs::read(&path).unwrap();
3715
3716 assert_refused_as_newer(Storage::new(&path).map(drop).unwrap_err());
3717 assert_refused_as_newer(crate::Database::new(&path).map(drop).unwrap_err());
3718
3719 assert_eq!(std::fs::read(&path).unwrap(), before);
3720 assert_eq!(
3721 recorded_version(&Connection::open(&path).unwrap()),
3722 (SCHEMA_VERSION + 1).to_string()
3723 );
3724 }
3725
3726 #[cfg(feature = "_has-encryption")]
3727 #[test]
3728 fn an_encrypted_database_a_newer_build_wrote_is_refused_and_left_as_it_was() {
3729 const KEY: &str = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
3730 let (_dir, path) = v9_snapshot_path();
3731 drop(crate::Database::new_encrypted(&path, KEY).unwrap());
3732 {
3733 let conn = Connection::open(&path).unwrap();
3734 conn.pragma_update(None, "key", format!("x'{KEY}'"))
3735 .unwrap();
3736 stamp_newer(&conn);
3737 }
3738 let before = std::fs::read(&path).unwrap();
3739
3740 assert_refused_as_newer(
3741 crate::Database::new_encrypted(&path, KEY)
3742 .map(drop)
3743 .unwrap_err(),
3744 );
3745
3746 assert_eq!(std::fs::read(&path).unwrap(), before);
3747 }
3748
3749 #[test]
3750 fn a_restore_refuses_a_snapshot_a_newer_build_wrote() {
3751 let (_dir, path) = v9_snapshot_path();
3752 write_newer_database(&path);
3753 let before = std::fs::read(&path).unwrap();
3754
3755 let mut storage = Storage::memory().unwrap();
3756 mark_live(&storage);
3757 assert_refused_as_newer(storage.restore_from(&path).unwrap_err());
3758 assert!(still_live(&storage), "the database must be left as it was");
3759 assert_eq!(recorded_version(storage.conn()), SCHEMA_VERSION.to_string());
3760 drop(storage);
3761
3762 assert_eq!(std::fs::read(&path).unwrap(), before);
3763 }
3764
3765 #[test]
3766 fn test_wal_mode_enabled() {
3767 let storage = test_storage();
3768 let mode: String = storage
3769 .conn()
3770 .query_row("PRAGMA journal_mode", [], |row| row.get(0))
3771 .unwrap();
3772 assert!(mode == "wal" || mode == "memory", "Got mode: {mode}");
3774 }
3775
3776 #[test]
3782 fn fnv1a_hash_matches_known_vectors() {
3783 let storage = test_storage();
3784 let cases: [(&str, i64); 6] = [
3785 ("", 2166136261),
3786 ("a", 3826002220),
3787 ("u#1", 2199603432),
3788 ("artist#42", 2385694177),
3789 ("café", 2821410889),
3790 ("tenant#9007199254740993", 2022216178),
3791 ];
3792 for (input, expected) in cases {
3793 let got: i64 = storage
3794 .conn()
3795 .query_row("SELECT fnv1a_hash(?1)", [input], |row| row.get(0))
3796 .unwrap();
3797 assert_eq!(got, expected, "fnv1a_hash({input:?})");
3798 }
3799 }
3800
3801 #[test]
3802 fn test_table_metadata_crud() {
3803 let storage = test_storage();
3804
3805 assert!(!storage.table_exists("TestTable").unwrap());
3807 assert!(storage.list_table_names().unwrap().is_empty());
3808
3809 storage
3811 .insert_table_metadata(&CreateTableMetadata {
3812 table_name: "TestTable",
3813 key_schema: r#"[{"AttributeName":"pk","KeyType":"HASH"}]"#,
3814 attribute_definitions: r#"[{"AttributeName":"pk","AttributeType":"S"}]"#,
3815 created_at: 1000000,
3816 ..Default::default()
3817 })
3818 .unwrap();
3819
3820 assert!(storage.table_exists("TestTable").unwrap());
3821 assert_eq!(storage.list_table_names().unwrap(), vec!["TestTable"]);
3822
3823 let meta = storage.get_table_metadata("TestTable").unwrap().unwrap();
3825 assert_eq!(meta.table_name, "TestTable");
3826 assert_eq!(meta.table_status, "ACTIVE");
3827 assert_eq!(meta.created_at, 1000000);
3828
3829 assert!(storage.delete_table_metadata("TestTable").unwrap());
3831 assert!(!storage.table_exists("TestTable").unwrap());
3832 }
3833
3834 #[test]
3835 fn test_create_and_drop_data_table() {
3836 let storage = test_storage();
3837 storage.create_data_table("MyTable").unwrap();
3838
3839 storage
3841 .put_item("MyTable", "pk1", "", r#"{"pk":{"S":"pk1"}}"#, 10)
3842 .unwrap();
3843
3844 let item = storage.get_item("MyTable", "pk1", "").unwrap();
3845 assert!(item.is_some());
3846
3847 storage.drop_data_table("MyTable").unwrap();
3848 }
3849
3850 #[test]
3851 fn test_item_crud() {
3852 let storage = test_storage();
3853 storage.create_data_table("Items").unwrap();
3854
3855 let old = storage
3857 .put_item(
3858 "Items",
3859 "user#1",
3860 "profile",
3861 r#"{"name":{"S":"Alice"}}"#,
3862 20,
3863 )
3864 .unwrap();
3865 assert!(old.is_none()); let item = storage.get_item("Items", "user#1", "profile").unwrap();
3869 assert_eq!(item.unwrap(), r#"{"name":{"S":"Alice"}}"#);
3870
3871 let old = storage
3873 .put_item("Items", "user#1", "profile", r#"{"name":{"S":"Bob"}}"#, 18)
3874 .unwrap();
3875 assert_eq!(old.unwrap(), r#"{"name":{"S":"Alice"}}"#);
3876
3877 let deleted = storage.delete_item("Items", "user#1", "profile").unwrap();
3879 assert_eq!(deleted.unwrap(), r#"{"name":{"S":"Bob"}}"#);
3880
3881 assert!(
3883 storage
3884 .get_item("Items", "user#1", "profile")
3885 .unwrap()
3886 .is_none()
3887 );
3888 }
3889
3890 #[test]
3891 fn test_query_items() {
3892 let storage = test_storage();
3893 storage.create_data_table("Orders").unwrap();
3894
3895 for i in 1..=5 {
3897 let sk = format!("order#{i:03}");
3898 let json = format!(r#"{{"id":{{"N":"{i}"}}}}"#);
3899 storage
3900 .put_item("Orders", "user#1", &sk, &json, 10)
3901 .unwrap();
3902 }
3903
3904 let results = storage
3906 .query_items(
3907 "Orders",
3908 "user#1",
3909 &QueryParams {
3910 forward: true,
3911 ..Default::default()
3912 },
3913 )
3914 .unwrap();
3915 assert_eq!(results.len(), 5);
3916 assert_eq!(results[0].1, "order#001"); let results = storage
3920 .query_items(
3921 "Orders",
3922 "user#1",
3923 &QueryParams {
3924 forward: true,
3925 limit: Some(2),
3926 ..Default::default()
3927 },
3928 )
3929 .unwrap();
3930 assert_eq!(results.len(), 2);
3931
3932 let results = storage
3934 .query_items(
3935 "Orders",
3936 "user#1",
3937 &QueryParams {
3938 forward: false,
3939 limit: Some(2),
3940 ..Default::default()
3941 },
3942 )
3943 .unwrap();
3944 assert_eq!(results.len(), 2);
3945 assert_eq!(results[0].1, "order#005"); }
3947
3948 #[test]
3949 fn test_scan_items() {
3950 let storage = test_storage();
3951 storage.create_data_table("ScanTest").unwrap();
3952
3953 storage.put_item("ScanTest", "a", "1", r#"{}"#, 2).unwrap();
3954 storage.put_item("ScanTest", "b", "2", r#"{}"#, 2).unwrap();
3955 storage.put_item("ScanTest", "c", "3", r#"{}"#, 2).unwrap();
3956
3957 let results = storage.scan_items("ScanTest", &Default::default()).unwrap();
3958 assert_eq!(results.len(), 3);
3959
3960 let results = storage
3962 .scan_items(
3963 "ScanTest",
3964 &ScanParams {
3965 limit: Some(2),
3966 ..Default::default()
3967 },
3968 )
3969 .unwrap();
3970 assert_eq!(results.len(), 2);
3971
3972 let results = storage
3974 .scan_items(
3975 "ScanTest",
3976 &ScanParams {
3977 limit: Some(2),
3978 exclusive_start_pk: Some("a"),
3979 exclusive_start_sk: Some("1"),
3980 ..Default::default()
3981 },
3982 )
3983 .unwrap();
3984 assert_eq!(results.len(), 2);
3985 assert_eq!(results[0].0, "b"); }
3987
3988 #[test]
3989 fn test_count_items() {
3990 let storage = test_storage();
3991 storage.create_data_table("CountTest").unwrap();
3992
3993 assert_eq!(storage.count_items("CountTest").unwrap(), 0);
3994
3995 storage.put_item("CountTest", "a", "", r#"{}"#, 2).unwrap();
3996 storage.put_item("CountTest", "b", "", r#"{}"#, 2).unwrap();
3997
3998 assert_eq!(storage.count_items("CountTest").unwrap(), 2);
3999 }
4000
4001 #[test]
4002 fn test_gsi_table_lifecycle() {
4003 let storage = test_storage();
4004 storage.create_gsi_table("Orders", "ByDate").unwrap();
4005
4006 let gsi_name = "Orders::gsi::ByDate";
4008 let sql = format!(
4009 "INSERT INTO \"{}\" (gsi_pk, gsi_sk, table_pk, table_sk, item_json) VALUES (?1, ?2, ?3, ?4, ?5)",
4010 gsi_name.replace('"', "\"\"")
4011 );
4012 storage
4013 .conn()
4014 .execute(
4015 &sql,
4016 params!["2024-01-01", "001", "user#1", "order#001", r#"{}"#],
4017 )
4018 .unwrap();
4019
4020 storage.drop_gsi_table("Orders", "ByDate").unwrap();
4021 }
4022
4023 #[test]
4024 fn test_nonexistent_table_metadata() {
4025 let storage = test_storage();
4026 assert!(storage.get_table_metadata("Nonexistent").unwrap().is_none());
4027 assert!(!storage.delete_table_metadata("Nonexistent").unwrap());
4028 }
4029
4030 #[test]
4031 fn test_metadata_cache_hit() {
4032 let storage = test_storage();
4033 storage
4034 .insert_table_metadata(&CreateTableMetadata {
4035 table_name: "CacheTest",
4036 key_schema: r#"[{"AttributeName":"pk","KeyType":"HASH"}]"#,
4037 attribute_definitions: r#"[{"AttributeName":"pk","AttributeType":"S"}]"#,
4038 created_at: 1000000,
4039 ..Default::default()
4040 })
4041 .unwrap();
4042
4043 let meta1 = storage.get_table_metadata("CacheTest").unwrap().unwrap();
4045 assert_eq!(meta1.table_name, "CacheTest");
4046
4047 let meta2 = storage.get_table_metadata("CacheTest").unwrap().unwrap();
4049 assert_eq!(meta2.table_name, "CacheTest");
4050 assert_eq!(meta1.created_at, meta2.created_at);
4051
4052 assert!(storage.metadata_cache.borrow().contains_key("CacheTest"));
4054 }
4055
4056 #[test]
4057 fn test_metadata_cache_invalidated_on_delete() {
4058 let storage = test_storage();
4059 storage
4060 .insert_table_metadata(&CreateTableMetadata {
4061 table_name: "DelCache",
4062 key_schema: r#"[{"AttributeName":"pk","KeyType":"HASH"}]"#,
4063 attribute_definitions: r#"[{"AttributeName":"pk","AttributeType":"S"}]"#,
4064 created_at: 1000000,
4065 ..Default::default()
4066 })
4067 .unwrap();
4068
4069 storage.get_table_metadata("DelCache").unwrap();
4071 assert!(storage.metadata_cache.borrow().contains_key("DelCache"));
4072
4073 storage.delete_table_metadata("DelCache").unwrap();
4075 assert!(!storage.metadata_cache.borrow().contains_key("DelCache"));
4076 }
4077
4078 #[test]
4079 fn test_metadata_cache_invalidated_on_stream_enable() {
4080 let storage = test_storage();
4081 storage
4082 .insert_table_metadata(&CreateTableMetadata {
4083 table_name: "StreamCache",
4084 key_schema: r#"[{"AttributeName":"pk","KeyType":"HASH"}]"#,
4085 attribute_definitions: r#"[{"AttributeName":"pk","AttributeType":"S"}]"#,
4086 created_at: 1000000,
4087 ..Default::default()
4088 })
4089 .unwrap();
4090
4091 let meta = storage.get_table_metadata("StreamCache").unwrap().unwrap();
4093 assert!(!meta.stream_enabled);
4094
4095 storage
4097 .enable_stream("StreamCache", "NEW_AND_OLD_IMAGES", "2024-01-01T00:00:00")
4098 .unwrap();
4099 assert!(!storage.metadata_cache.borrow().contains_key("StreamCache"));
4100
4101 let meta = storage.get_table_metadata("StreamCache").unwrap().unwrap();
4103 assert!(meta.stream_enabled);
4104 }
4105
4106 #[test]
4107 fn test_metadata_cache_invalidated_on_ttl_update() {
4108 let storage = test_storage();
4109 storage
4110 .insert_table_metadata(&CreateTableMetadata {
4111 table_name: "TtlCache",
4112 key_schema: r#"[{"AttributeName":"pk","KeyType":"HASH"}]"#,
4113 attribute_definitions: r#"[{"AttributeName":"pk","AttributeType":"S"}]"#,
4114 created_at: 1000000,
4115 ..Default::default()
4116 })
4117 .unwrap();
4118
4119 let meta = storage.get_table_metadata("TtlCache").unwrap().unwrap();
4121 assert!(!meta.ttl_enabled);
4122
4123 storage
4125 .update_ttl_config("TtlCache", Some("expires_at"), true)
4126 .unwrap();
4127 assert!(!storage.metadata_cache.borrow().contains_key("TtlCache"));
4128
4129 let meta = storage.get_table_metadata("TtlCache").unwrap().unwrap();
4131 assert!(meta.ttl_enabled);
4132 assert_eq!(meta.ttl_attribute, Some("expires_at".to_string()));
4133 }
4134
4135 #[test]
4136 fn test_num_to_buffer_zero() {
4137 assert_eq!(num_to_buffer("0"), vec![0x80]);
4139 assert_eq!(num_to_buffer("-0"), vec![0x80]);
4140 }
4141
4142 #[test]
4143 fn test_hash_prefix_string_keys() {
4144 let h1 = compute_hash_prefix(&AttributeValue::S("3635".into()));
4147 let h2 = compute_hash_prefix(&AttributeValue::S("228".into()));
4148 let h3 = compute_hash_prefix(&AttributeValue::S("1668".into()));
4149 let h4 = compute_hash_prefix(&AttributeValue::S("3435".into()));
4150
4151 assert_eq!(
4154 hash_bucket(&h1),
4155 0,
4156 "3635 should be bucket 0, got hash {h1}"
4157 );
4158 assert_eq!(hash_bucket(&h2), 0, "228 should be bucket 0, got hash {h2}");
4159
4160 assert_eq!(
4162 hash_bucket(&h3),
4163 1,
4164 "1668 should be bucket 1, got hash {h3}"
4165 );
4166
4167 assert_eq!(
4169 hash_bucket(&h4),
4170 4,
4171 "3435 should be bucket 4, got hash {h4}"
4172 );
4173 }
4174
4175 #[test]
4176 fn test_hash_prefix_number_keys() {
4177 let h1 = compute_hash_prefix(&AttributeValue::N("251".into()));
4180 assert_eq!(hash_bucket(&h1), 1, "251 should be bucket 1, got hash {h1}");
4181
4182 let h2 = compute_hash_prefix(&AttributeValue::N("2388".into()));
4184 assert_eq!(
4185 hash_bucket(&h2),
4186 4095,
4187 "2388 should be bucket 4095, got hash {h2}"
4188 );
4189 }
4190
4191 #[test]
4192 fn test_hash_in_segment() {
4193 assert!(hash_in_segment("000000", 0, 4096));
4195 assert!(!hash_in_segment("000000", 1, 4096));
4196
4197 assert!(hash_in_segment("001000", 1, 4096));
4199 assert!(!hash_in_segment("001000", 0, 4096));
4200
4201 assert!(hash_in_segment("fff000", 4095, 4096));
4203 assert!(!hash_in_segment("fff000", 0, 4096));
4204
4205 assert!(hash_in_segment("000000", 0, 2));
4207 assert!(hash_in_segment("7ff000", 0, 2));
4208 assert!(hash_in_segment("800000", 1, 2));
4209 assert!(hash_in_segment("fff000", 1, 2));
4210 }
4211}