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 let has_event_id: bool =
539 self.conn
540 .query_row(sql_builders::STREAM_RECORDS_HAVE_EVENT_ID, [], |row| {
541 row.get(0)
542 })?;
543 if !has_event_id {
544 self.conn
545 .execute_batch(sql_builders::ADD_STREAM_EVENT_ID_COLUMN)?;
546 self.conn
547 .execute_batch(sql_builders::BACKFILL_STREAM_EVENT_IDS)?;
548 }
549 if new_database {
550 let (sql, params) = sql_builders::init_schema_version(SCHEMA_VERSION);
551 self.conn
552 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
553 }
554 let recorded = self.recorded_schema_version()?;
555 transaction.commit()?;
556 Ok(recorded)
557 }
558
559 fn migrate_v1_to_v2(&self) -> Result<()> {
561 let mut stmt = self.conn.prepare("SELECT table_name FROM _tables")?;
562 let table_names: Vec<String> = stmt
563 .query_map([], |row| row.get(0))?
564 .collect::<std::result::Result<Vec<_>, _>>()?;
565
566 for table_name in &table_names {
567 let escaped = format!("\"{}\"", table_name.replace('"', "\"\""));
568 let _ = self.conn.execute(
569 &format!("ALTER TABLE {escaped} ADD COLUMN cached_at REAL"),
570 [],
571 );
572 }
573
574 self.conn.execute(
575 "INSERT OR REPLACE INTO _config (key, value) VALUES ('schema_version', '2')",
576 [],
577 )?;
578
579 Ok(())
580 }
581
582 fn migrate_v2_to_v3(&self) -> Result<()> {
584 let _ = self
585 .conn
586 .execute("ALTER TABLE _tables ADD COLUMN tags TEXT", []);
587
588 self.conn.execute(
589 "INSERT OR REPLACE INTO _config (key, value) VALUES ('schema_version', '3')",
590 [],
591 )?;
592
593 Ok(())
594 }
595
596 fn migrate_v3_to_v4(&self) -> Result<()> {
598 let _ = self
599 .conn
600 .execute("ALTER TABLE _tables ADD COLUMN sse_specification TEXT", []);
601 let _ = self
602 .conn
603 .execute("ALTER TABLE _tables ADD COLUMN table_class TEXT", []);
604 let _ = self.conn.execute(
605 "ALTER TABLE _tables ADD COLUMN deletion_protection_enabled INTEGER DEFAULT 0",
606 [],
607 );
608
609 self.conn.execute(
610 "INSERT OR REPLACE INTO _config (key, value) VALUES ('schema_version', '4')",
611 [],
612 )?;
613
614 Ok(())
615 }
616
617 fn migrate_v4_to_v5(&self) -> Result<()> {
619 let mut stmt = self
620 .conn
621 .prepare("SELECT table_name, gsi_definitions, lsi_definitions FROM _tables")?;
622 let tables: Vec<(String, Option<String>, Option<String>)> = stmt
623 .query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))?
624 .collect::<std::result::Result<Vec<_>, _>>()?;
625
626 for (table_name, gsi_json, lsi_json) in &tables {
627 if let Some(json) = gsi_json
628 && let Ok(gsis) = serde_json::from_str::<Vec<serde_json::Value>>(json)
629 {
630 for gsi in &gsis {
631 if let Some(idx) = gsi.get("IndexName").and_then(|v| v.as_str()) {
632 let gsi_table = escape_table_name(&format!("{table_name}::gsi::{idx}"));
633 let idx_name =
634 escape_table_name(&format!("{table_name}::gsi::{idx}::base_key"));
635 let _ = self.conn.execute_batch(&format!(
636 "CREATE INDEX IF NOT EXISTS \"{idx_name}\" ON \"{gsi_table}\" (table_pk, table_sk)"
637 ));
638 }
639 }
640 }
641 if let Some(json) = lsi_json
642 && let Ok(lsis) = serde_json::from_str::<Vec<serde_json::Value>>(json)
643 {
644 for lsi in &lsis {
645 if let Some(idx) = lsi.get("IndexName").and_then(|v| v.as_str()) {
646 let lsi_table = escape_table_name(&format!("{table_name}::lsi::{idx}"));
647 let idx_name =
648 escape_table_name(&format!("{table_name}::lsi::{idx}::base_key"));
649 let _ = self.conn.execute_batch(&format!(
650 "CREATE INDEX IF NOT EXISTS \"{idx_name}\" ON \"{lsi_table}\" (base_pk, base_sk)"
651 ));
652 }
653 }
654 }
655 }
656
657 self.conn.execute(
658 "INSERT OR REPLACE INTO _config (key, value) VALUES ('schema_version', '5')",
659 [],
660 )?;
661
662 Ok(())
663 }
664
665 fn migrate_v5_to_v6(&self) -> Result<()> {
667 let mut stmt = self.conn.prepare("SELECT table_name FROM _tables")?;
668 let table_names: Vec<String> = stmt
669 .query_map([], |row| row.get(0))?
670 .collect::<std::result::Result<Vec<_>, _>>()?;
671
672 for table_name in &table_names {
673 let escaped = escape_table_name(table_name);
674 let _ = self.conn.execute(
675 &format!(
676 "ALTER TABLE \"{escaped}\" ADD COLUMN hash_prefix TEXT NOT NULL DEFAULT ''"
677 ),
678 [],
679 );
680 }
681
682 self.conn.execute(
683 "INSERT OR REPLACE INTO _config (key, value) VALUES ('schema_version', '6')",
684 [],
685 )?;
686
687 Ok(())
688 }
689
690 fn migrate_v6_to_v7(&self) -> Result<()> {
692 let _ = self.conn.execute(
693 "ALTER TABLE _tables ADD COLUMN on_demand_throughput TEXT",
694 [],
695 );
696
697 self.conn.execute(
698 "INSERT OR REPLACE INTO _config (key, value) VALUES ('schema_version', '7')",
699 [],
700 )?;
701
702 Ok(())
703 }
704
705 fn migrate_v7_to_v8(&self) -> Result<()> {
716 let _ = self
717 .conn
718 .execute("ALTER TABLE _tables ADD COLUMN table_id TEXT", []);
719
720 let names: Vec<String> = {
721 let mut stmt = self
722 .conn
723 .prepare("SELECT table_name FROM _tables WHERE table_id IS NULL")?;
724 let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
725 rows.collect::<rusqlite::Result<Vec<String>>>()?
726 };
727 for name in names {
728 let id = uuid::Uuid::new_v4().to_string();
729 self.conn.execute(
730 "UPDATE _tables SET table_id = ?1 WHERE table_name = ?2",
731 params![id, name],
732 )?;
733 }
734
735 self.conn.execute(
736 "INSERT OR REPLACE INTO _config (key, value) VALUES ('schema_version', '8')",
737 [],
738 )?;
739
740 Ok(())
741 }
742
743 fn migrate_v8_to_v9(&self) -> Result<()> {
751 if let Err(e) = self
752 .conn
753 .execute(sql_builders::ADD_VECTOR_INDEX_DEFINITIONS_COLUMN, [])
754 && !sql_builders::is_duplicate_column_error(&e.to_string())
755 {
756 return Err(e.into());
757 }
758
759 self.conn.execute(
760 "INSERT OR REPLACE INTO _config (key, value) VALUES ('schema_version', '9')",
761 [],
762 )?;
763
764 Ok(())
765 }
766
767 pub fn conn(&self) -> &Connection {
769 &self.conn
770 }
771
772 pub fn conn_mut(&mut self) -> &mut Connection {
774 &mut self.conn
775 }
776
777 pub fn insert_table_metadata(&self, m: &CreateTableMetadata) -> Result<()> {
783 let table_name = m.table_name;
784 let (sql, params) = sql_builders::insert_table_metadata(m);
785 self.conn
786 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
787 self.metadata_cache.borrow_mut().remove(table_name);
788 Ok(())
789 }
790
791 pub fn get_table_metadata(&self, table_name: &str) -> Result<Option<TableMetadata>> {
797 if let Some(cached) = self.metadata_cache.borrow().get(table_name) {
799 return Ok(Some(cached.clone()));
800 }
801
802 let (sql, params) = sql_builders::get_table_metadata(table_name);
803 let mut stmt = self.conn.prepare(&sql)?;
804
805 let result = stmt.query_row(rusqlite::params_from_iter(params.iter()), row_to_metadata);
806
807 match result {
808 Ok(meta) => {
809 self.metadata_cache
810 .borrow_mut()
811 .insert(table_name.to_string(), meta.clone());
812 Ok(Some(meta))
813 }
814 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
815 Err(e) => Err(DynoxideError::from(e)),
816 }
817 }
818
819 pub fn delete_table_metadata(&self, table_name: &str) -> Result<bool> {
821 let (sql, params) = sql_builders::delete_table_metadata(table_name);
822 let affected = self
823 .conn
824 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
825 self.metadata_cache.borrow_mut().remove(table_name);
826 Ok(affected > 0)
827 }
828
829 pub fn update_table_metadata(
831 &self,
832 table_name: &str,
833 attribute_definitions: &str,
834 gsi_definitions: Option<&str>,
835 ) -> Result<()> {
836 let (sql, params) =
837 sql_builders::update_table_metadata(table_name, attribute_definitions, gsi_definitions);
838 self.conn
839 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
840 self.metadata_cache.borrow_mut().remove(table_name);
841 Ok(())
842 }
843
844 pub fn update_vector_index_definitions(
846 &self,
847 table_name: &str,
848 vector_index_definitions: Option<&str>,
849 ) -> Result<()> {
850 let (sql, params) =
851 sql_builders::update_vector_index_definitions(table_name, vector_index_definitions);
852 self.conn
853 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
854 self.metadata_cache.borrow_mut().remove(table_name);
855 Ok(())
856 }
857
858 pub fn update_provisioned_throughput(
860 &self,
861 table_name: &str,
862 provisioned_throughput: &str,
863 ) -> Result<()> {
864 let (sql, params) =
865 sql_builders::update_provisioned_throughput(table_name, provisioned_throughput);
866 self.conn
867 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
868 self.metadata_cache.borrow_mut().remove(table_name);
869 Ok(())
870 }
871
872 pub fn clear_provisioned_throughput(&self, table_name: &str) -> Result<()> {
874 let (sql, params) = sql_builders::clear_provisioned_throughput(table_name);
875 self.conn
876 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
877 self.metadata_cache.borrow_mut().remove(table_name);
878 Ok(())
879 }
880
881 pub fn clear_on_demand_throughput(&self, table_name: &str) -> Result<()> {
883 let (sql, params) = sql_builders::clear_on_demand_throughput(table_name);
884 self.conn
885 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
886 self.metadata_cache.borrow_mut().remove(table_name);
887 Ok(())
888 }
889
890 pub fn update_billing_mode(&self, table_name: &str, billing_mode: &str) -> Result<()> {
892 let (sql, params) = sql_builders::update_billing_mode(table_name, billing_mode);
893 self.conn
894 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
895 self.metadata_cache.borrow_mut().remove(table_name);
896 Ok(())
897 }
898
899 pub fn update_table_class(&self, table_name: &str, table_class: &str) -> Result<()> {
901 let (sql, params) = sql_builders::update_table_class(table_name, table_class);
902 self.conn
903 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
904 self.metadata_cache.borrow_mut().remove(table_name);
905 Ok(())
906 }
907
908 pub fn update_on_demand_throughput(
910 &self,
911 table_name: &str,
912 on_demand_throughput: &str,
913 ) -> Result<()> {
914 let (sql, params) =
915 sql_builders::update_on_demand_throughput(table_name, on_demand_throughput);
916 self.conn
917 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
918 self.metadata_cache.borrow_mut().remove(table_name);
919 Ok(())
920 }
921
922 pub fn get_tags(&self, table_name: &str) -> Result<Vec<crate::types::Tag>> {
928 let tags_json: Option<String> = self.conn.query_row(
929 "SELECT tags FROM _tables WHERE table_name = ?1",
930 params![table_name],
931 |row| row.get(0),
932 )?;
933
934 match tags_json {
935 Some(json) => serde_json::from_str(&json)
936 .map_err(|e| DynoxideError::InternalServerError(format!("Bad tags JSON: {e}"))),
937 None => Ok(Vec::new()),
938 }
939 }
940
941 pub fn set_tags(&self, table_name: &str, new_tags: &[crate::types::Tag]) -> Result<()> {
943 use std::collections::BTreeMap;
944
945 let existing = self.get_tags(table_name)?;
946 let mut tag_map: BTreeMap<String, String> =
947 existing.into_iter().map(|t| (t.key, t.value)).collect();
948
949 for tag in new_tags {
950 tag_map.insert(tag.key.clone(), tag.value.clone());
951 }
952
953 if tag_map.len() > 50 {
954 return Err(DynoxideError::ValidationException(
955 "One or more parameter values were invalid: \
956 Too many tags: tag limit is 50"
957 .to_string(),
958 ));
959 }
960
961 let merged: Vec<crate::types::Tag> = tag_map
962 .into_iter()
963 .map(|(k, v)| crate::types::Tag { key: k, value: v })
964 .collect();
965
966 let json = serde_json::to_string(&merged)
967 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
968
969 self.conn.execute(
970 "UPDATE _tables SET tags = ?1 WHERE table_name = ?2",
971 params![json, table_name],
972 )?;
973 Ok(())
974 }
975
976 pub fn update_deletion_protection(&self, table_name: &str, enabled: bool) -> Result<()> {
978 let (sql, params) = sql_builders::update_deletion_protection(table_name, enabled);
979 self.conn
980 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
981 self.metadata_cache.borrow_mut().remove(table_name);
982 Ok(())
983 }
984
985 pub fn remove_tags(&self, table_name: &str, keys: &[String]) -> Result<()> {
987 let mut tags = self.get_tags(table_name)?;
988 tags.retain(|t| !keys.contains(&t.key));
989
990 let json = if tags.is_empty() {
991 None
992 } else {
993 Some(
994 serde_json::to_string(&tags)
995 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?,
996 )
997 };
998
999 self.conn.execute(
1000 "UPDATE _tables SET tags = ?1 WHERE table_name = ?2",
1001 params![json, table_name],
1002 )?;
1003 Ok(())
1004 }
1005
1006 pub fn list_table_names(&self) -> Result<Vec<String>> {
1008 let (sql, params) = sql_builders::list_table_names();
1009 let mut stmt = self.conn.prepare(&sql)?;
1010 let names = stmt
1011 .query_map(rusqlite::params_from_iter(params.iter()), |row| row.get(0))?
1012 .collect::<std::result::Result<Vec<String>, _>>()?;
1013 Ok(names)
1014 }
1015
1016 pub fn table_exists(&self, table_name: &str) -> Result<bool> {
1018 let (sql, params) = sql_builders::table_exists(table_name);
1019 let count: i32 =
1020 self.conn
1021 .query_row(&sql, rusqlite::params_from_iter(params.iter()), |row| {
1022 row.get(0)
1023 })?;
1024 Ok(count > 0)
1025 }
1026
1027 #[allow(dead_code)]
1029 pub(crate) fn invalidate_metadata_cache(&self, table_name: &str) {
1030 self.metadata_cache.borrow_mut().remove(table_name);
1031 }
1032
1033 pub fn create_data_table(&self, table_name: &str) -> Result<()> {
1039 let (sql, params) = sql_builders::create_data_table(table_name);
1040 self.conn
1041 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
1042 Ok(())
1043 }
1044
1045 pub fn drop_data_table(&self, table_name: &str) -> Result<()> {
1047 let (sql, params) = sql_builders::drop_data_table(table_name);
1048 self.conn
1049 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
1050 Ok(())
1051 }
1052
1053 pub fn create_gsi_table(&self, table_name: &str, index_name: &str) -> Result<()> {
1055 let (sql, _) = sql_builders::create_gsi_table(table_name, index_name);
1056 self.conn.execute_batch(&sql)?;
1057 Ok(())
1058 }
1059
1060 pub fn drop_gsi_table(&self, table_name: &str, index_name: &str) -> Result<()> {
1062 let (sql, params) = sql_builders::drop_gsi_table(table_name, index_name);
1063 self.conn
1064 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
1065 Ok(())
1066 }
1067
1068 #[allow(clippy::too_many_arguments)]
1074 pub fn insert_gsi_item(
1075 &self,
1076 table_name: &str,
1077 index_name: &str,
1078 gsi_pk: &str,
1079 gsi_sk: &str,
1080 table_pk: &str,
1081 table_sk: &str,
1082 item_json: &str,
1083 ) -> Result<()> {
1084 let sql = sql_builders::gsi_insert_sql(table_name, index_name);
1085 let params = sql_builders::gsi_insert_params(gsi_pk, gsi_sk, table_pk, table_sk, item_json);
1086 self.conn
1087 .prepare_cached(&sql)?
1088 .execute(rusqlite::params_from_iter(params.iter()))?;
1089 Ok(())
1090 }
1091
1092 pub fn insert_gsi_items(
1096 &self,
1097 table_name: &str,
1098 index_name: &str,
1099 rows: &[crate::storage_backend::GsiItemRow],
1100 ) -> Result<()> {
1101 let sql = sql_builders::gsi_insert_sql(table_name, index_name);
1102 let mut stmt = self.conn.prepare_cached(&sql)?;
1103 for row in rows {
1104 let params = sql_builders::gsi_insert_params(
1105 &row.gsi_pk,
1106 &row.gsi_sk,
1107 &row.table_pk,
1108 &row.table_sk,
1109 &row.item_json,
1110 );
1111 stmt.execute(rusqlite::params_from_iter(params.iter()))?;
1112 }
1113 Ok(())
1114 }
1115
1116 pub fn delete_gsi_item(
1118 &self,
1119 table_name: &str,
1120 index_name: &str,
1121 table_pk: &str,
1122 table_sk: &str,
1123 ) -> Result<()> {
1124 let (sql, params) =
1125 sql_builders::delete_gsi_item(table_name, index_name, table_pk, table_sk);
1126 self.conn
1127 .prepare_cached(&sql)?
1128 .execute(rusqlite::params_from_iter(params.iter()))?;
1129 Ok(())
1130 }
1131
1132 pub fn query_gsi_items(
1134 &self,
1135 table_name: &str,
1136 index_name: &str,
1137 gsi_pk: &str,
1138 params: &QueryParams,
1139 ) -> Result<Vec<(String, String, String)>> {
1140 let (sql, params_vec) =
1141 sql_builders::query_gsi_items(table_name, index_name, gsi_pk, params);
1142 let mut stmt = self.conn.prepare(&sql)?;
1143 let rows = stmt
1144 .query_map(rusqlite::params_from_iter(params_vec.iter()), |row| {
1145 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
1146 })?
1147 .collect::<std::result::Result<Vec<_>, _>>()?;
1148
1149 Ok(rows)
1150 }
1151
1152 pub fn scan_gsi_items(
1154 &self,
1155 table_name: &str,
1156 index_name: &str,
1157 params: &ScanParams,
1158 ) -> Result<Vec<(String, String, String)>> {
1159 let (sql, params_vec) = sql_builders::scan_gsi_items(table_name, index_name, params);
1160 let mut stmt = self.conn.prepare(&sql)?;
1161 let rows = stmt
1162 .query_map(rusqlite::params_from_iter(params_vec.iter()), |row| {
1163 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
1164 })?
1165 .collect::<std::result::Result<Vec<_>, _>>()?;
1166
1167 Ok(rows)
1168 }
1169
1170 pub fn create_lsi_table(&self, table_name: &str, index_name: &str) -> Result<()> {
1176 let (sql, _) = sql_builders::create_lsi_table(table_name, index_name);
1177 self.conn.execute_batch(&sql)?;
1178 Ok(())
1179 }
1180
1181 pub fn create_vector_table(&self, table_name: &str, index_name: &str) -> Result<()> {
1183 let (sql, _) = sql_builders::create_vector_table(table_name, index_name);
1184 self.conn.execute_batch(&sql)?;
1185 Ok(())
1186 }
1187
1188 pub fn insert_vector_items(
1191 &self,
1192 table_name: &str,
1193 index_name: &str,
1194 rows: &[crate::storage_backend::VectorItemRow],
1195 ) -> Result<()> {
1196 let sql = sql_builders::vector_insert_sql(table_name, index_name);
1197 let mut stmt = self.conn.prepare_cached(&sql)?;
1198 for row in rows {
1199 let params = sql_builders::vector_insert_params(
1200 &row.table_pk,
1201 &row.table_sk,
1202 &row.hash_value,
1203 &row.vector_json,
1204 &row.filter_json,
1205 &row.item_json,
1206 row.entry_bytes,
1207 );
1208 stmt.execute(rusqlite::params_from_iter(params.iter()))?;
1209 }
1210 Ok(())
1211 }
1212
1213 pub fn delete_vector_item(
1216 &self,
1217 table_name: &str,
1218 index_name: &str,
1219 table_pk: &str,
1220 table_sk: &str,
1221 ) -> Result<()> {
1222 let (sql, params) =
1223 sql_builders::delete_vector_item(table_name, index_name, table_pk, table_sk);
1224 self.conn
1225 .prepare_cached(&sql)?
1226 .execute(rusqlite::params_from_iter(params.iter()))?;
1227 Ok(())
1228 }
1229
1230 pub fn query_vector_candidates(
1234 &self,
1235 table_name: &str,
1236 index_name: &str,
1237 hash_value: Option<&str>,
1238 ) -> Result<Vec<crate::storage_backend::VectorCandidateRow>> {
1239 let (sql, params) =
1240 sql_builders::query_vector_candidates(table_name, index_name, hash_value);
1241 let mut stmt = self.conn.prepare(&sql)?;
1242 let rows = stmt
1243 .query_map(rusqlite::params_from_iter(params.iter()), |row| {
1244 Ok(crate::storage_backend::VectorCandidateRow {
1245 table_pk: row.get(0)?,
1246 table_sk: row.get(1)?,
1247 vector_json: row.get(2)?,
1248 filter_json: row.get(3)?,
1249 entry_bytes: row.get(4)?,
1250 })
1251 })?
1252 .collect::<std::result::Result<Vec<_>, _>>()?;
1253 Ok(rows)
1254 }
1255
1256 pub fn vector_items_for_keys(
1258 &self,
1259 table_name: &str,
1260 index_name: &str,
1261 keys: &[(String, String)],
1262 ) -> Result<Vec<(String, String, String)>> {
1263 if keys.is_empty() {
1264 return Ok(Vec::new());
1265 }
1266 let (sql, params) = sql_builders::vector_items_for_keys(table_name, index_name, keys);
1267 let mut stmt = self.conn.prepare(&sql)?;
1268 let rows = stmt
1269 .query_map(rusqlite::params_from_iter(params.iter()), |row| {
1270 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
1271 })?
1272 .collect::<std::result::Result<Vec<_>, _>>()?;
1273 Ok(rows)
1274 }
1275
1276 pub fn drop_vector_table(&self, table_name: &str, index_name: &str) -> Result<()> {
1278 let (sql, params) = sql_builders::drop_vector_table(table_name, index_name);
1279 self.conn
1280 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
1281 Ok(())
1282 }
1283
1284 pub fn drop_lsi_table(&self, table_name: &str, index_name: &str) -> Result<()> {
1286 let (sql, params) = sql_builders::drop_lsi_table(table_name, index_name);
1287 self.conn
1288 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
1289 Ok(())
1290 }
1291
1292 #[allow(clippy::too_many_arguments)]
1298 pub fn insert_lsi_item(
1299 &self,
1300 table_name: &str,
1301 index_name: &str,
1302 pk: &str,
1303 sk: &str,
1304 base_pk: &str,
1305 base_sk: &str,
1306 item_json: &str,
1307 ) -> Result<()> {
1308 let sql = sql_builders::lsi_insert_sql(table_name, index_name);
1309 let params = sql_builders::lsi_insert_params(pk, sk, base_pk, base_sk, item_json);
1310 self.conn
1311 .prepare_cached(&sql)?
1312 .execute(rusqlite::params_from_iter(params.iter()))?;
1313 Ok(())
1314 }
1315
1316 pub fn delete_lsi_item(
1318 &self,
1319 table_name: &str,
1320 index_name: &str,
1321 base_pk: &str,
1322 base_sk: &str,
1323 ) -> Result<()> {
1324 let (sql, params) = sql_builders::delete_lsi_item(table_name, index_name, base_pk, base_sk);
1325 self.conn
1326 .prepare_cached(&sql)?
1327 .execute(rusqlite::params_from_iter(params.iter()))?;
1328 Ok(())
1329 }
1330
1331 pub fn query_lsi_items(
1333 &self,
1334 table_name: &str,
1335 index_name: &str,
1336 pk: &str,
1337 params: &QueryParams,
1338 ) -> Result<Vec<(String, String, String)>> {
1339 let (sql, params_vec) = sql_builders::query_lsi_items(table_name, index_name, pk, params);
1340 let mut stmt = self.conn.prepare(&sql)?;
1341 let rows = stmt
1342 .query_map(rusqlite::params_from_iter(params_vec.iter()), |row| {
1343 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
1344 })?
1345 .collect::<std::result::Result<Vec<_>, _>>()?;
1346
1347 Ok(rows)
1348 }
1349
1350 pub fn scan_lsi_items(
1352 &self,
1353 table_name: &str,
1354 index_name: &str,
1355 params: &ScanParams,
1356 ) -> Result<Vec<(String, String, String)>> {
1357 let (sql, params_vec) = sql_builders::scan_lsi_items(table_name, index_name, params);
1358 let mut stmt = self.conn.prepare(&sql)?;
1359 let rows = stmt
1360 .query_map(rusqlite::params_from_iter(params_vec.iter()), |row| {
1361 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
1362 })?
1363 .collect::<std::result::Result<Vec<_>, _>>()?;
1364
1365 Ok(rows)
1366 }
1367
1368 pub fn begin_transaction(&self) -> Result<()> {
1374 self.conn.execute_batch(sql_builders::BEGIN)?;
1375 Ok(())
1376 }
1377
1378 pub fn commit(&self) -> Result<()> {
1380 self.conn.execute_batch(sql_builders::COMMIT)?;
1381 Ok(())
1382 }
1383
1384 pub fn rollback(&self) -> Result<()> {
1386 self.conn.execute_batch(sql_builders::ROLLBACK)?;
1387 Ok(())
1388 }
1389
1390 pub(crate) fn recorded_schema_version(&self) -> Result<Option<u32>> {
1393 use rusqlite::OptionalExtension;
1394 let value: Option<String> = self
1395 .conn
1396 .query_row(sql_builders::GET_SCHEMA_VERSION, [], |r| r.get(0))
1397 .optional()?;
1398 Ok(crate::storage_backend::schema::parse_recorded(
1399 value.as_deref(),
1400 ))
1401 }
1402
1403 pub(crate) fn record_schema_version(&self, version: u32) -> Result<()> {
1406 let (sql, params) = sql_builders::set_schema_version(version);
1407 self.conn
1408 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
1409 Ok(())
1410 }
1411
1412 pub fn enable_bulk_loading(&self) -> Result<()> {
1418 self.conn.execute_batch(
1419 "PRAGMA synchronous = OFF;
1420 PRAGMA cache_size = -64000;
1421 PRAGMA temp_store = MEMORY;
1422 PRAGMA mmap_size = 268435456;",
1423 )?;
1424 Ok(())
1425 }
1426
1427 pub fn disable_bulk_loading(&self) -> Result<()> {
1429 self.conn.execute_batch(
1430 "PRAGMA synchronous = NORMAL;
1431 PRAGMA cache_size = -2000;
1432 PRAGMA temp_store = DEFAULT;
1433 PRAGMA mmap_size = 0;",
1434 )?;
1435 Ok(())
1436 }
1437
1438 pub fn put_item(
1444 &self,
1445 table_name: &str,
1446 pk: &str,
1447 sk: &str,
1448 item_json: &str,
1449 item_size: usize,
1450 ) -> Result<Option<String>> {
1451 self.put_item_with_hash(table_name, pk, sk, item_json, item_size, "")
1452 }
1453
1454 pub fn put_item_with_hash(
1456 &self,
1457 table_name: &str,
1458 pk: &str,
1459 sk: &str,
1460 item_json: &str,
1461 item_size: usize,
1462 hash_prefix: &str,
1463 ) -> Result<Option<String>> {
1464 let old_item = self.get_item(table_name, pk, sk)?;
1466
1467 let (sql, params) =
1468 sql_builders::put_item_with_hash(table_name, pk, sk, item_json, item_size, hash_prefix);
1469 self.conn
1470 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
1471
1472 Ok(old_item)
1473 }
1474
1475 pub fn put_base_items(
1480 &self,
1481 table_name: &str,
1482 rows: &[crate::storage_backend::BaseItemRow],
1483 ) -> Result<()> {
1484 let escaped = escape_table_name(table_name);
1485 let sql = format!(
1486 "INSERT OR REPLACE INTO \"{escaped}\" (pk, sk, item_json, item_size, cached_at, hash_prefix) \
1487 VALUES (?1, ?2, ?3, ?4, ?5, ?6)"
1488 );
1489 let mut stmt = self.conn.prepare_cached(&sql)?;
1490 for row in rows {
1491 stmt.execute(params![
1492 row.pk,
1493 row.sk,
1494 row.item_json,
1495 row.item_size as i64,
1496 row.cached_at,
1497 row.hash_prefix
1498 ])?;
1499 }
1500 Ok(())
1501 }
1502
1503 pub fn get_item(&self, table_name: &str, pk: &str, sk: &str) -> Result<Option<String>> {
1505 let (sql, params) = sql_builders::get_item(table_name, pk, sk);
1506 let result = self
1507 .conn
1508 .query_row(&sql, rusqlite::params_from_iter(params.iter()), |row| {
1509 row.get(0)
1510 });
1511
1512 match result {
1513 Ok(json) => Ok(Some(json)),
1514 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
1515 Err(e) => Err(DynoxideError::from(e)),
1516 }
1517 }
1518
1519 pub fn get_partition_size(&self, table_name: &str, pk: &str) -> Result<i64> {
1521 let (sql, params) = sql_builders::get_partition_size(table_name, pk);
1522 let size: i64 =
1523 self.conn
1524 .query_row(&sql, rusqlite::params_from_iter(params.iter()), |row| {
1525 row.get(0)
1526 })?;
1527 Ok(size)
1528 }
1529
1530 pub fn get_lsi_partition_size(
1533 &self,
1534 table_name: &str,
1535 index_name: &str,
1536 pk: &str,
1537 ) -> Result<i64> {
1538 let (sql, params) = sql_builders::get_lsi_partition_size(table_name, index_name, pk);
1539 let size: i64 =
1540 self.conn
1541 .query_row(&sql, rusqlite::params_from_iter(params.iter()), |row| {
1542 row.get(0)
1543 })?;
1544 Ok(size)
1545 }
1546
1547 pub fn delete_item(&self, table_name: &str, pk: &str, sk: &str) -> Result<Option<String>> {
1549 let old_item = self.get_item(table_name, pk, sk)?;
1550
1551 let (sql, params) = sql_builders::delete_item(table_name, pk, sk);
1552 self.conn
1553 .execute(&sql, rusqlite::params_from_iter(params.iter()))?;
1554
1555 Ok(old_item)
1556 }
1557
1558 pub fn query_items(
1563 &self,
1564 table_name: &str,
1565 pk: &str,
1566 params: &QueryParams,
1567 ) -> Result<Vec<(String, String, String)>> {
1568 let (sql, params_vec) = sql_builders::query_items(table_name, pk, params);
1569 let mut stmt = self.conn.prepare(&sql)?;
1570 let rows = stmt
1571 .query_map(rusqlite::params_from_iter(params_vec.iter()), |row| {
1572 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
1573 })?
1574 .collect::<std::result::Result<Vec<_>, _>>()?;
1575
1576 Ok(rows)
1577 }
1578
1579 pub fn scan_items(
1584 &self,
1585 table_name: &str,
1586 params: &ScanParams,
1587 ) -> Result<Vec<(String, String, String)>> {
1588 let (sql, params_vec) = sql_builders::scan_items(table_name, params);
1589 let mut stmt = self.conn.prepare(&sql)?;
1590 let rows = stmt
1591 .query_map(rusqlite::params_from_iter(params_vec.iter()), |row| {
1592 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
1593 })?
1594 .collect::<std::result::Result<Vec<_>, _>>()?;
1595
1596 Ok(rows)
1597 }
1598
1599 pub fn count_items(&self, table_name: &str) -> Result<i64> {
1601 let (sql, params) = sql_builders::count_items(table_name);
1602 let count: i64 =
1603 self.conn
1604 .query_row(&sql, rusqlite::params_from_iter(params.iter()), |row| {
1605 row.get(0)
1606 })?;
1607 Ok(count)
1608 }
1609
1610 pub fn item_count_and_size(&self, table_name: &str) -> Result<(i64, i64)> {
1612 let (sql, params) = sql_builders::item_count_and_size(table_name);
1613 let stats: (i64, i64) =
1614 self.conn
1615 .query_row(&sql, rusqlite::params_from_iter(params.iter()), |row| {
1616 Ok((row.get(0)?, row.get(1)?))
1617 })?;
1618 Ok(stats)
1619 }
1620
1621 pub fn gsi_item_count_and_size(
1624 &self,
1625 table_name: &str,
1626 index_name: &str,
1627 whole_items: bool,
1628 ) -> Result<(i64, i64)> {
1629 if whole_items {
1630 self.count_and_size(sql_builders::gsi_count_and_size_from_base(
1631 table_name, index_name,
1632 ))
1633 } else {
1634 self.count_and_size_entries(sql_builders::gsi_entries_item_json(table_name, index_name))
1635 }
1636 }
1637
1638 pub fn lsi_item_count_and_size(
1641 &self,
1642 table_name: &str,
1643 index_name: &str,
1644 whole_items: bool,
1645 ) -> Result<(i64, i64)> {
1646 if whole_items {
1647 self.count_and_size(sql_builders::lsi_count_and_size_from_base(
1648 table_name, index_name,
1649 ))
1650 } else {
1651 self.count_and_size_entries(sql_builders::lsi_entries_item_json(table_name, index_name))
1652 }
1653 }
1654
1655 pub fn vector_item_count_and_size(
1658 &self,
1659 table_name: &str,
1660 index_name: &str,
1661 ) -> Result<(i64, i64)> {
1662 self.count_and_size(sql_builders::vector_count_and_size(table_name, index_name))
1663 }
1664
1665 pub fn set_item_sizes(
1668 &self,
1669 table_name: &str,
1670 sizes: &[(String, String, usize)],
1671 ) -> Result<()> {
1672 for (pk, sk, size) in sizes {
1673 #[cfg(test)]
1674 Self::count_down(&self.item_size_fault, "injected item size write failure")?;
1675 let (sql, params) = sql_builders::set_item_size(table_name, pk, sk, *size);
1676 self.conn
1677 .prepare_cached(&sql)?
1678 .execute(rusqlite::params_from_iter(params.iter()))?;
1679 }
1680 Ok(())
1681 }
1682
1683 fn count_and_size(
1684 &self,
1685 (sql, params): (String, Vec<sql_builders::SqlParam<'static>>),
1686 ) -> Result<(i64, i64)> {
1687 Ok(self
1688 .conn
1689 .query_row(&sql, rusqlite::params_from_iter(params.iter()), |row| {
1690 Ok((row.get(0)?, row.get(1)?))
1691 })?)
1692 }
1693
1694 fn count_and_size_entries(
1695 &self,
1696 (sql, params): (String, Vec<sql_builders::SqlParam<'static>>),
1697 ) -> Result<(i64, i64)> {
1698 let mut stmt = self.conn.prepare(&sql)?;
1699 let mut rows = stmt.query(rusqlite::params_from_iter(params.iter()))?;
1700 let (mut count, mut bytes) = (0i64, 0i64);
1701 while let Some(row) = rows.next()? {
1702 let json: String = row.get(0)?;
1703 count += 1;
1704 bytes += crate::storage_backend::projected_entry_size(&json)
1705 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
1706 }
1707 Ok((count, bytes))
1708 }
1709
1710 pub fn db_path(&self) -> Option<String> {
1716 self.conn
1717 .path()
1718 .filter(|p| !p.is_empty())
1719 .map(|p| p.to_owned())
1720 }
1721
1722 pub fn db_size_bytes(&self) -> Result<u64> {
1724 let size: i64 = self.conn.query_row(
1725 "SELECT page_count * page_size FROM pragma_page_count(), pragma_page_size()",
1726 [],
1727 |row| row.get(0),
1728 )?;
1729 Ok(size as u64)
1730 }
1731
1732 pub fn table_count(&self) -> Result<usize> {
1734 let count: i64 = self
1735 .conn
1736 .query_row("SELECT COUNT(*) FROM _tables", [], |row| row.get(0))?;
1737 Ok(count as usize)
1738 }
1739
1740 pub fn table_stats(&self) -> Result<Vec<TableStats>> {
1744 let table_names = self.list_table_names()?;
1745 let mut stats = Vec::with_capacity(table_names.len());
1746 for name in table_names {
1747 let sql = format!(
1748 "SELECT COUNT(*), COALESCE(SUM(item_size), 0) FROM \"{}\"",
1749 escape_table_name(&name)
1750 );
1751 let (item_count, size_bytes): (i64, i64) = self
1752 .conn
1753 .query_row(&sql, [], |row| Ok((row.get(0)?, row.get(1)?)))?;
1754 stats.push(TableStats {
1755 table_name: name,
1756 item_count,
1757 size_bytes: size_bytes as u64,
1758 });
1759 }
1760 Ok(stats)
1761 }
1762
1763 pub fn database_info(&self) -> Result<DatabaseInfo> {
1768 let path = self.db_path();
1769 let size_bytes = self.db_size_bytes()?;
1770 let table_count = self.table_count()?;
1771 let stats = self.table_stats()?;
1772
1773 let mut table_details = Vec::with_capacity(stats.len());
1774 for s in stats {
1775 let metadata = self.get_table_metadata(&s.table_name)?;
1776 table_details.push(TableInfoEntry { stats: s, metadata });
1777 }
1778
1779 Ok(DatabaseInfo {
1780 path,
1781 size_bytes,
1782 table_count,
1783 tables: table_details,
1784 })
1785 }
1786
1787 pub fn vacuum_into(&self, path: &str) -> Result<()> {
1794 if path.contains('\0') {
1795 return Err(DynoxideError::ValidationException(
1796 "path contains null byte".to_string(),
1797 ));
1798 }
1799 self.conn
1800 .execute_batch(&format!("VACUUM INTO '{}'", path.replace('\'', "''")))?;
1801 Ok(())
1802 }
1803
1804 pub fn vacuum(&self) -> Result<()> {
1806 self.conn.execute_batch("VACUUM")?;
1807 Ok(())
1808 }
1809
1810 pub fn restore_from(&mut self, path: &str) -> Result<()> {
1819 let source = Connection::open_with_flags(
1823 path,
1824 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY
1825 | rusqlite::OpenFlags::SQLITE_OPEN_URI
1826 | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
1827 )?;
1828 self.restore_from_connection(&source)
1829 }
1830
1831 pub fn backup_to_memory(&self) -> Result<Connection> {
1836 let mut dest = Connection::open_in_memory()?;
1837 {
1838 let backup = rusqlite::backup::Backup::new(&self.conn, &mut dest)?;
1839 run_backup(&backup)?;
1840 }
1841 Ok(dest)
1842 }
1843
1844 pub fn restore_from_connection(&mut self, source: &Connection) -> Result<()> {
1859 ensure_snapshot(source)?;
1860 refuse_newer_schema(source)?;
1861 let staged = self.staged_copy(source)?;
1862 {
1863 let backup = rusqlite::backup::Backup::new(&staged.conn, &mut self.conn)?;
1864 run_backup(&backup)?;
1865 }
1866 self.metadata_cache.borrow_mut().clear();
1867 Ok(())
1868 }
1869
1870 fn staged_copy(&self, source: &Connection) -> Result<Storage> {
1874 let mut staged = Storage {
1875 conn: Connection::open("")?,
1880 metadata_cache: RefCell::new(HashMap::new()),
1881 #[cfg(test)]
1884 gsi_insert_fault: RefCell::new(self.gsi_insert_fault.take()),
1885 #[cfg(test)]
1886 item_size_fault: RefCell::new(self.item_size_fault.take()),
1887 clock: Arc::clone(&self.clock),
1888 };
1889 let copied = match rusqlite::backup::Backup::new(source, &mut staged.conn) {
1890 Ok(backup) => run_backup(&backup),
1891 Err(e) => Err(e.into()),
1892 };
1893 let migrated = copied
1894 .and_then(|()| Self::register_functions(&staged.conn))
1895 .and_then(|()| staged.migrate_to_current());
1896 #[cfg(test)]
1897 {
1898 *self.gsi_insert_fault.borrow_mut() = staged.gsi_insert_fault.take();
1899 *self.item_size_fault.borrow_mut() = staged.item_size_fault.take();
1900 }
1901 migrated.map(|()| staged)
1902 }
1903
1904 pub fn connection_size_bytes(conn: &Connection) -> Result<u64> {
1906 let size: i64 = conn.query_row(
1907 "SELECT page_count * page_size FROM pragma_page_count(), pragma_page_size()",
1908 [],
1909 |row| row.get(0),
1910 )?;
1911 Ok(size as u64)
1912 }
1913
1914 pub fn enable_stream(&self, table_name: &str, view_type: &str, label: &str) -> Result<()> {
1920 self.conn.execute(
1921 "UPDATE _tables SET stream_enabled = 1, stream_view_type = ?1, stream_label = ?2 WHERE table_name = ?3",
1922 params![view_type, label, table_name],
1923 )?;
1924 self.metadata_cache.borrow_mut().remove(table_name);
1925 Ok(())
1926 }
1927
1928 pub fn disable_stream(&self, table_name: &str) -> Result<()> {
1930 self.conn.execute(
1931 "UPDATE _tables SET stream_enabled = 0 WHERE table_name = ?1",
1932 params![table_name],
1933 )?;
1934 self.metadata_cache.borrow_mut().remove(table_name);
1935 Ok(())
1936 }
1937
1938 #[allow(clippy::too_many_arguments)]
1940 pub fn insert_stream_record(
1941 &self,
1942 table_name: &str,
1943 event_name: &str,
1944 keys_json: &str,
1945 new_image: Option<&str>,
1946 old_image: Option<&str>,
1947 sequence_number: &str,
1948 shard_id: &str,
1949 created_at: i64,
1950 ) -> Result<()> {
1951 self.insert_stream_record_with_identity(
1952 table_name,
1953 event_name,
1954 keys_json,
1955 new_image,
1956 old_image,
1957 sequence_number,
1958 shard_id,
1959 created_at,
1960 None,
1961 )
1962 }
1963
1964 #[allow(clippy::too_many_arguments)]
1966 pub fn insert_stream_record_with_identity(
1967 &self,
1968 table_name: &str,
1969 event_name: &str,
1970 keys_json: &str,
1971 new_image: Option<&str>,
1972 old_image: Option<&str>,
1973 sequence_number: &str,
1974 shard_id: &str,
1975 created_at: i64,
1976 user_identity: Option<&str>,
1977 ) -> Result<()> {
1978 let event_id = uuid::Uuid::new_v4().simple().to_string();
1980 self.conn.execute(
1981 "INSERT INTO _stream_records (table_name, event_name, keys_json, new_image, old_image, sequence_number, shard_id, created_at, user_identity, event_id)
1982 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
1983 params![table_name, event_name, keys_json, new_image, old_image, sequence_number, shard_id, created_at, user_identity, event_id],
1984 )?;
1985 Ok(())
1986 }
1987
1988 pub fn next_stream_sequence_number(&self, table_name: &str) -> Result<i64> {
1991 let result: std::result::Result<i64, _> = self.conn.query_row(
1992 "SELECT COALESCE(MAX(CAST(sequence_number AS INTEGER)), 0) + 1 FROM _stream_records WHERE table_name = ?1",
1993 params![table_name],
1994 |row| row.get(0),
1995 );
1996 let next = result.unwrap_or(1);
1997 Ok(next.max(crate::streams::sequence_number_floor(self.clock())))
1998 }
1999
2000 pub fn get_stream_records(
2002 &self,
2003 table_name: &str,
2004 shard_id: &str,
2005 after_sequence: i64,
2006 limit: usize,
2007 ) -> Result<Vec<StreamRecord>> {
2008 Ok(self
2009 .stream_records_with_event_ids(table_name, shard_id, after_sequence, limit)?
2010 .into_iter()
2011 .map(|(record, _)| record)
2012 .collect())
2013 }
2014
2015 pub(crate) fn stream_records_with_event_ids(
2019 &self,
2020 table_name: &str,
2021 shard_id: &str,
2022 after_sequence: i64,
2023 limit: usize,
2024 ) -> Result<Vec<(StreamRecord, Option<String>)>> {
2025 let mut stmt = self.conn.prepare(
2026 "SELECT event_name, keys_json, new_image, old_image, sequence_number, created_at, user_identity, event_id
2027 FROM _stream_records
2028 WHERE table_name = ?1 AND shard_id = ?2 AND CAST(sequence_number AS INTEGER) > ?3
2029 ORDER BY CAST(sequence_number AS INTEGER) ASC
2030 LIMIT ?4",
2031 )?;
2032 let rows = stmt
2033 .query_map(
2034 params![table_name, shard_id, after_sequence, limit as i64],
2035 |row| {
2036 let record = StreamRecord {
2037 event_name: row.get(0)?,
2038 keys_json: row.get(1)?,
2039 new_image: row.get(2)?,
2040 old_image: row.get(3)?,
2041 sequence_number: row.get(4)?,
2042 created_at: row.get(5)?,
2043 user_identity: row.get(6)?,
2044 };
2045 Ok((record, row.get(7)?))
2046 },
2047 )?
2048 .collect::<std::result::Result<Vec<_>, _>>()?;
2049 Ok(rows)
2050 }
2051
2052 pub fn list_stream_enabled_tables(&self) -> Result<Vec<TableMetadata>> {
2054 let sql = format!(
2055 "SELECT {} FROM _tables WHERE stream_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 update_ttl_config(
2071 &self,
2072 table_name: &str,
2073 attribute_name: Option<&str>,
2074 enabled: bool,
2075 ) -> Result<()> {
2076 self.conn.execute(
2077 "UPDATE _tables SET ttl_attribute = ?1, ttl_enabled = ?2 WHERE table_name = ?3",
2078 params![attribute_name, enabled as i32, table_name],
2079 )?;
2080 self.metadata_cache.borrow_mut().remove(table_name);
2081 Ok(())
2082 }
2083
2084 pub fn list_ttl_enabled_tables(&self) -> Result<Vec<TableMetadata>> {
2086 let sql = format!(
2087 "SELECT {} FROM _tables WHERE ttl_enabled = 1 ORDER BY table_name",
2088 sql_builders::TABLE_METADATA_COLUMNS
2089 );
2090 let mut stmt = self.conn.prepare(&sql)?;
2091 let rows = stmt
2092 .query_map([], row_to_metadata)?
2093 .collect::<std::result::Result<Vec<_>, _>>()?;
2094 Ok(rows)
2095 }
2096
2097 pub fn get_shard_sequence_range(
2099 &self,
2100 table_name: &str,
2101 shard_id: &str,
2102 ) -> Result<(Option<String>, Option<String>)> {
2103 let result: std::result::Result<(Option<String>, Option<String>), _> = self.conn.query_row(
2105 "SELECT MIN(CAST(sequence_number AS INTEGER)), MAX(CAST(sequence_number AS INTEGER)) FROM _stream_records WHERE table_name = ?1 AND shard_id = ?2",
2106 params![table_name, shard_id],
2107 |row| {
2108 let min: Option<i64> = row.get(0)?;
2109 let max: Option<i64> = row.get(1)?;
2110 Ok((min.map(|n| n.to_string()), max.map(|n| n.to_string())))
2111 },
2112 );
2113 match result {
2114 Ok(range) => Ok(range),
2115 Err(_) => Ok((None, None)),
2116 }
2117 }
2118
2119 pub fn touch_cached_at(
2125 &self,
2126 table_name: &str,
2127 pk: &str,
2128 sk: &str,
2129 timestamp: f64,
2130 ) -> Result<()> {
2131 let sql = format!(
2132 "UPDATE \"{}\" SET cached_at = ?1 WHERE pk = ?2 AND sk = ?3",
2133 escape_table_name(table_name)
2134 );
2135 self.conn.execute(&sql, params![timestamp, pk, sk])?;
2136 Ok(())
2137 }
2138
2139 pub fn get_lru_items(
2144 &self,
2145 table_name: &str,
2146 limit: usize,
2147 ) -> Result<Vec<(String, String, i64)>> {
2148 let sql = format!(
2149 "SELECT pk, sk, item_size FROM \"{}\" WHERE cached_at IS NOT NULL ORDER BY cached_at ASC LIMIT ?1",
2150 escape_table_name(table_name)
2151 );
2152 let mut stmt = self.conn.prepare(&sql)?;
2153 let rows = stmt
2154 .query_map(params![limit as i64], |row| {
2155 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
2156 })?
2157 .collect::<std::result::Result<Vec<_>, _>>()?;
2158 Ok(rows)
2159 }
2160}
2161
2162#[derive(Debug, Clone)]
2164pub struct StreamRecord {
2165 pub event_name: String,
2166 pub keys_json: String,
2167 pub new_image: Option<String>,
2168 pub old_image: Option<String>,
2169 pub sequence_number: String,
2170 pub created_at: i64,
2171 pub user_identity: Option<String>,
2172}
2173
2174#[derive(Debug, Clone)]
2176pub struct TableStats {
2177 pub table_name: String,
2178 pub item_count: i64,
2179 pub size_bytes: u64,
2180}
2181
2182#[derive(Debug, Clone)]
2184pub struct DatabaseInfo {
2185 pub path: Option<String>,
2186 pub size_bytes: u64,
2187 pub table_count: usize,
2188 pub tables: Vec<TableInfoEntry>,
2189}
2190
2191#[derive(Debug, Clone)]
2193pub struct TableInfoEntry {
2194 pub stats: TableStats,
2195 pub metadata: Option<TableMetadata>,
2196}
2197
2198#[derive(Debug, Clone)]
2204pub struct TableMetadata {
2205 pub table_name: String,
2206 pub key_schema: String,
2207 pub attribute_definitions: String,
2208 pub gsi_definitions: Option<String>,
2209 pub lsi_definitions: Option<String>,
2210 pub stream_enabled: bool,
2211 pub stream_view_type: Option<String>,
2212 pub stream_label: Option<String>,
2213 pub ttl_attribute: Option<String>,
2214 pub ttl_enabled: bool,
2215 pub created_at: i64,
2216 pub table_status: String,
2217 pub billing_mode: Option<String>,
2218 pub provisioned_throughput: Option<String>,
2219 pub sse_specification: Option<String>,
2220 pub table_class: Option<String>,
2221 pub deletion_protection_enabled: bool,
2222 pub on_demand_throughput: Option<String>,
2223 pub table_id: Option<String>,
2226 pub vector_index_definitions: Option<String>,
2229}
2230
2231#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2233fn row_to_metadata(row: &rusqlite::Row) -> rusqlite::Result<TableMetadata> {
2234 Ok(TableMetadata {
2235 table_name: row.get(0)?,
2236 key_schema: row.get(1)?,
2237 attribute_definitions: row.get(2)?,
2238 gsi_definitions: row.get(3)?,
2239 lsi_definitions: row.get(4)?,
2240 stream_enabled: row.get::<_, i32>(5)? != 0,
2241 stream_view_type: row.get(6)?,
2242 stream_label: row.get(7)?,
2243 ttl_attribute: row.get(8)?,
2244 ttl_enabled: row.get::<_, i32>(9)? != 0,
2245 created_at: row.get(10)?,
2246 table_status: row.get(11)?,
2247 billing_mode: row.get(12)?,
2248 provisioned_throughput: row.get(13)?,
2249 sse_specification: row.get(14)?,
2250 table_class: row.get(15)?,
2251 deletion_protection_enabled: row.get::<_, i32>(16).unwrap_or(0) != 0,
2252 on_demand_throughput: row.get(17)?,
2253 table_id: row.get(18)?,
2254 vector_index_definitions: row.get(19)?,
2255 })
2256}
2257
2258#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2261const BACKUP_LOCK_PAUSE: std::time::Duration = std::time::Duration::from_millis(10);
2262
2263#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2268const BACKUP_LOCK_PATIENCE: std::time::Duration = REQUEST_BUSY_TIMEOUT;
2269
2270#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2273const BACKUP_RESTART_LIMIT: u32 = 50;
2274
2275#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2277#[derive(Clone, Copy, Debug, PartialEq)]
2278enum BackupStep {
2279 Done,
2280 More {
2282 remaining: i32,
2283 },
2284 Busy,
2286}
2287
2288#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2290#[derive(Clone, Copy, Debug, PartialEq)]
2291enum BackupNext {
2292 Finish,
2293 Step,
2294 Pause,
2295 GiveUpLocked,
2296 GiveUpRestarting,
2297}
2298
2299#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2302#[derive(Default)]
2303struct BackupWatch {
2304 busy_since: Option<std::time::Instant>,
2305 last_remaining: Option<i32>,
2306 restarts: u32,
2307}
2308
2309#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2310impl BackupWatch {
2311 fn observe(
2322 &mut self,
2323 step: BackupStep,
2324 started: std::time::Instant,
2325 now: std::time::Instant,
2326 patience: std::time::Duration,
2327 ) -> BackupNext {
2328 match step {
2329 BackupStep::Done => BackupNext::Finish,
2330 BackupStep::More { remaining } => {
2331 self.busy_since = None;
2332 let restarted = self.last_remaining.is_some_and(|last| remaining >= last);
2333 self.last_remaining = Some(remaining);
2334 if !restarted {
2335 return BackupNext::Step;
2336 }
2337 self.restarts += 1;
2338 if self.restarts > BACKUP_RESTART_LIMIT {
2339 BackupNext::GiveUpRestarting
2340 } else {
2341 BackupNext::Pause
2342 }
2343 }
2344 BackupStep::Busy => {
2345 let since = *self.busy_since.get_or_insert(started);
2346 if now.saturating_duration_since(since) >= patience {
2347 BackupNext::GiveUpLocked
2348 } else {
2349 BackupNext::Pause
2350 }
2351 }
2352 }
2353 }
2354}
2355
2356#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2366fn run_backup(backup: &rusqlite::backup::Backup<'_, '_>) -> Result<()> {
2367 let mut watch = BackupWatch::default();
2368 loop {
2369 let started = std::time::Instant::now();
2370 let step = backup_step(backup)?;
2371 match watch.observe(
2372 step,
2373 started,
2374 std::time::Instant::now(),
2375 BACKUP_LOCK_PATIENCE,
2376 ) {
2377 BackupNext::Finish => return Ok(()),
2378 BackupNext::Step => {}
2379 BackupNext::Pause => std::thread::sleep(BACKUP_LOCK_PAUSE),
2380 BackupNext::GiveUpLocked => {
2381 return Err(rusqlite::Error::SqliteFailure(
2382 rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_BUSY),
2383 Some("database is locked".to_string()),
2384 )
2385 .into());
2386 }
2387 BackupNext::GiveUpRestarting => {
2388 return Err(rusqlite::Error::SqliteFailure(
2389 rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_BUSY),
2390 Some(format!(
2391 "the database being copied kept changing; gave up after {BACKUP_RESTART_LIMIT} restarts"
2392 )),
2393 )
2394 .into());
2395 }
2396 }
2397 }
2398}
2399
2400#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2402fn backup_step(backup: &rusqlite::backup::Backup<'_, '_>) -> Result<BackupStep> {
2403 use rusqlite::backup::StepResult;
2404 Ok(match backup.step(100)? {
2405 StepResult::Done => BackupStep::Done,
2406 StepResult::More => BackupStep::More {
2407 remaining: backup.progress().remaining,
2408 },
2409 _ => BackupStep::Busy,
2411 })
2412}
2413
2414#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2420fn ensure_snapshot(source: &Connection) -> Result<()> {
2421 if table_exists(source, "_tables")? {
2422 Ok(())
2423 } else {
2424 Err(DynoxideError::ValidationException(
2425 "not a Dynoxide snapshot: it has no table metadata".to_string(),
2426 ))
2427 }
2428}
2429
2430#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2432fn table_exists(conn: &Connection, name: &str) -> Result<bool> {
2433 Ok(conn.query_row(sql_builders::TABLE_EXISTS, [name], |row| row.get(0))?)
2434}
2435
2436#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
2441fn refuse_newer_schema(conn: &Connection) -> Result<()> {
2442 use rusqlite::OptionalExtension;
2443 if !table_exists(conn, "_config")? {
2444 return Ok(());
2445 }
2446 let value: Option<String> = conn
2447 .query_row(sql_builders::GET_SCHEMA_VERSION, [], |row| row.get(0))
2448 .optional()?;
2449 crate::storage_backend::schema::refuse_newer(crate::storage_backend::schema::parse_recorded(
2450 value.as_deref(),
2451 ))
2452 .map_err(DynoxideError::InternalServerError)
2453}
2454
2455#[cfg(all(test, any(feature = "native-sqlite", feature = "_has-encryption")))]
2456mod tests {
2457 use super::*;
2458
2459 fn test_storage() -> Storage {
2460 Storage::memory().expect("Failed to create in-memory storage")
2461 }
2462
2463 #[test]
2464 fn test_initialize_creates_metadata_tables() {
2465 let storage = test_storage();
2466 let version: String = storage
2468 .conn()
2469 .query_row(
2470 "SELECT value FROM _config WHERE key = 'schema_version'",
2471 [],
2472 |row| row.get(0),
2473 )
2474 .unwrap();
2475 assert_eq!(version, SCHEMA_VERSION.to_string());
2476 }
2477
2478 #[test]
2479 fn test_migrate_v6_to_v7_adds_on_demand_throughput_column() {
2480 let tmp = tempfile::NamedTempFile::new().unwrap();
2484 let path = tmp.path().to_str().unwrap().to_string();
2485
2486 {
2489 let conn = Connection::open(&path).unwrap();
2490 conn.execute_batch(
2491 "CREATE TABLE _config (key TEXT PRIMARY KEY, value TEXT NOT NULL);
2492 CREATE TABLE _tables (
2493 table_name TEXT PRIMARY KEY,
2494 key_schema TEXT NOT NULL,
2495 attribute_definitions TEXT NOT NULL,
2496 gsi_definitions TEXT,
2497 lsi_definitions TEXT,
2498 stream_enabled INTEGER DEFAULT 0,
2499 stream_view_type TEXT,
2500 stream_label TEXT,
2501 ttl_attribute TEXT,
2502 ttl_enabled INTEGER DEFAULT 0,
2503 created_at INTEGER NOT NULL,
2504 table_status TEXT NOT NULL DEFAULT 'ACTIVE',
2505 billing_mode TEXT DEFAULT 'PAY_PER_REQUEST',
2506 provisioned_throughput TEXT,
2507 tags TEXT,
2508 sse_specification TEXT,
2509 table_class TEXT,
2510 deletion_protection_enabled INTEGER DEFAULT 0
2511 );",
2512 )
2513 .unwrap();
2514 conn.execute(
2515 "INSERT INTO _config (key, value) VALUES ('schema_version', '6')",
2516 [],
2517 )
2518 .unwrap();
2519 conn.execute(
2520 "INSERT INTO _tables (table_name, key_schema, attribute_definitions, created_at) \
2521 VALUES ('LegacyTable', ?1, ?2, 0)",
2522 params![
2523 r#"[{"AttributeName":"pk","KeyType":"HASH"}]"#,
2524 r#"[{"AttributeName":"pk","AttributeType":"S"}]"#,
2525 ],
2526 )
2527 .unwrap();
2528 }
2529
2530 let storage = Storage::new(&path).unwrap();
2533 let version: String = storage
2534 .conn()
2535 .query_row(
2536 "SELECT value FROM _config WHERE key = 'schema_version'",
2537 [],
2538 |r| r.get(0),
2539 )
2540 .unwrap();
2541 assert_eq!(version, SCHEMA_VERSION.to_string());
2542
2543 let meta = storage.get_table_metadata("LegacyTable").unwrap().unwrap();
2546 assert_eq!(meta.table_name, "LegacyTable");
2547 assert!(meta.on_demand_throughput.is_none());
2548
2549 let col: Option<String> = storage
2551 .conn()
2552 .query_row(
2553 "SELECT on_demand_throughput FROM _tables WHERE table_name = 'LegacyTable'",
2554 [],
2555 |r| r.get(0),
2556 )
2557 .unwrap();
2558 assert!(col.is_none());
2559 }
2560
2561 #[test]
2565 fn test_migrate_v7_to_v8_backfills_table_id() {
2566 let tmp = tempfile::NamedTempFile::new().unwrap();
2567 let path = tmp.path().to_str().unwrap().to_string();
2568
2569 {
2572 let conn = Connection::open(&path).unwrap();
2573 conn.execute_batch(
2574 "CREATE TABLE _config (key TEXT PRIMARY KEY, value TEXT NOT NULL);
2575 CREATE TABLE _tables (
2576 table_name TEXT PRIMARY KEY,
2577 key_schema TEXT NOT NULL,
2578 attribute_definitions TEXT NOT NULL,
2579 gsi_definitions TEXT,
2580 lsi_definitions TEXT,
2581 stream_enabled INTEGER DEFAULT 0,
2582 stream_view_type TEXT,
2583 stream_label TEXT,
2584 ttl_attribute TEXT,
2585 ttl_enabled INTEGER DEFAULT 0,
2586 created_at INTEGER NOT NULL,
2587 table_status TEXT NOT NULL DEFAULT 'ACTIVE',
2588 billing_mode TEXT DEFAULT 'PAY_PER_REQUEST',
2589 provisioned_throughput TEXT,
2590 tags TEXT,
2591 sse_specification TEXT,
2592 table_class TEXT,
2593 deletion_protection_enabled INTEGER DEFAULT 0,
2594 on_demand_throughput TEXT
2595 );",
2596 )
2597 .unwrap();
2598 conn.execute(
2599 "INSERT INTO _config (key, value) VALUES ('schema_version', '7')",
2600 [],
2601 )
2602 .unwrap();
2603 conn.execute(
2604 "INSERT INTO _tables (table_name, key_schema, attribute_definitions, created_at) \
2605 VALUES ('LegacyTable', ?1, ?2, 0)",
2606 params![
2607 r#"[{"AttributeName":"pk","KeyType":"HASH"}]"#,
2608 r#"[{"AttributeName":"pk","AttributeType":"S"}]"#,
2609 ],
2610 )
2611 .unwrap();
2612 }
2613
2614 let storage = Storage::new(&path).unwrap();
2616 let version: String = storage
2617 .conn()
2618 .query_row(
2619 "SELECT value FROM _config WHERE key = 'schema_version'",
2620 [],
2621 |r| r.get(0),
2622 )
2623 .unwrap();
2624 assert_eq!(version, SCHEMA_VERSION.to_string());
2625
2626 let meta = storage.get_table_metadata("LegacyTable").unwrap().unwrap();
2629 let id = meta.table_id.expect("legacy table should be backfilled");
2630 assert!(!id.is_empty());
2631
2632 drop(storage);
2634 let storage2 = Storage::new(&path).unwrap();
2635 let meta2 = storage2.get_table_metadata("LegacyTable").unwrap().unwrap();
2636 assert_eq!(meta2.table_id.as_deref(), Some(id.as_str()));
2637 }
2638
2639 fn write_pre_event_id_database(path: &str) {
2642 let storage = Storage::new(path).unwrap();
2643 for seq in ["1", "2"] {
2644 storage
2645 .insert_stream_record("T", "INSERT", "{}", None, None, seq, "s", 0)
2646 .unwrap();
2647 }
2648 storage
2649 .conn()
2650 .execute_batch("ALTER TABLE _stream_records DROP COLUMN event_id")
2651 .unwrap();
2652 }
2653
2654 fn stored_event_ids(storage: &Storage) -> Vec<Option<String>> {
2655 storage
2656 .stream_records_with_event_ids("T", "s", 0, 10)
2657 .unwrap()
2658 .into_iter()
2659 .map(|(_, event_id)| event_id)
2660 .collect()
2661 }
2662
2663 #[test]
2666 fn stream_records_without_an_event_id_keep_their_sequence_number_on_open() {
2667 let tmp = tempfile::NamedTempFile::new().unwrap();
2668 let path = tmp.path().to_str().unwrap().to_string();
2669 write_pre_event_id_database(&path);
2670
2671 let expected = vec![Some("1".to_string()), Some("2".to_string())];
2672 let storage = Storage::new(&path).unwrap();
2673 assert_eq!(stored_event_ids(&storage), expected);
2674
2675 storage
2678 .conn()
2679 .execute_batch("UPDATE _stream_records SET event_id = NULL WHERE sequence_number = '2'")
2680 .unwrap();
2681 drop(storage);
2682 let storage = Storage::new(&path).unwrap();
2683 assert_eq!(
2684 stored_event_ids(&storage),
2685 vec![Some("1".to_string()), None]
2686 );
2687 }
2688
2689 #[test]
2692 fn a_restored_snapshot_without_an_event_id_column_is_backfilled() {
2693 let tmp = tempfile::NamedTempFile::new().unwrap();
2694 let path = tmp.path().to_str().unwrap().to_string();
2695 write_pre_event_id_database(&path);
2696
2697 let mut storage = Storage::memory().unwrap();
2698 storage.restore_from(&path).unwrap();
2699 assert_eq!(
2700 stored_event_ids(&storage),
2701 vec![Some("1".to_string()), Some("2".to_string())]
2702 );
2703 }
2704
2705 #[test]
2709 fn test_migrate_v8_to_v9_adds_vector_index_definitions_column() {
2710 let tmp = tempfile::NamedTempFile::new().unwrap();
2711 let path = tmp.path().to_str().unwrap().to_string();
2712
2713 {
2716 let conn = Connection::open(&path).unwrap();
2717 conn.execute_batch(
2718 "CREATE TABLE _config (key TEXT PRIMARY KEY, value TEXT NOT NULL);
2719 CREATE TABLE _tables (
2720 table_name TEXT PRIMARY KEY,
2721 key_schema TEXT NOT NULL,
2722 attribute_definitions TEXT NOT NULL,
2723 gsi_definitions TEXT,
2724 lsi_definitions TEXT,
2725 stream_enabled INTEGER DEFAULT 0,
2726 stream_view_type TEXT,
2727 stream_label TEXT,
2728 ttl_attribute TEXT,
2729 ttl_enabled INTEGER DEFAULT 0,
2730 created_at INTEGER NOT NULL,
2731 table_status TEXT NOT NULL DEFAULT 'ACTIVE',
2732 billing_mode TEXT DEFAULT 'PAY_PER_REQUEST',
2733 provisioned_throughput TEXT,
2734 tags TEXT,
2735 sse_specification TEXT,
2736 table_class TEXT,
2737 deletion_protection_enabled INTEGER DEFAULT 0,
2738 on_demand_throughput TEXT,
2739 table_id TEXT
2740 );",
2741 )
2742 .unwrap();
2743 conn.execute(
2744 "INSERT INTO _config (key, value) VALUES ('schema_version', '8')",
2745 [],
2746 )
2747 .unwrap();
2748 conn.execute(
2749 "INSERT INTO _tables (table_name, key_schema, attribute_definitions, created_at, table_id) \
2750 VALUES ('LegacyTable', ?1, ?2, 0, 'fixed-id')",
2751 params![
2752 r#"[{"AttributeName":"pk","KeyType":"HASH"}]"#,
2753 r#"[{"AttributeName":"pk","AttributeType":"S"}]"#,
2754 ],
2755 )
2756 .unwrap();
2757 }
2758
2759 let storage = Storage::new(&path).unwrap();
2761 let version: String = storage
2762 .conn()
2763 .query_row(
2764 "SELECT value FROM _config WHERE key = 'schema_version'",
2765 [],
2766 |r| r.get(0),
2767 )
2768 .unwrap();
2769 assert_eq!(version, SCHEMA_VERSION.to_string());
2770
2771 let meta = storage.get_table_metadata("LegacyTable").unwrap().unwrap();
2774 assert_eq!(meta.table_name, "LegacyTable");
2775 assert!(meta.vector_index_definitions.is_none());
2776
2777 let col: Option<String> = storage
2779 .conn()
2780 .query_row(
2781 "SELECT vector_index_definitions FROM _tables WHERE table_name = 'LegacyTable'",
2782 [],
2783 |r| r.get(0),
2784 )
2785 .unwrap();
2786 assert!(col.is_none());
2787 }
2788
2789 #[test]
2790 fn test_migrate_v9_to_v10_rebuilds_a_multi_attribute_gsi() {
2791 let tmp = tempfile::NamedTempFile::new().unwrap();
2792 let path = tmp.path().to_str().unwrap().to_string();
2793
2794 {
2796 let db = crate::Database::new(&path).unwrap();
2797 db.create_table(
2798 serde_json::from_value(serde_json::json!({
2799 "TableName": "Legacy",
2800 "BillingMode": "PAY_PER_REQUEST",
2801 "AttributeDefinitions": [
2802 {"AttributeName": "pk", "AttributeType": "S"},
2803 {"AttributeName": "h", "AttributeType": "S"},
2804 {"AttributeName": "a", "AttributeType": "S"},
2805 {"AttributeName": "b", "AttributeType": "S"}
2806 ],
2807 "KeySchema": [{"AttributeName": "pk", "KeyType": "HASH"}],
2808 "GlobalSecondaryIndexes": [{
2809 "IndexName": "byKey",
2810 "KeySchema": [
2811 {"AttributeName": "h", "KeyType": "HASH"},
2812 {"AttributeName": "a", "KeyType": "RANGE"},
2813 {"AttributeName": "b", "KeyType": "RANGE"}
2814 ],
2815 "Projection": {"ProjectionType": "ALL"}
2816 }]
2817 }))
2818 .unwrap(),
2819 )
2820 .unwrap();
2821 for item in [
2822 serde_json::json!({"pk": {"S": "1"}, "h": {"S": "x"}, "a": {"S": "m"}, "b": {"S": "2"}}),
2823 serde_json::json!({"pk": {"S": "2"}, "h": {"S": "x"}, "a": {"S": "m"}, "b": {"S": "1"}}),
2824 serde_json::json!({"pk": {"S": "3"}, "h": {"S": "x"}, "a": {"S": "m"}}),
2825 ] {
2826 db.put_item(
2827 serde_json::from_value(
2828 serde_json::json!({"TableName": "Legacy", "Item": item}),
2829 )
2830 .unwrap(),
2831 )
2832 .unwrap();
2833 }
2834 }
2835
2836 {
2839 let conn = Connection::open(&path).unwrap();
2840 conn.execute_batch(
2841 "DELETE FROM \"Legacy::gsi::byKey\";
2842 INSERT INTO \"Legacy::gsi::byKey\" (gsi_pk, gsi_sk, table_pk, table_sk, item_json)
2843 SELECT 'S:x', 'S:m', pk, sk, item_json FROM \"Legacy\";
2844 UPDATE _config SET value = '9' WHERE key = 'schema_version';",
2845 )
2846 .unwrap();
2847 }
2848
2849 let storage = Storage::new(&path).unwrap();
2850 let version: String = storage
2851 .conn()
2852 .query_row(
2853 "SELECT value FROM _config WHERE key = 'schema_version'",
2854 [],
2855 |r| r.get(0),
2856 )
2857 .unwrap();
2858 assert_eq!(version, SCHEMA_VERSION.to_string());
2859 let rows: Vec<(String, String)> = {
2860 let mut stmt = storage
2861 .conn()
2862 .prepare(
2863 "SELECT table_pk, gsi_sk FROM \"Legacy::gsi::byKey\" ORDER BY gsi_sk, table_pk",
2864 )
2865 .unwrap();
2866 stmt.query_map([], |r| Ok((r.get(0)?, r.get(1)?)))
2867 .unwrap()
2868 .collect::<rusqlite::Result<_>>()
2869 .unwrap()
2870 };
2871 let expected_key = |b: &str| {
2872 crate::actions::gsi::composite_key_string(&[
2873 &AttributeValue::S("m".into()),
2874 &AttributeValue::S(b.into()),
2875 ])
2876 .unwrap()
2877 };
2878 assert_eq!(
2879 rows,
2880 vec![
2881 ("S:2".to_string(), expected_key("1")),
2882 ("S:1".to_string(), expected_key("2")),
2883 ]
2884 );
2885 }
2886
2887 fn v9_snapshot_path() -> (tempfile::TempDir, String) {
2891 let dir = tempfile::tempdir().unwrap();
2892 let path = dir.path().join("legacy.db").to_str().unwrap().to_string();
2893 (dir, path)
2894 }
2895
2896 fn write_v9_database(path: &str, extra: usize) {
2911 {
2912 let db = crate::Database::new(path).unwrap();
2913 db.create_table(
2914 serde_json::from_value(serde_json::json!({
2915 "TableName": "Legacy",
2916 "BillingMode": "PAY_PER_REQUEST",
2917 "AttributeDefinitions": [
2918 {"AttributeName": "pk", "AttributeType": "S"},
2919 {"AttributeName": "h", "AttributeType": "S"},
2920 {"AttributeName": "h2", "AttributeType": "S"},
2921 {"AttributeName": "a", "AttributeType": "S"},
2922 {"AttributeName": "b", "AttributeType": "S"}
2923 ],
2924 "KeySchema": [{"AttributeName": "pk", "KeyType": "HASH"}],
2925 "GlobalSecondaryIndexes": [
2926 {
2927 "IndexName": "byKey",
2928 "KeySchema": [
2929 {"AttributeName": "h", "KeyType": "HASH"},
2930 {"AttributeName": "a", "KeyType": "RANGE"},
2931 {"AttributeName": "b", "KeyType": "RANGE"}
2932 ],
2933 "Projection": {"ProjectionType": "ALL"}
2934 },
2935 {
2936 "IndexName": "byTwoHash",
2937 "KeySchema": [
2938 {"AttributeName": "h", "KeyType": "HASH"},
2939 {"AttributeName": "h2", "KeyType": "HASH"},
2940 {"AttributeName": "a", "KeyType": "RANGE"}
2941 ],
2942 "Projection": {"ProjectionType": "ALL"}
2943 },
2944 {
2945 "IndexName": "bySingle",
2946 "KeySchema": [
2947 {"AttributeName": "h", "KeyType": "HASH"},
2948 {"AttributeName": "a", "KeyType": "RANGE"}
2949 ],
2950 "Projection": {"ProjectionType": "KEYS_ONLY"}
2951 }
2952 ]
2953 }))
2954 .unwrap(),
2955 )
2956 .unwrap();
2957 let named = [
2958 serde_json::json!({"pk": {"S": "1"}, "h": {"S": "x"}, "h2": {"S": "y"}, "a": {"S": "m"}, "b": {"S": "2"}}),
2959 serde_json::json!({"pk": {"S": "2"}, "h": {"S": "x"}, "h2": {"S": "y"}, "a": {"S": "m"}, "b": {"S": "1"}}),
2960 serde_json::json!({"pk": {"S": "3"}, "h": {"S": "x"}, "h2": {"S": "y"}, "a": {"S": "m"}}),
2961 ];
2962 let extras = (0..extra).map(|i| {
2963 serde_json::json!({
2964 "pk": {"S": format!("e{i:05}")},
2965 "h": {"S": "x"}, "h2": {"S": "y"}, "a": {"S": "m"},
2966 "b": {"S": format!("{i:05}")}
2967 })
2968 });
2969 for item in named.into_iter().chain(extras) {
2970 db.put_item(
2971 serde_json::from_value(
2972 serde_json::json!({"TableName": "Legacy", "Item": item}),
2973 )
2974 .unwrap(),
2975 )
2976 .unwrap();
2977 }
2978 db.create_table(
2979 serde_json::from_value(serde_json::json!({
2980 "TableName": "Vec",
2981 "BillingMode": "PAY_PER_REQUEST",
2982 "AttributeDefinitions": [{"AttributeName": "pk", "AttributeType": "S"}],
2983 "KeySchema": [{"AttributeName": "pk", "KeyType": "HASH"}],
2984 "VectorIndexes": [{
2985 "IndexName": "vix",
2986 "VectorAttribute": {"AttributeName": "e"},
2987 "Dimensions": 3,
2988 "DistanceFunction": "COSINE",
2989 "Projection": {"ProjectionType": "KEYS_ONLY"}
2990 }]
2991 }))
2992 .unwrap(),
2993 )
2994 .unwrap();
2995 db.put_item(
2996 serde_json::from_value(serde_json::json!({
2997 "TableName": "Vec",
2998 "Item": {
2999 "pk": {"S": "v"},
3000 "e": {"L": [{"N": "0.125"}, {"N": "0.125"}, {"N": "0.125"}]}
3001 }
3002 }))
3003 .unwrap(),
3004 )
3005 .unwrap();
3006 }
3007 let conn = Connection::open(path).unwrap();
3008 conn.execute_batch(
3009 "UPDATE \"Vec\" SET item_size = item_size + 7;
3010 DELETE FROM \"Legacy::gsi::byKey\";
3011 INSERT INTO \"Legacy::gsi::byKey\" (gsi_pk, gsi_sk, table_pk, table_sk, item_json)
3012 SELECT 'S:x', 'S:m', pk, sk, item_json FROM \"Legacy\";
3013 DELETE FROM \"Legacy::gsi::byTwoHash\";
3014 INSERT INTO \"Legacy::gsi::byTwoHash\" (gsi_pk, gsi_sk, table_pk, table_sk, item_json)
3015 SELECT 'S:x', 'S:m', pk, sk, item_json FROM \"Legacy\";
3016 UPDATE _config SET value = '9' WHERE key = 'schema_version';",
3017 )
3018 .unwrap();
3019 }
3020
3021 fn gsi_rows(conn: &Connection, index: &str) -> Vec<[String; 5]> {
3023 let mut stmt = conn
3024 .prepare(&format!(
3025 "SELECT gsi_pk, gsi_sk, table_pk, table_sk, item_json \
3026 FROM \"Legacy::gsi::{index}\" ORDER BY table_pk, table_sk"
3027 ))
3028 .unwrap();
3029 stmt.query_map([], |r| {
3030 Ok([r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?, r.get(4)?])
3031 })
3032 .unwrap()
3033 .collect::<rusqlite::Result<_>>()
3034 .unwrap()
3035 }
3036
3037 fn recorded_version(conn: &Connection) -> String {
3038 conn.query_row(
3039 "SELECT value FROM _config WHERE key = 'schema_version'",
3040 [],
3041 |r| r.get(0),
3042 )
3043 .unwrap()
3044 }
3045
3046 fn open_with_faults(
3049 path: &str,
3050 gsi_insert: Option<u32>,
3051 item_size: Option<u32>,
3052 ) -> Result<Storage> {
3053 let mut storage = Storage {
3054 conn: Connection::open(path)?,
3055 metadata_cache: RefCell::new(HashMap::new()),
3056 gsi_insert_fault: RefCell::new(gsi_insert),
3057 item_size_fault: RefCell::new(item_size),
3058 clock: Arc::new(SystemClock),
3059 };
3060 storage.initialize()?;
3061 Ok(storage)
3062 }
3063
3064 #[test]
3065 fn an_interrupted_gsi_rebuild_leaves_the_database_as_it_was() {
3066 let (_dir, path) = v9_snapshot_path();
3067 write_v9_database(&path, 0);
3068 let before = {
3069 let conn = Connection::open(&path).unwrap();
3070 (gsi_rows(&conn, "byKey"), gsi_rows(&conn, "byTwoHash"))
3071 };
3072
3073 assert!(open_with_faults(&path, Some(0), None).is_err());
3076
3077 let conn = Connection::open(&path).unwrap();
3078 assert_eq!(recorded_version(&conn), "9");
3079 assert_eq!(
3080 (gsi_rows(&conn, "byKey"), gsi_rows(&conn, "byTwoHash")),
3081 before,
3082 "an interrupted rebuild must leave every index as it found it"
3083 );
3084 drop(conn);
3085
3086 let storage = Storage::new(&path).unwrap();
3088 assert_eq!(recorded_version(storage.conn()), SCHEMA_VERSION.to_string());
3089 }
3090
3091 #[test]
3092 fn an_interrupted_size_recompute_leaves_the_database_as_it_was() {
3093 let (_dir, path) = v9_snapshot_path();
3094 {
3095 let db = crate::Database::new(&path).unwrap();
3096 db.create_table(
3097 serde_json::from_value(serde_json::json!({
3098 "TableName": "Vec",
3099 "BillingMode": "PAY_PER_REQUEST",
3100 "AttributeDefinitions": [{"AttributeName": "pk", "AttributeType": "S"}],
3101 "KeySchema": [{"AttributeName": "pk", "KeyType": "HASH"}],
3102 "VectorIndexes": [{
3103 "IndexName": "vix",
3104 "VectorAttribute": {"AttributeName": "e"},
3105 "Dimensions": 3,
3106 "DistanceFunction": "COSINE",
3107 "Projection": {"ProjectionType": "KEYS_ONLY"}
3108 }]
3109 }))
3110 .unwrap(),
3111 )
3112 .unwrap();
3113 for pk in ["a", "b", "c"] {
3114 db.put_item(
3115 serde_json::from_value(serde_json::json!({
3116 "TableName": "Vec",
3117 "Item": {
3118 "pk": {"S": pk},
3119 "e": {"L": [{"N": "0.125"}, {"N": "0.125"}, {"N": "0.125"}]}
3120 }
3121 }))
3122 .unwrap(),
3123 )
3124 .unwrap();
3125 }
3126 }
3127 let sizes = |conn: &Connection| -> Vec<(String, i64)> {
3129 let mut stmt = conn
3130 .prepare("SELECT pk, item_size FROM \"Vec\" ORDER BY pk")
3131 .unwrap();
3132 stmt.query_map([], |r| Ok((r.get(0)?, r.get(1)?)))
3133 .unwrap()
3134 .collect::<rusqlite::Result<_>>()
3135 .unwrap()
3136 };
3137 let before = {
3138 let conn = Connection::open(&path).unwrap();
3139 conn.execute_batch(
3140 "UPDATE \"Vec\" SET item_size = item_size + 7;
3141 UPDATE _config SET value = '10' WHERE key = 'schema_version';",
3142 )
3143 .unwrap();
3144 sizes(&conn)
3145 };
3146
3147 assert!(open_with_faults(&path, None, Some(1)).is_err());
3149
3150 let conn = Connection::open(&path).unwrap();
3151 assert_eq!(recorded_version(&conn), "10");
3152 assert_eq!(
3153 sizes(&conn),
3154 before,
3155 "an interrupted recompute must leave every stored size as it found it"
3156 );
3157 }
3158
3159 fn vec_size(conn: &Connection) -> i64 {
3160 conn.query_row("SELECT item_size FROM \"Vec\" WHERE pk = 'S:v'", [], |r| {
3161 r.get(0)
3162 })
3163 .unwrap()
3164 }
3165
3166 #[test]
3167 fn a_second_open_runs_no_migration() {
3168 let (_dir, path) = v9_snapshot_path();
3169 write_v9_database(&path, 0);
3170 drop(Storage::new(&path).unwrap());
3171 let storage = open_with_faults(&path, Some(0), Some(0)).unwrap();
3174 assert_eq!(recorded_version(storage.conn()), SCHEMA_VERSION.to_string());
3175 }
3176
3177 #[test]
3178 fn a_recompute_interrupted_after_the_rebuild_leaves_version_10() {
3179 let (_dir, path) = v9_snapshot_path();
3180 write_v9_database(&path, 0);
3181 let stale_size = vec_size(&Connection::open(&path).unwrap());
3182
3183 assert!(open_with_faults(&path, None, Some(0)).is_err());
3186
3187 let conn = Connection::open(&path).unwrap();
3188 assert_eq!(
3189 recorded_version(&conn),
3190 "10",
3191 "the rebuild's step is kept and the recompute's is not"
3192 );
3193 assert_eq!(vec_size(&conn), stale_size, "the recompute is rolled back");
3194 let composite = crate::actions::gsi::composite_key_string(&[
3195 &AttributeValue::S("m".into()),
3196 &AttributeValue::S("2".into()),
3197 ])
3198 .unwrap();
3199 assert!(
3200 gsi_rows(&conn, "byKey")
3201 .iter()
3202 .any(|[_, gsi_sk, _, _, _]| *gsi_sk == composite),
3203 "the rebuilt index is kept"
3204 );
3205 drop(conn);
3206
3207 let storage = open_with_faults(&path, Some(0), None).unwrap();
3209 assert_eq!(recorded_version(storage.conn()), SCHEMA_VERSION.to_string());
3210 assert_eq!(vec_size(storage.conn()), stale_size - 7);
3211 }
3212
3213 #[test]
3214 fn the_rebuild_keeps_single_attribute_indexes_and_keys_every_hash_attribute() {
3215 let (_dir, path) = v9_snapshot_path();
3216 write_v9_database(&path, 0);
3217 let single_before = gsi_rows(&Connection::open(&path).unwrap(), "bySingle");
3218
3219 let storage = Storage::new(&path).unwrap();
3220 assert_eq!(
3221 gsi_rows(storage.conn(), "bySingle"),
3222 single_before,
3223 "a single-attribute index is stored the same way and is left alone"
3224 );
3225 let two_hash = crate::actions::gsi::composite_key_string(&[
3226 &AttributeValue::S("x".into()),
3227 &AttributeValue::S("y".into()),
3228 ])
3229 .unwrap();
3230 let keys: Vec<(String, String)> = gsi_rows(storage.conn(), "byTwoHash")
3231 .into_iter()
3232 .map(|[gsi_pk, _, table_pk, _, _]| (gsi_pk, table_pk))
3233 .collect();
3234 assert_eq!(
3235 keys,
3236 ["S:1", "S:2", "S:3"]
3237 .map(|pk| (two_hash.clone(), pk.to_string()))
3238 .to_vec()
3239 );
3240 }
3241
3242 #[test]
3243 fn the_rebuild_runs_past_the_first_backfill_batch() {
3244 let tmp = tempfile::NamedTempFile::new().unwrap();
3245 let path = tmp.path().to_str().unwrap().to_string();
3246 write_v9_database(&path, 1_005);
3248
3249 let storage = Storage::new(&path).unwrap();
3250 assert_eq!(gsi_rows(storage.conn(), "byKey").len(), 2 + 1_005);
3253 assert_eq!(gsi_rows(storage.conn(), "byTwoHash").len(), 3 + 1_005);
3254 }
3255
3256 #[test]
3257 fn a_restored_snapshot_from_an_earlier_version_is_brought_up_to_date() {
3258 let (_dir, path) = v9_snapshot_path();
3259 write_v9_database(&path, 0);
3260
3261 let mut storage = Storage::memory().unwrap();
3262 storage.restore_from(&path).unwrap();
3263
3264 assert_eq!(recorded_version(storage.conn()), SCHEMA_VERSION.to_string());
3265 let sort_keys: Vec<String> = gsi_rows(storage.conn(), "byKey")
3266 .into_iter()
3267 .map(|[_, gsi_sk, _, _, _]| gsi_sk)
3268 .collect();
3269 let composite = |b: &str| {
3270 crate::actions::gsi::composite_key_string(&[
3271 &AttributeValue::S("m".into()),
3272 &AttributeValue::S(b.into()),
3273 ])
3274 .unwrap()
3275 };
3276 assert_eq!(sort_keys, vec![composite("2"), composite("1")]);
3277 }
3278
3279 fn mark_live(storage: &Storage) {
3282 storage
3283 .conn()
3284 .execute_batch(
3285 "CREATE TABLE live_marker (v TEXT); INSERT INTO live_marker VALUES ('live');",
3286 )
3287 .unwrap();
3288 }
3289
3290 fn still_live(storage: &Storage) -> bool {
3291 storage
3292 .conn()
3293 .query_row("SELECT v FROM live_marker", [], |r| r.get::<_, String>(0))
3294 .map(|v| v == "live")
3295 .unwrap_or(false)
3296 }
3297
3298 #[test]
3299 fn restoring_from_a_missing_file_errors_and_leaves_the_database_alone() {
3300 let dir = tempfile::tempdir().unwrap();
3301 let missing = dir.path().join("no-such-snapshot.db");
3302 let missing = missing.to_str().unwrap();
3303 let mut storage = Storage::memory().unwrap();
3304 mark_live(&storage);
3305
3306 assert!(storage.restore_from(missing).is_err());
3307 assert!(
3308 !std::path::Path::new(missing).exists(),
3309 "a mistyped path must not be created"
3310 );
3311 assert!(still_live(&storage), "the database must be left as it was");
3312 }
3313
3314 #[test]
3315 fn a_restore_whose_migration_fails_leaves_the_database_alone() {
3316 let (_dir, path) = v9_snapshot_path();
3317 write_v9_database(&path, 0);
3318
3319 let mut storage = Storage::memory().unwrap();
3320 mark_live(&storage);
3321 storage.fail_gsi_insert_after(0);
3323 let err = storage.restore_from(&path).unwrap_err().to_string();
3324 assert!(
3325 err.contains("injected GSI insert failure"),
3326 "the restore must fail in the migration, not elsewhere: {err}"
3327 );
3328
3329 assert!(still_live(&storage), "the database must be left as it was");
3330 let legacy: i64 = storage
3331 .conn()
3332 .query_row(
3333 "SELECT count(*) FROM sqlite_master WHERE name = 'Legacy'",
3334 [],
3335 |r| r.get(0),
3336 )
3337 .unwrap();
3338 assert_eq!(legacy, 0, "none of the snapshot may be left behind");
3339 assert_eq!(recorded_version(storage.conn()), SCHEMA_VERSION.to_string());
3340 }
3341
3342 #[test]
3343 fn restoring_from_something_that_is_not_a_snapshot_errors_and_leaves_the_database_alone() {
3344 let dir = tempfile::tempdir().unwrap();
3345 let empty = dir.path().join("zero-bytes.db");
3346 std::fs::write(&empty, b"").unwrap();
3347 let foreign = dir.path().join("foreign.db");
3348 Connection::open(&foreign)
3349 .unwrap()
3350 .execute_batch("CREATE TABLE notes (body TEXT); INSERT INTO notes VALUES ('x');")
3351 .unwrap();
3352
3353 for source in [
3354 empty.to_str().unwrap(),
3355 foreign.to_str().unwrap(),
3356 ":memory:",
3357 ] {
3358 let mut storage = Storage::memory().unwrap();
3359 mark_live(&storage);
3360 let err = storage.restore_from(source).unwrap_err().to_string();
3361 assert!(
3362 err.contains("not a Dynoxide snapshot"),
3363 "{source}: refused for the wrong reason: {err}"
3364 );
3365 assert!(
3366 still_live(&storage),
3367 "{source}: the database must be left as it was"
3368 );
3369 }
3370 }
3371
3372 #[test]
3373 fn a_backup_gives_up_on_a_source_that_stays_locked() {
3374 let dir = tempfile::tempdir().unwrap();
3375 let path = dir.path().join("locked.db");
3376 let path = path.to_str().unwrap().to_string();
3377 {
3378 let conn = Connection::open(&path).unwrap();
3379 conn.execute_batch(
3380 "PRAGMA journal_mode = DELETE; CREATE TABLE t (v); INSERT INTO t VALUES (1);",
3381 )
3382 .unwrap();
3383 }
3384 let holder = Connection::open(&path).unwrap();
3386 holder
3387 .execute_batch("BEGIN EXCLUSIVE; INSERT INTO t VALUES (2);")
3388 .unwrap();
3389
3390 let (done, outcome) = std::sync::mpsc::channel();
3393 let copier_path = path.clone();
3394 std::thread::spawn(move || {
3395 let source = Connection::open(&copier_path).unwrap();
3396 let mut dest = Connection::open_in_memory().unwrap();
3397 let result = rusqlite::backup::Backup::new(&source, &mut dest)
3398 .map_err(DynoxideError::from)
3399 .and_then(|backup| run_backup(&backup));
3400 let _ = done.send(result.map_err(|e| e.to_string()));
3401 });
3402
3403 let result = outcome
3407 .recv_timeout(std::time::Duration::from_secs(15))
3408 .expect("a backup of a locked source must give up within its patience");
3409 let err = result.expect_err("the source stayed locked, so the copy must fail");
3410 assert!(err.contains("locked") || err.contains("busy"), "{err}");
3411 drop(holder);
3412 }
3413
3414 fn briefly_locked_source(hold: std::time::Duration) -> (tempfile::TempDir, String) {
3416 let dir = tempfile::tempdir().unwrap();
3417 let path = dir.path().join("briefly-locked.db");
3418 let path = path.to_str().unwrap().to_string();
3419 {
3420 let conn = Connection::open(&path).unwrap();
3421 conn.execute_batch(
3422 "PRAGMA journal_mode = DELETE; CREATE TABLE t (v); INSERT INTO t VALUES (1);",
3423 )
3424 .unwrap();
3425 }
3426 let (locked, is_locked) = std::sync::mpsc::channel();
3427 let holder_path = path.clone();
3428 std::thread::spawn(move || {
3429 let holder = Connection::open(&holder_path).unwrap();
3430 holder
3431 .execute_batch("BEGIN EXCLUSIVE; INSERT INTO t VALUES (2);")
3432 .unwrap();
3433 let _ = locked.send(());
3434 std::thread::sleep(hold);
3435 holder.execute_batch("COMMIT").unwrap();
3436 });
3437 is_locked.recv().unwrap();
3438 (dir, path)
3439 }
3440
3441 fn copy_rows(source: &Connection) -> Result<i64> {
3442 let mut dest = Connection::open_in_memory().unwrap();
3443 {
3444 let backup = rusqlite::backup::Backup::new(source, &mut dest)?;
3445 run_backup(&backup)?;
3446 }
3447 Ok(dest.query_row("SELECT COUNT(*) FROM t", [], |row| row.get(0))?)
3448 }
3449
3450 #[test]
3451 fn a_backup_waits_out_a_short_lock() {
3452 let (_dir, path) = briefly_locked_source(std::time::Duration::from_millis(50));
3453 let source = Connection::open(&path).unwrap();
3455 assert_eq!(
3456 copy_rows(&source).unwrap(),
3457 2,
3458 "the copy sees the committed row"
3459 );
3460 }
3461
3462 #[test]
3463 fn a_backup_retries_a_lock_that_clears() {
3464 let (_dir, path) = briefly_locked_source(std::time::Duration::from_millis(50));
3465 let source = Connection::open(&path).unwrap();
3467 source.busy_timeout(std::time::Duration::ZERO).unwrap();
3468 assert_eq!(
3469 copy_rows(&source).unwrap(),
3470 2,
3471 "the copy sees the committed row"
3472 );
3473 }
3474
3475 #[test]
3476 fn a_backup_watch_times_a_busy_spell_from_the_step_that_found_it() {
3477 let patience = std::time::Duration::from_secs(5);
3478 let t0 = std::time::Instant::now();
3479 let at = |ms: u64| t0 + std::time::Duration::from_millis(ms);
3480 let mut watch = BackupWatch::default();
3481 assert_eq!(
3483 watch.observe(BackupStep::Busy, at(0), at(4_900), patience),
3484 BackupNext::Pause
3485 );
3486 assert_eq!(
3488 watch.observe(BackupStep::Busy, at(4_910), at(5_000), patience),
3489 BackupNext::GiveUpLocked
3490 );
3491 let mut watch = BackupWatch::default();
3493 assert_eq!(
3494 watch.observe(BackupStep::Busy, at(0), at(4_000), patience),
3495 BackupNext::Pause
3496 );
3497 assert_eq!(
3498 watch.observe(
3499 BackupStep::More { remaining: 10 },
3500 at(4_010),
3501 at(4_020),
3502 patience
3503 ),
3504 BackupNext::Step
3505 );
3506 assert_eq!(
3507 watch.observe(BackupStep::Busy, at(4_030), at(8_000), patience),
3508 BackupNext::Pause
3509 );
3510 assert_eq!(
3511 watch.observe(BackupStep::Done, at(8_010), at(8_020), patience),
3512 BackupNext::Finish
3513 );
3514 }
3515
3516 #[test]
3517 fn a_backup_watch_gives_up_on_a_copy_that_keeps_starting_over() {
3518 let patience = std::time::Duration::from_secs(5);
3519 let t = std::time::Instant::now();
3520 let mut watch = BackupWatch::default();
3521 assert_eq!(
3522 watch.observe(BackupStep::More { remaining: 100 }, t, t, patience),
3523 BackupNext::Step
3524 );
3525 assert_eq!(
3527 watch.observe(BackupStep::More { remaining: 50 }, t, t, patience),
3528 BackupNext::Step
3529 );
3530 for _ in 0..BACKUP_RESTART_LIMIT {
3533 assert_eq!(
3534 watch.observe(BackupStep::More { remaining: 100 }, t, t, patience),
3535 BackupNext::Pause
3536 );
3537 assert_eq!(
3538 watch.observe(BackupStep::More { remaining: 50 }, t, t, patience),
3539 BackupNext::Step
3540 );
3541 }
3542 assert_eq!(
3543 watch.observe(BackupStep::More { remaining: 100 }, t, t, patience),
3544 BackupNext::GiveUpRestarting
3545 );
3546 }
3547
3548 #[test]
3549 fn a_backup_watch_counts_a_copy_that_starts_over_at_the_same_place() {
3550 let patience = std::time::Duration::from_secs(5);
3555 let t = std::time::Instant::now();
3556 let mut watch = BackupWatch::default();
3557 assert_eq!(
3558 watch.observe(BackupStep::More { remaining: 302 }, t, t, patience),
3559 BackupNext::Step
3560 );
3561 for restart in 1..=BACKUP_RESTART_LIMIT {
3564 assert_eq!(
3565 watch.observe(BackupStep::More { remaining: 302 }, t, t, patience),
3566 BackupNext::Pause,
3567 "restart {restart}"
3568 );
3569 }
3570 assert_eq!(
3571 watch.observe(BackupStep::More { remaining: 302 }, t, t, patience),
3572 BackupNext::GiveUpRestarting
3573 );
3574 }
3575
3576 #[test]
3577 fn a_backup_of_a_source_changed_between_steps_gives_up() {
3578 let dir = tempfile::tempdir().unwrap();
3579 let path = dir.path().join("changing.db");
3580 let path = path.to_str().unwrap().to_string();
3581 let writer = Connection::open(&path).unwrap();
3582 writer
3583 .execute_batch(
3584 "PRAGMA journal_mode = DELETE; CREATE TABLE t (v); \
3585 WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < 400) \
3586 INSERT INTO t SELECT zeroblob(3000) FROM n;",
3587 )
3588 .unwrap();
3589 let source = Connection::open(&path).unwrap();
3590 let mut dest = Connection::open_in_memory().unwrap();
3591 let backup = rusqlite::backup::Backup::new(&source, &mut dest).unwrap();
3592 let patience = std::time::Duration::from_secs(5);
3593 let mut watch = BackupWatch::default();
3594 let mut outcome = None;
3595 for step in 0..(2 * BACKUP_RESTART_LIMIT + 10) {
3600 let t = std::time::Instant::now();
3601 match watch.observe(backup_step(&backup).unwrap(), t, t, patience) {
3602 BackupNext::Step | BackupNext::Pause => {}
3603 other => {
3604 outcome = Some((step, other));
3605 break;
3606 }
3607 }
3608 writer
3609 .execute("UPDATE t SET v = randomblob(3000) WHERE rowid = 1", [])
3610 .unwrap();
3611 }
3612 let (step, next) = outcome.expect("a copy that keeps starting over must give up");
3613 assert_eq!(next, BackupNext::GiveUpRestarting);
3614 assert!(
3615 step <= BACKUP_RESTART_LIMIT + 1,
3616 "gave up after {step} steps, not at the restart limit"
3617 );
3618 }
3619
3620 #[test]
3621 fn a_restore_leaves_its_source_file_unchanged() {
3622 let (_dir, path) = v9_snapshot_path();
3623 write_v9_database(&path, 0);
3624 let before = std::fs::read(&path).unwrap();
3625
3626 let mut storage = Storage::memory().unwrap();
3627 storage.restore_from(&path).unwrap();
3628 drop(storage);
3629
3630 assert_eq!(std::fs::read(&path).unwrap(), before);
3631 assert_eq!(recorded_version(&Connection::open(&path).unwrap()), "9");
3632 }
3633
3634 fn busy_timeout_ms(conn: &Connection) -> i64 {
3635 conn.query_row("PRAGMA busy_timeout", [], |r| r.get(0))
3636 .unwrap()
3637 }
3638
3639 fn open_and_after(storage: &Storage) -> (Option<i64>, i64) {
3642 (
3643 OPENED_WITH_BUSY_TIMEOUT_MS.take(),
3644 busy_timeout_ms(storage.conn()),
3645 )
3646 }
3647
3648 #[test]
3653 fn a_database_file_waits_thirty_seconds_for_a_lock_while_opening_and_five_after() {
3654 let (_dir, path) = v9_snapshot_path();
3655 let storage = Storage::new(&path).unwrap();
3656 assert_eq!(open_and_after(&storage), (Some(30_000), 5_000));
3657 let memory = Storage::memory().unwrap();
3660 assert_eq!(open_and_after(&memory), (Some(5_000), 5_000));
3661 }
3662
3663 #[cfg(feature = "_has-encryption")]
3664 #[test]
3665 fn an_encrypted_database_file_waits_thirty_seconds_for_a_lock_while_opening_and_five_after() {
3666 const KEY: &str = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
3667 let (_dir, path) = v9_snapshot_path();
3668 let storage = Storage::new_encrypted(&path, KEY).unwrap();
3669 assert_eq!(open_and_after(&storage), (Some(30_000), 5_000));
3670 }
3671
3672 #[test]
3678 fn a_second_open_waits_for_a_migration_another_holds_then_finds_it_done() {
3679 let (_dir, path) = v9_snapshot_path();
3680 write_v9_database(&path, 0);
3681 let stale_index = gsi_rows(&Connection::open(&path).unwrap(), "byKey");
3682
3683 let first = Connection::open(&path).unwrap();
3688 first.execute_batch("BEGIN IMMEDIATE").unwrap();
3689 first
3690 .execute_batch(&format!(
3691 "UPDATE \"Vec\" SET item_size = 1;
3692 UPDATE _config SET value = '{SCHEMA_VERSION}' WHERE key = 'schema_version';"
3693 ))
3694 .unwrap();
3695
3696 let (started, starting) = std::sync::mpsc::channel();
3697 let second_path = path.clone();
3698 let second = std::thread::spawn(move || {
3699 started.send(()).unwrap();
3700 let storage = Storage::new(&second_path)?;
3701 Ok::<_, DynoxideError>((
3702 recorded_version(storage.conn()),
3703 vec_size(storage.conn()),
3704 gsi_rows(storage.conn(), "byKey"),
3705 ))
3706 });
3707 starting.recv().unwrap();
3708 std::thread::sleep(std::time::Duration::from_millis(200));
3709 first.execute_batch("COMMIT").unwrap();
3710
3711 let (version, size, index) = second.join().unwrap().unwrap();
3712 assert_eq!(version, SCHEMA_VERSION.to_string());
3713 assert_eq!(
3714 size, 1,
3715 "the second open must not recompute the sizes again"
3716 );
3717 assert_eq!(
3718 index, stale_index,
3719 "the second open must not rebuild the index the first left"
3720 );
3721 }
3722
3723 #[test]
3728 fn a_database_that_records_no_version_is_brought_up_to_date() {
3729 let (_dir, path) = v9_snapshot_path();
3730 write_v9_database(&path, 0);
3731 let table_id = |conn: &Connection| -> String {
3732 conn.query_row(
3733 "SELECT table_id FROM _tables WHERE table_name = 'Legacy'",
3734 [],
3735 |r| r.get(0),
3736 )
3737 .unwrap()
3738 };
3739 let (stale_size, id_before) = {
3740 let conn = Connection::open(&path).unwrap();
3741 conn.execute("DELETE FROM _config WHERE key = 'schema_version'", [])
3742 .unwrap();
3743 (vec_size(&conn), table_id(&conn))
3744 };
3745
3746 let storage = Storage::new(&path).unwrap();
3747 assert_eq!(recorded_version(storage.conn()), SCHEMA_VERSION.to_string());
3748 assert_eq!(
3749 vec_size(storage.conn()),
3750 stale_size - 7,
3751 "vector table sizes are recomputed"
3752 );
3753 let composite = crate::actions::gsi::composite_key_string(&[
3754 &AttributeValue::S("m".into()),
3755 &AttributeValue::S("2".into()),
3756 ])
3757 .unwrap();
3758 assert!(
3759 gsi_rows(storage.conn(), "byKey")
3760 .iter()
3761 .any(|[_, gsi_sk, _, _, _]| *gsi_sk == composite),
3762 "multi-attribute indexes are rebuilt"
3763 );
3764 assert_eq!(table_id(storage.conn()), id_before, "a table keeps its id");
3765 drop(storage);
3766
3767 let db = crate::Database::new(&path).unwrap();
3768 let item = db
3769 .get_item(
3770 serde_json::from_value(serde_json::json!({
3771 "TableName": "Legacy",
3772 "Key": {"pk": {"S": "1"}}
3773 }))
3774 .unwrap(),
3775 )
3776 .unwrap()
3777 .item;
3778 assert!(item.is_some(), "the items read back");
3779 }
3780
3781 fn stamp_newer(conn: &Connection) {
3785 conn.execute(
3786 "UPDATE _config SET value = ?1 WHERE key = 'schema_version'",
3787 [(SCHEMA_VERSION + 1).to_string()],
3788 )
3789 .unwrap();
3790 conn.query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |_| Ok(()))
3791 .unwrap();
3792 }
3793
3794 fn write_newer_database(path: &str) {
3796 drop(crate::Database::new(path).unwrap());
3797 stamp_newer(&Connection::open(path).unwrap());
3798 }
3799
3800 fn assert_refused_as_newer(err: DynoxideError) {
3801 let message = err.to_string();
3802 assert_eq!(
3803 message,
3804 crate::storage_backend::schema::newer_than_this_build(SCHEMA_VERSION + 1)
3805 );
3806 assert!(
3807 message.contains(&format!("schema version {}", SCHEMA_VERSION + 1))
3808 && message.contains(&format!("up to schema version {SCHEMA_VERSION}")),
3809 "{message}"
3810 );
3811 }
3812
3813 #[test]
3814 fn a_database_a_newer_build_wrote_is_refused_and_left_as_it_was() {
3815 let (_dir, path) = v9_snapshot_path();
3816 write_newer_database(&path);
3817 let before = std::fs::read(&path).unwrap();
3818
3819 assert_refused_as_newer(Storage::new(&path).map(drop).unwrap_err());
3820 assert_refused_as_newer(crate::Database::new(&path).map(drop).unwrap_err());
3821
3822 assert_eq!(std::fs::read(&path).unwrap(), before);
3823 assert_eq!(
3824 recorded_version(&Connection::open(&path).unwrap()),
3825 (SCHEMA_VERSION + 1).to_string()
3826 );
3827 }
3828
3829 #[test]
3833 fn a_newer_stamp_still_in_the_wal_is_refused_and_both_files_are_left_as_they_were() {
3834 let (_dir, path) = v9_snapshot_path();
3835 drop(crate::Database::new(&path).unwrap());
3836 let writer = Connection::open(&path).unwrap();
3837 writer
3838 .execute_batch("PRAGMA wal_autocheckpoint = 0")
3839 .unwrap();
3840 writer
3841 .execute(
3842 "UPDATE _config SET value = ?1 WHERE key = 'schema_version'",
3843 [(SCHEMA_VERSION + 1).to_string()],
3844 )
3845 .unwrap();
3846 let wal = format!("{path}-wal");
3847 let main_before = std::fs::read(&path).unwrap();
3848 let wal_before = std::fs::read(&wal).unwrap();
3849 assert!(!wal_before.is_empty(), "the stamp is in the WAL");
3850
3851 assert_refused_as_newer(Storage::new(&path).map(drop).unwrap_err());
3852 assert_refused_as_newer(crate::Database::new(&path).map(drop).unwrap_err());
3853
3854 assert_eq!(std::fs::read(&path).unwrap(), main_before);
3855 assert_eq!(std::fs::read(&wal).unwrap(), wal_before);
3856 assert_eq!(recorded_version(&writer), (SCHEMA_VERSION + 1).to_string());
3857 }
3858
3859 #[cfg(feature = "_has-encryption")]
3860 #[test]
3861 fn an_encrypted_database_a_newer_build_wrote_is_refused_and_left_as_it_was() {
3862 const KEY: &str = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
3863 let (_dir, path) = v9_snapshot_path();
3864 drop(crate::Database::new_encrypted(&path, KEY).unwrap());
3865 {
3866 let conn = Connection::open(&path).unwrap();
3867 conn.pragma_update(None, "key", format!("x'{KEY}'"))
3868 .unwrap();
3869 stamp_newer(&conn);
3870 }
3871 let before = std::fs::read(&path).unwrap();
3872
3873 assert_refused_as_newer(
3874 crate::Database::new_encrypted(&path, KEY)
3875 .map(drop)
3876 .unwrap_err(),
3877 );
3878
3879 assert_eq!(std::fs::read(&path).unwrap(), before);
3880 }
3881
3882 #[test]
3883 fn a_restore_refuses_a_snapshot_a_newer_build_wrote() {
3884 let (_dir, path) = v9_snapshot_path();
3885 write_newer_database(&path);
3886 let before = std::fs::read(&path).unwrap();
3887
3888 let mut storage = Storage::memory().unwrap();
3889 mark_live(&storage);
3890 assert_refused_as_newer(storage.restore_from(&path).unwrap_err());
3891 assert!(still_live(&storage), "the database must be left as it was");
3892 assert_eq!(recorded_version(storage.conn()), SCHEMA_VERSION.to_string());
3893 drop(storage);
3894
3895 assert_eq!(std::fs::read(&path).unwrap(), before);
3896 }
3897
3898 #[test]
3899 fn test_wal_mode_enabled() {
3900 let storage = test_storage();
3901 let mode: String = storage
3902 .conn()
3903 .query_row("PRAGMA journal_mode", [], |row| row.get(0))
3904 .unwrap();
3905 assert!(mode == "wal" || mode == "memory", "Got mode: {mode}");
3907 }
3908
3909 #[test]
3915 fn fnv1a_hash_matches_known_vectors() {
3916 let storage = test_storage();
3917 let cases: [(&str, i64); 6] = [
3918 ("", 2166136261),
3919 ("a", 3826002220),
3920 ("u#1", 2199603432),
3921 ("artist#42", 2385694177),
3922 ("café", 2821410889),
3923 ("tenant#9007199254740993", 2022216178),
3924 ];
3925 for (input, expected) in cases {
3926 let got: i64 = storage
3927 .conn()
3928 .query_row("SELECT fnv1a_hash(?1)", [input], |row| row.get(0))
3929 .unwrap();
3930 assert_eq!(got, expected, "fnv1a_hash({input:?})");
3931 }
3932 }
3933
3934 #[test]
3935 fn test_table_metadata_crud() {
3936 let storage = test_storage();
3937
3938 assert!(!storage.table_exists("TestTable").unwrap());
3940 assert!(storage.list_table_names().unwrap().is_empty());
3941
3942 storage
3944 .insert_table_metadata(&CreateTableMetadata {
3945 table_name: "TestTable",
3946 key_schema: r#"[{"AttributeName":"pk","KeyType":"HASH"}]"#,
3947 attribute_definitions: r#"[{"AttributeName":"pk","AttributeType":"S"}]"#,
3948 created_at: 1000000,
3949 ..Default::default()
3950 })
3951 .unwrap();
3952
3953 assert!(storage.table_exists("TestTable").unwrap());
3954 assert_eq!(storage.list_table_names().unwrap(), vec!["TestTable"]);
3955
3956 let meta = storage.get_table_metadata("TestTable").unwrap().unwrap();
3958 assert_eq!(meta.table_name, "TestTable");
3959 assert_eq!(meta.table_status, "ACTIVE");
3960 assert_eq!(meta.created_at, 1000000);
3961
3962 assert!(storage.delete_table_metadata("TestTable").unwrap());
3964 assert!(!storage.table_exists("TestTable").unwrap());
3965 }
3966
3967 #[test]
3968 fn test_create_and_drop_data_table() {
3969 let storage = test_storage();
3970 storage.create_data_table("MyTable").unwrap();
3971
3972 storage
3974 .put_item("MyTable", "pk1", "", r#"{"pk":{"S":"pk1"}}"#, 10)
3975 .unwrap();
3976
3977 let item = storage.get_item("MyTable", "pk1", "").unwrap();
3978 assert!(item.is_some());
3979
3980 storage.drop_data_table("MyTable").unwrap();
3981 }
3982
3983 #[test]
3984 fn test_item_crud() {
3985 let storage = test_storage();
3986 storage.create_data_table("Items").unwrap();
3987
3988 let old = storage
3990 .put_item(
3991 "Items",
3992 "user#1",
3993 "profile",
3994 r#"{"name":{"S":"Alice"}}"#,
3995 20,
3996 )
3997 .unwrap();
3998 assert!(old.is_none()); let item = storage.get_item("Items", "user#1", "profile").unwrap();
4002 assert_eq!(item.unwrap(), r#"{"name":{"S":"Alice"}}"#);
4003
4004 let old = storage
4006 .put_item("Items", "user#1", "profile", r#"{"name":{"S":"Bob"}}"#, 18)
4007 .unwrap();
4008 assert_eq!(old.unwrap(), r#"{"name":{"S":"Alice"}}"#);
4009
4010 let deleted = storage.delete_item("Items", "user#1", "profile").unwrap();
4012 assert_eq!(deleted.unwrap(), r#"{"name":{"S":"Bob"}}"#);
4013
4014 assert!(
4016 storage
4017 .get_item("Items", "user#1", "profile")
4018 .unwrap()
4019 .is_none()
4020 );
4021 }
4022
4023 #[test]
4024 fn test_query_items() {
4025 let storage = test_storage();
4026 storage.create_data_table("Orders").unwrap();
4027
4028 for i in 1..=5 {
4030 let sk = format!("order#{i:03}");
4031 let json = format!(r#"{{"id":{{"N":"{i}"}}}}"#);
4032 storage
4033 .put_item("Orders", "user#1", &sk, &json, 10)
4034 .unwrap();
4035 }
4036
4037 let results = storage
4039 .query_items(
4040 "Orders",
4041 "user#1",
4042 &QueryParams {
4043 forward: true,
4044 ..Default::default()
4045 },
4046 )
4047 .unwrap();
4048 assert_eq!(results.len(), 5);
4049 assert_eq!(results[0].1, "order#001"); let results = storage
4053 .query_items(
4054 "Orders",
4055 "user#1",
4056 &QueryParams {
4057 forward: true,
4058 limit: Some(2),
4059 ..Default::default()
4060 },
4061 )
4062 .unwrap();
4063 assert_eq!(results.len(), 2);
4064
4065 let results = storage
4067 .query_items(
4068 "Orders",
4069 "user#1",
4070 &QueryParams {
4071 forward: false,
4072 limit: Some(2),
4073 ..Default::default()
4074 },
4075 )
4076 .unwrap();
4077 assert_eq!(results.len(), 2);
4078 assert_eq!(results[0].1, "order#005"); }
4080
4081 #[test]
4082 fn test_scan_items() {
4083 let storage = test_storage();
4084 storage.create_data_table("ScanTest").unwrap();
4085
4086 storage.put_item("ScanTest", "a", "1", r#"{}"#, 2).unwrap();
4087 storage.put_item("ScanTest", "b", "2", r#"{}"#, 2).unwrap();
4088 storage.put_item("ScanTest", "c", "3", r#"{}"#, 2).unwrap();
4089
4090 let results = storage.scan_items("ScanTest", &Default::default()).unwrap();
4091 assert_eq!(results.len(), 3);
4092
4093 let results = storage
4095 .scan_items(
4096 "ScanTest",
4097 &ScanParams {
4098 limit: Some(2),
4099 ..Default::default()
4100 },
4101 )
4102 .unwrap();
4103 assert_eq!(results.len(), 2);
4104
4105 let results = storage
4107 .scan_items(
4108 "ScanTest",
4109 &ScanParams {
4110 limit: Some(2),
4111 exclusive_start_pk: Some("a"),
4112 exclusive_start_sk: Some("1"),
4113 ..Default::default()
4114 },
4115 )
4116 .unwrap();
4117 assert_eq!(results.len(), 2);
4118 assert_eq!(results[0].0, "b"); }
4120
4121 #[test]
4122 fn test_count_items() {
4123 let storage = test_storage();
4124 storage.create_data_table("CountTest").unwrap();
4125
4126 assert_eq!(storage.count_items("CountTest").unwrap(), 0);
4127
4128 storage.put_item("CountTest", "a", "", r#"{}"#, 2).unwrap();
4129 storage.put_item("CountTest", "b", "", r#"{}"#, 2).unwrap();
4130
4131 assert_eq!(storage.count_items("CountTest").unwrap(), 2);
4132 }
4133
4134 #[test]
4135 fn test_gsi_table_lifecycle() {
4136 let storage = test_storage();
4137 storage.create_gsi_table("Orders", "ByDate").unwrap();
4138
4139 let gsi_name = "Orders::gsi::ByDate";
4141 let sql = format!(
4142 "INSERT INTO \"{}\" (gsi_pk, gsi_sk, table_pk, table_sk, item_json) VALUES (?1, ?2, ?3, ?4, ?5)",
4143 gsi_name.replace('"', "\"\"")
4144 );
4145 storage
4146 .conn()
4147 .execute(
4148 &sql,
4149 params!["2024-01-01", "001", "user#1", "order#001", r#"{}"#],
4150 )
4151 .unwrap();
4152
4153 storage.drop_gsi_table("Orders", "ByDate").unwrap();
4154 }
4155
4156 #[test]
4157 fn test_nonexistent_table_metadata() {
4158 let storage = test_storage();
4159 assert!(storage.get_table_metadata("Nonexistent").unwrap().is_none());
4160 assert!(!storage.delete_table_metadata("Nonexistent").unwrap());
4161 }
4162
4163 #[test]
4164 fn test_metadata_cache_hit() {
4165 let storage = test_storage();
4166 storage
4167 .insert_table_metadata(&CreateTableMetadata {
4168 table_name: "CacheTest",
4169 key_schema: r#"[{"AttributeName":"pk","KeyType":"HASH"}]"#,
4170 attribute_definitions: r#"[{"AttributeName":"pk","AttributeType":"S"}]"#,
4171 created_at: 1000000,
4172 ..Default::default()
4173 })
4174 .unwrap();
4175
4176 let meta1 = storage.get_table_metadata("CacheTest").unwrap().unwrap();
4178 assert_eq!(meta1.table_name, "CacheTest");
4179
4180 let meta2 = storage.get_table_metadata("CacheTest").unwrap().unwrap();
4182 assert_eq!(meta2.table_name, "CacheTest");
4183 assert_eq!(meta1.created_at, meta2.created_at);
4184
4185 assert!(storage.metadata_cache.borrow().contains_key("CacheTest"));
4187 }
4188
4189 #[test]
4190 fn test_metadata_cache_invalidated_on_delete() {
4191 let storage = test_storage();
4192 storage
4193 .insert_table_metadata(&CreateTableMetadata {
4194 table_name: "DelCache",
4195 key_schema: r#"[{"AttributeName":"pk","KeyType":"HASH"}]"#,
4196 attribute_definitions: r#"[{"AttributeName":"pk","AttributeType":"S"}]"#,
4197 created_at: 1000000,
4198 ..Default::default()
4199 })
4200 .unwrap();
4201
4202 storage.get_table_metadata("DelCache").unwrap();
4204 assert!(storage.metadata_cache.borrow().contains_key("DelCache"));
4205
4206 storage.delete_table_metadata("DelCache").unwrap();
4208 assert!(!storage.metadata_cache.borrow().contains_key("DelCache"));
4209 }
4210
4211 #[test]
4212 fn test_metadata_cache_invalidated_on_stream_enable() {
4213 let storage = test_storage();
4214 storage
4215 .insert_table_metadata(&CreateTableMetadata {
4216 table_name: "StreamCache",
4217 key_schema: r#"[{"AttributeName":"pk","KeyType":"HASH"}]"#,
4218 attribute_definitions: r#"[{"AttributeName":"pk","AttributeType":"S"}]"#,
4219 created_at: 1000000,
4220 ..Default::default()
4221 })
4222 .unwrap();
4223
4224 let meta = storage.get_table_metadata("StreamCache").unwrap().unwrap();
4226 assert!(!meta.stream_enabled);
4227
4228 storage
4230 .enable_stream("StreamCache", "NEW_AND_OLD_IMAGES", "2024-01-01T00:00:00")
4231 .unwrap();
4232 assert!(!storage.metadata_cache.borrow().contains_key("StreamCache"));
4233
4234 let meta = storage.get_table_metadata("StreamCache").unwrap().unwrap();
4236 assert!(meta.stream_enabled);
4237 }
4238
4239 #[test]
4240 fn test_metadata_cache_invalidated_on_ttl_update() {
4241 let storage = test_storage();
4242 storage
4243 .insert_table_metadata(&CreateTableMetadata {
4244 table_name: "TtlCache",
4245 key_schema: r#"[{"AttributeName":"pk","KeyType":"HASH"}]"#,
4246 attribute_definitions: r#"[{"AttributeName":"pk","AttributeType":"S"}]"#,
4247 created_at: 1000000,
4248 ..Default::default()
4249 })
4250 .unwrap();
4251
4252 let meta = storage.get_table_metadata("TtlCache").unwrap().unwrap();
4254 assert!(!meta.ttl_enabled);
4255
4256 storage
4258 .update_ttl_config("TtlCache", Some("expires_at"), true)
4259 .unwrap();
4260 assert!(!storage.metadata_cache.borrow().contains_key("TtlCache"));
4261
4262 let meta = storage.get_table_metadata("TtlCache").unwrap().unwrap();
4264 assert!(meta.ttl_enabled);
4265 assert_eq!(meta.ttl_attribute, Some("expires_at".to_string()));
4266 }
4267
4268 #[test]
4269 fn test_num_to_buffer_zero() {
4270 assert_eq!(num_to_buffer("0"), vec![0x80]);
4272 assert_eq!(num_to_buffer("-0"), vec![0x80]);
4273 }
4274
4275 #[test]
4276 fn test_hash_prefix_string_keys() {
4277 let h1 = compute_hash_prefix(&AttributeValue::S("3635".into()));
4280 let h2 = compute_hash_prefix(&AttributeValue::S("228".into()));
4281 let h3 = compute_hash_prefix(&AttributeValue::S("1668".into()));
4282 let h4 = compute_hash_prefix(&AttributeValue::S("3435".into()));
4283
4284 assert_eq!(
4287 hash_bucket(&h1),
4288 0,
4289 "3635 should be bucket 0, got hash {h1}"
4290 );
4291 assert_eq!(hash_bucket(&h2), 0, "228 should be bucket 0, got hash {h2}");
4292
4293 assert_eq!(
4295 hash_bucket(&h3),
4296 1,
4297 "1668 should be bucket 1, got hash {h3}"
4298 );
4299
4300 assert_eq!(
4302 hash_bucket(&h4),
4303 4,
4304 "3435 should be bucket 4, got hash {h4}"
4305 );
4306 }
4307
4308 #[test]
4309 fn a_key_string_read_back_hashes_as_the_key_did() {
4310 let mut keys: Vec<AttributeValue> = [
4311 "0",
4312 "-0",
4313 "1",
4314 "-1",
4315 "12300",
4316 "1.23e4",
4317 "0.000123",
4318 "-0.5",
4319 "123.4560",
4320 "99999999999999999999999999999999999999",
4321 "-1E-130",
4322 "9.9E125",
4323 ]
4324 .iter()
4325 .map(|n| AttributeValue::N((*n).to_string()))
4326 .collect();
4327 keys.extend(["", "a", "item-0042", "ü:é"].map(|s| AttributeValue::S(s.into())));
4328 keys.extend([vec![], vec![0u8], vec![0xff, 0x00, 0x10]].map(AttributeValue::B));
4329 for key in keys {
4330 let stored = key.to_key_string().unwrap();
4331 let read_back = crate::types::key_value_from_key_string(&stored)
4332 .unwrap_or_else(|| panic!("{stored} did not read back"));
4333 assert_eq!(
4334 compute_hash_prefix(&read_back),
4335 compute_hash_prefix(&key),
4336 "{key:?} via {stored}"
4337 );
4338 }
4339 assert!(crate::types::key_value_from_key_string("X:1").is_none());
4340 assert!(crate::types::key_value_from_key_string("N:12").is_none());
4341 }
4342
4343 #[test]
4344 fn test_hash_prefix_number_keys() {
4345 let h1 = compute_hash_prefix(&AttributeValue::N("251".into()));
4348 assert_eq!(hash_bucket(&h1), 1, "251 should be bucket 1, got hash {h1}");
4349
4350 let h2 = compute_hash_prefix(&AttributeValue::N("2388".into()));
4352 assert_eq!(
4353 hash_bucket(&h2),
4354 4095,
4355 "2388 should be bucket 4095, got hash {h2}"
4356 );
4357 }
4358
4359 #[test]
4360 fn test_hash_in_segment() {
4361 assert!(hash_in_segment("000000", 0, 4096));
4363 assert!(!hash_in_segment("000000", 1, 4096));
4364
4365 assert!(hash_in_segment("001000", 1, 4096));
4367 assert!(!hash_in_segment("001000", 0, 4096));
4368
4369 assert!(hash_in_segment("fff000", 4095, 4096));
4371 assert!(!hash_in_segment("fff000", 0, 4096));
4372
4373 assert!(hash_in_segment("000000", 0, 2));
4375 assert!(hash_in_segment("7ff000", 0, 2));
4376 assert!(hash_in_segment("800000", 1, 2));
4377 assert!(hash_in_segment("fff000", 1, 2));
4378 }
4379}