1use crate::db::Database;
46use crate::sql::{self, Dialect, Value};
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum ColumnKind {
55 Uuid,
56 Text,
57 Blob,
58 I64,
59 Bool,
60}
61
62#[derive(Debug, Clone, Copy)]
64pub struct TableSpec {
65 pub name: &'static str,
67 pub key: &'static [&'static str],
70 pub columns: &'static [(&'static str, ColumnKind)],
72}
73
74use ColumnKind::{Blob, Bool, I64, Text, Uuid};
75
76pub const TABLES: &[TableSpec] = &[
85 TableSpec {
87 name: "nonces",
88 key: &["value"],
89 columns: &[("value", Text), ("created_at", I64)],
90 },
91 TableSpec {
92 name: "eab_keys",
93 key: &["kid"],
94 columns: &[
95 ("kid", Uuid),
96 ("secret", Blob),
97 ("label", Text),
98 ("profile", Text),
99 ("status", Text),
100 ("created_at", I64),
101 ],
102 },
103 TableSpec {
104 name: "jobs",
105 key: &["id"],
106 columns: &[
107 ("id", Uuid),
108 ("kind", Text),
109 ("dedup_key", Text),
110 ("payload", Text),
111 ("status", Text),
112 ("run_at", I64),
113 ("attempts", I64),
114 ("max_attempts", I64),
115 ("deadline", I64),
116 ("lease_until", I64),
117 ("lease_owner", Text),
118 ("last_error", Text),
119 ("created_at", I64),
120 ("updated_at", I64),
121 ],
122 },
123 TableSpec {
124 name: "audit_log",
125 key: &["id"],
126 columns: &[
127 ("id", I64),
128 ("created_at", I64),
129 ("event", Text),
130 ("outcome", Text),
131 ("profile", Text),
132 ("actor_kind", Text),
133 ("actor_id", Text),
134 ("account_id", Text),
135 ("order_id", Text),
136 ("cert_serial", Text),
137 ("identifiers", Text),
138 ("client_ip", Text),
139 ("client_ptr", Text),
140 ("user_agent", Text),
141 ("request_id", Text),
142 ("reason", Text),
143 ("detail", Text),
144 ],
145 },
146 TableSpec {
147 name: "revocations",
148 key: &["issuer", "serial"],
149 columns: &[
150 ("issuer", Text),
151 ("serial", Text),
152 ("revoked_at", I64),
153 ("reason", I64),
154 ("not_after", I64),
155 ],
156 },
157 TableSpec {
158 name: "crls",
159 key: &["issuer"],
160 columns: &[
161 ("issuer", Text),
162 ("crl_number", I64),
163 ("der", Blob),
164 ("this_update", I64),
165 ("next_update", I64),
166 ],
167 },
168 TableSpec {
169 name: "http01_tokens",
170 key: &["token"],
171 columns: &[
172 ("token", Text),
173 ("key_authorization", Text),
174 ("created_at", I64),
175 ("expires_at", I64),
176 ],
177 },
178 TableSpec {
179 name: "admin_users",
180 key: &["id"],
181 columns: &[
182 ("id", Uuid),
183 ("username", Text),
184 ("password_hash", Text),
185 ("status", Text),
186 ("totp_secret", Blob),
187 ("totp_pending_secret", Blob),
188 ("totp_last_step", I64),
189 ("created_at", I64),
190 ("updated_at", I64),
191 ("last_login_at", I64),
192 ("role", Text),
193 ("contact_email", Text),
194 ("known_login_ips", Text),
195 ],
196 },
197 TableSpec {
199 name: "accounts",
200 key: &["id"],
201 columns: &[
202 ("id", Uuid),
203 ("profile", Text),
204 ("pubkey", Blob),
205 ("contact", Text),
206 ("status", Text),
207 ("created_at", I64),
208 ("created_ip", Text),
209 ("created_ptr", Text),
210 ("last_seen_at", I64),
211 ("last_seen_ip", Text),
212 ("last_seen_ptr", Text),
213 ("eab_kid", Uuid),
214 ("terms_of_service_agreed", Bool),
215 ],
216 },
217 TableSpec {
218 name: "orders",
219 key: &["id"],
220 columns: &[
221 ("id", Uuid),
222 ("profile", Text),
223 ("account_id", Uuid),
224 ("status", Text),
225 ("identifiers", Text),
226 ("expires", I64),
227 ("not_before", I64),
228 ("not_after", I64),
229 ("error", Text),
230 ("certificate", Text),
231 ("replaces", Text),
232 ("created_at", I64),
233 ("created_ip", Text),
234 ("created_ptr", Text),
235 ("cert_serial", Text),
236 ("cert_pubkey", Blob),
237 ("revoked_at", I64),
238 ("revocation_reason", I64),
239 ("cert_not_after", I64),
240 ],
241 },
242 TableSpec {
243 name: "authorizations",
244 key: &["id"],
245 columns: &[
246 ("id", Uuid),
247 ("order_id", Uuid),
248 ("identifier", Text),
249 ("status", Text),
250 ("expires", I64),
251 ("created_at", I64),
252 ],
253 },
254 TableSpec {
255 name: "challenges",
256 key: &["id"],
257 columns: &[
258 ("id", Uuid),
259 ("authz_id", Uuid),
260 ("type", Text),
261 ("token", Text),
262 ("status", Text),
263 ("validated", I64),
264 ("created_at", I64),
265 ("error", Text),
266 ],
267 },
268 TableSpec {
269 name: "upstream_orders",
270 key: &["order_id"],
271 columns: &[
272 ("order_id", Uuid),
273 ("upstream_order_url", Text),
274 ("upstream_finalize_url", Text),
275 ("upstream_certificate_url", Text),
276 ("csr_der", Blob),
277 ("status", Text),
278 ("error", Text),
279 ("created_at", I64),
280 ("updated_at", I64),
281 ("client_ip", Text),
282 ("client_ptr", Text),
283 ("user_agent", Text),
284 ("request_id", Text),
285 ],
286 },
287 TableSpec {
288 name: "admin_sessions",
289 key: &["token_hash"],
290 columns: &[
291 ("token_hash", Text),
292 ("user_id", Uuid),
293 ("csrf_token", Text),
294 ("state", Text),
295 ("mfa_attempts", I64),
296 ("created_at", I64),
297 ("expires_at", I64),
298 ("last_seen_at", I64),
299 ("created_ip", Text),
300 ("user_agent", Text),
301 ],
302 },
303 TableSpec {
304 name: "admin_recovery_codes",
305 key: &["id"],
306 columns: &[
307 ("id", Uuid),
308 ("user_id", Uuid),
309 ("code_hash", Text),
310 ("created_at", I64),
311 ("used_at", I64),
312 ],
313 },
314];
315
316const BATCH: i64 = 1000;
321
322#[derive(Debug, Clone, PartialEq, Eq)]
324pub struct TableCount {
325 pub table: &'static str,
326 pub rows: u64,
327}
328
329#[derive(Debug, Clone, PartialEq, Eq)]
331pub struct TransferReport {
332 pub tables: Vec<TableCount>,
333}
334
335impl TransferReport {
336 #[must_use]
338 pub fn total(&self) -> u64 {
339 self.tables.iter().map(|table| table.rows).sum()
340 }
341}
342
343pub async fn non_empty_tables(database: &Database) -> Result<Vec<TableCount>, sqlx::Error> {
349 let mut found = Vec::new();
350 for table in TABLES {
351 let rows = count(table.name, database).await?;
352 if rows > 0 {
353 found.push(TableCount {
354 table: table.name,
355 rows,
356 });
357 }
358 }
359 Ok(found)
360}
361
362async fn count(table: &'static str, database: &Database) -> Result<u64, sqlx::Error> {
364 let sql = format!("SELECT COUNT(*) FROM {table};");
365 let count: i64 = sql::query(sqlx::AssertSqlSafe(sql))
366 .fetch_one(database)
367 .await?
368 .try_get(0usize)?;
369 Ok(u64::try_from(count).unwrap_or(0))
370}
371
372impl Database {
373 pub async fn transfer_to(&self, target: &Database) -> Result<TransferReport, sqlx::Error> {
383 let mut tx = target.write_transaction().await?;
384 let mut tables = Vec::with_capacity(TABLES.len());
385
386 for spec in TABLES {
387 let rows = copy_table(spec, self, &mut tx).await?;
388 tables.push(TableCount {
389 table: spec.name,
390 rows,
391 });
392 }
393
394 if target.dialect() == Dialect::Postgres {
398 sql::query(
399 "SELECT setval(pg_get_serial_sequence('audit_log', 'id'), \
400 (SELECT COALESCE(MAX(id), 1) FROM audit_log));",
401 )
402 .fetch_one(tx.conn())
403 .await?;
404 }
405
406 tx.commit().await?;
407 Ok(TransferReport { tables })
408 }
409}
410
411async fn copy_table(
413 spec: &TableSpec,
414 source: &Database,
415 tx: &mut crate::db::Tx,
416) -> Result<u64, sqlx::Error> {
417 let names = spec
418 .columns
419 .iter()
420 .map(|(name, _)| quote(name))
421 .collect::<Vec<_>>()
422 .join(", ");
423 let order = spec
424 .key
425 .iter()
426 .map(|name| quote(name))
427 .collect::<Vec<_>>()
428 .join(", ");
429
430 let mut after: Option<Vec<Value>> = None;
431 let mut copied = 0u64;
432
433 loop {
434 let batch = read_batch(spec, source, &names, &order, after.as_deref()).await?;
435 if batch.is_empty() {
436 return Ok(copied);
437 }
438
439 after = Some(key_of(spec, batch.last().expect("the batch is not empty")));
440 copied += batch.len() as u64;
441 write_batch(spec, &names, &batch, tx).await?;
442
443 if batch.len() < usize::try_from(BATCH).unwrap_or(usize::MAX) {
444 return Ok(copied);
445 }
446 }
447}
448
449async fn read_batch(
456 spec: &TableSpec,
457 source: &Database,
458 names: &str,
459 order: &str,
460 after: Option<&[Value]>,
461) -> Result<Vec<Vec<Value>>, sqlx::Error> {
462 let table = spec.name;
463 let where_clause = match after {
464 None => String::new(),
465 Some(_) => {
466 let markers = vec!["?"; spec.key.len()].join(", ");
467 match spec.key.len() {
468 1 => format!(" WHERE {order} > {markers}"),
469 _ => format!(" WHERE ({order}) > ({markers})"),
470 }
471 }
472 };
473 let sql = format!("SELECT {names} FROM {table}{where_clause} ORDER BY {order} LIMIT {BATCH};");
474
475 let mut query = sql::query(sqlx::AssertSqlSafe(sql));
476 for value in after.unwrap_or(&[]) {
477 query = query.bind(value.clone());
478 }
479
480 let rows = query.fetch_all(source).await?;
481 rows.iter()
482 .map(|row| {
483 spec.columns
484 .iter()
485 .map(|(name, kind)| read(row, name, *kind))
486 .collect::<Result<Vec<_>, _>>()
487 })
488 .collect()
489}
490
491async fn write_batch(
493 spec: &TableSpec,
494 names: &str,
495 batch: &[Vec<Value>],
496 tx: &mut crate::db::Tx,
497) -> Result<(), sqlx::Error> {
498 let table = spec.name;
499 let row = format!("({})", vec!["?"; spec.columns.len()].join(", "));
500 let values = vec![row; batch.len()].join(", ");
501
502 let overriding = match (tx.conn().dialect(), table) {
506 (Dialect::Postgres, "audit_log") => " OVERRIDING SYSTEM VALUE",
507 _ => "",
508 };
509
510 let sql = format!("INSERT INTO {table} ({names}){overriding} VALUES {values};");
511 let mut query = sql::query(sqlx::AssertSqlSafe(sql));
512 for row in batch {
513 for value in row {
514 query = query.bind(value.clone());
515 }
516 }
517 query.execute(tx.conn()).await?;
518 Ok(())
519}
520
521fn key_of(spec: &TableSpec, row: &[Value]) -> Vec<Value> {
523 spec.key
524 .iter()
525 .map(|key| {
526 let index = spec
527 .columns
528 .iter()
529 .position(|(name, _)| name == key)
530 .expect("a key column is one of the table's columns");
531 row[index].clone()
532 })
533 .collect()
534}
535
536fn read(row: &sql::Row, name: &str, kind: ColumnKind) -> Result<Value, sqlx::Error> {
538 Ok(match kind {
539 ColumnKind::Uuid => match row.try_get::<Option<uuid::Uuid>>(name)? {
540 Some(value) => Value::Uuid(value),
541 None => Value::Null(sql::NullKind::Uuid),
542 },
543 ColumnKind::Text => match row.try_get::<Option<String>>(name)? {
544 Some(value) => Value::Text(value),
545 None => Value::Null(sql::NullKind::Text),
546 },
547 ColumnKind::Blob => match row.try_get::<Option<Vec<u8>>>(name)? {
548 Some(value) => Value::Blob(value),
549 None => Value::Null(sql::NullKind::Blob),
550 },
551 ColumnKind::I64 => match row.try_get::<Option<i64>>(name)? {
552 Some(value) => Value::I64(value),
553 None => Value::Null(sql::NullKind::I64),
554 },
555 ColumnKind::Bool => match row.try_get::<Option<bool>>(name)? {
556 Some(value) => Value::Bool(value),
557 None => Value::Null(sql::NullKind::Bool),
558 },
559 })
560}
561
562fn quote(name: &str) -> String {
568 format!("\"{name}\"")
569}
570
571#[cfg(test)]
572mod tests {
573 use super::*;
574 use std::sync::Arc;
575
576 #[tokio::test]
587 async fn the_manifest_names_every_column() {
588 let database = Database::connect_for_test().await.unwrap();
589
590 for spec in TABLES {
591 let declared: Vec<&str> = spec.columns.iter().map(|(name, _)| *name).collect();
592 let live = live_columns(&database, spec.name).await;
593 assert_eq!(
594 live, declared,
595 "the manifest for `{}` has drifted from the schema; a column \
596 missing here is a column the transfer would drop",
597 spec.name
598 );
599 }
600 }
601
602 #[tokio::test]
607 async fn the_manifest_names_every_table() {
608 let database = Database::connect_for_test().await.unwrap();
609
610 let mut live = live_tables(&database).await;
611 let mut declared: Vec<String> = TABLES.iter().map(|s| s.name.to_string()).collect();
612 live.sort();
613 declared.sort();
614
615 assert_eq!(
616 live, declared,
617 "every table but `_sqlx_migrations` is copied; one missing here is \
618 one the transfer would leave behind"
619 );
620 }
621
622 #[test]
628 fn the_manifest_is_in_dependency_order() {
629 const EDGES: &[(&str, &str)] = &[
630 ("orders", "accounts"),
631 ("authorizations", "orders"),
632 ("challenges", "authorizations"),
633 ("upstream_orders", "orders"),
634 ("admin_sessions", "admin_users"),
635 ("admin_recovery_codes", "admin_users"),
636 ];
637
638 let position = |name: &str| {
639 TABLES
640 .iter()
641 .position(|spec| spec.name == name)
642 .unwrap_or_else(|| panic!("{name} should be in the manifest"))
643 };
644
645 for (child, parent) in EDGES {
646 assert!(
647 position(parent) < position(child),
648 "{parent} must be copied before {child}, or the foreign key refuses the row"
649 );
650 }
651 }
652
653 #[test]
659 fn every_key_is_a_column_of_its_table() {
660 for spec in TABLES {
661 assert!(!spec.key.is_empty(), "{} declares no key", spec.name);
662 for key in spec.key {
663 assert!(
664 spec.columns.iter().any(|(name, _)| name == key),
665 "{}.{key} is a key but not a column",
666 spec.name
667 );
668 }
669 }
670 }
671
672 #[tokio::test]
681 async fn a_copy_carries_every_table_and_leaves_the_source_alone() {
682 let source = Arc::new(Database::connect_for_test().await.unwrap());
683 let target = Database::connect_for_test().await.unwrap();
684
685 crate::testutil::seed_every_table(&source).await;
686 let before = crate::testutil::row_counts(&source).await;
687 assert!(
688 before.iter().all(|(_, rows)| *rows > 0),
689 "every table must be seeded or this proves nothing: {before:?}"
690 );
691 assert!(
692 non_empty_tables(&target)
693 .await
694 .expect("an empty target counts")
695 .is_empty(),
696 "the target starts empty"
697 );
698
699 let report = source
700 .transfer_to(&target)
701 .await
702 .expect("the copy should succeed");
703
704 assert_eq!(
705 report.total(),
706 before.iter().map(|(_, rows)| rows).sum::<u64>(),
707 "the report counts what the tables hold"
708 );
709 assert_eq!(
710 report.tables.len(),
711 TABLES.len(),
712 "every table is reported, including any that were empty"
713 );
714 assert_eq!(
715 crate::testutil::row_counts(&target).await,
716 before,
717 "the target holds what the source held"
718 );
719 assert_eq!(
720 crate::testutil::row_counts(&source).await,
721 before,
722 "the source is only read"
723 );
724
725 let agreed = crate::sql::query(
729 "SELECT terms_of_service_agreed FROM accounts ORDER BY created_at, id;",
730 )
731 .fetch_all(&target)
732 .await
733 .expect("the accounts are readable")
734 .iter()
735 .map(|row| {
736 row.try_get::<Option<bool>>(0usize)
737 .expect("a nullable bool")
738 })
739 .collect::<Vec<_>>();
740 assert!(
741 agreed.contains(&Some(true)) && agreed.contains(&None),
742 "an agreement and its absence both survive: {agreed:?}"
743 );
744 }
745
746 #[tokio::test]
758 async fn a_table_longer_than_one_batch_is_copied_whole() {
759 let source = Arc::new(Database::connect_for_test().await.unwrap());
760 let target = Database::connect_for_test().await.unwrap();
761
762 let rows = u64::try_from(BATCH).expect("the batch size is positive") + 1;
763 for index in 0..rows {
764 crate::nonce::Nonce::new()
765 .save(&source)
766 .await
767 .expect("a nonce is storable");
768 crate::revocation::Revocation {
769 issuer: "a".repeat(64),
770 serial: format!("{index:08x}"),
771 revoked_at: 1,
772 reason: None,
773 not_after: None,
774 }
775 .insert_if_absent(&source)
776 .await
777 .expect("a revocation is storable");
778 }
779
780 let report = source
781 .transfer_to(&target)
782 .await
783 .expect("the copy should succeed");
784
785 for table in ["nonces", "revocations"] {
786 let reported = report
787 .tables
788 .iter()
789 .find(|entry| entry.table == table)
790 .unwrap_or_else(|| panic!("{table} is in the manifest"));
791 assert_eq!(reported.rows, rows, "{table}: one batch and one row");
792 assert_eq!(
793 count(table, &target).await.expect("a count"),
794 rows,
795 "{table}: and the target agrees"
796 );
797 }
798 }
799
800 #[tokio::test]
809 async fn a_failed_copy_leaves_the_target_as_it_was() {
810 let source = Arc::new(Database::connect_for_test().await.unwrap());
811 let target = Arc::new(Database::connect_for_test().await.unwrap());
812
813 crate::testutil::seed_every_table(&source).await;
814 crate::admin_user::AdminUser::create("alice", "hash", None, &target)
815 .await
816 .expect("the target has an operator of its own");
817
818 let error = source
819 .transfer_to(&target)
820 .await
821 .expect_err("the duplicate username must fail the copy");
822 assert!(
823 crate::sql::is_unique_violation(&error),
824 "the cause is the username, not something else: {error}"
825 );
826
827 for (table, rows) in crate::testutil::row_counts(&target).await {
828 let expected = u64::from(table == "admin_users");
829 assert_eq!(
830 rows, expected,
831 "`{table}` should hold {expected} row(s) after the rollback"
832 );
833 }
834 }
835
836 async fn live_tables(database: &Database) -> Vec<String> {
838 let sql = match database.dialect() {
839 Dialect::Sqlite => {
840 "SELECT name FROM sqlite_master WHERE type = 'table' \
841 AND name NOT LIKE 'sqlite_%' AND name <> '_sqlx_migrations';"
842 }
843 Dialect::Postgres => {
844 "SELECT table_name FROM information_schema.tables \
845 WHERE table_schema = ANY (current_schemas(false)) \
846 AND table_type = 'BASE TABLE' AND table_name <> '_sqlx_migrations';"
847 }
848 };
849 sql::query(sql)
850 .fetch_all(database)
851 .await
852 .expect("the catalog should be readable")
853 .iter()
854 .map(|row| row.try_get::<String>(0usize).expect("a name"))
855 .collect()
856 }
857
858 async fn live_columns(database: &Database, table: &str) -> Vec<String> {
860 match database.dialect() {
861 Dialect::Sqlite => sql::query(sqlx::AssertSqlSafe(format!(
862 "SELECT name FROM pragma_table_info('{table}');"
863 )))
864 .fetch_all(database)
865 .await
866 .expect("the table should exist")
867 .iter()
868 .map(|row| row.try_get::<String>(0usize).expect("a name"))
869 .collect(),
870 Dialect::Postgres => sql::query(
871 "SELECT column_name FROM information_schema.columns \
872 WHERE table_schema = ANY (current_schemas(false)) AND table_name = ? \
873 ORDER BY ordinal_position;",
874 )
875 .bind(table)
876 .fetch_all(database)
877 .await
878 .expect("the table should exist")
879 .iter()
880 .map(|row| row.try_get::<String>(0usize).expect("a name"))
881 .collect(),
882 }
883 }
884}