1use std::path::Path;
42use std::str::FromStr;
43
44use chrono::{DateTime, NaiveDate, NaiveTime, Utc};
45use ipnetwork::IpNetwork;
46use mac_address::MacAddress;
47use rust_decimal::Decimal;
48use serde::{Deserialize, Serialize};
49use serde_json::{Map, Value};
50use sqlx::Row;
51use uuid::Uuid;
52
53use crate::db::DbPool;
54use crate::migrate::{Column, ModelMeta};
55use crate::orm::{ArrayElement, SqlType, TsVector};
56
57const DUMP_VERSION: &str = "1";
58
59type ResolvedTable<'a> = (ModelMeta, &'a [Map<String, Value>]);
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct Dump {
69 pub umbral_dump_version: String,
70 pub exported_at: String,
71 pub models: Vec<ModelDump>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct ModelDump {
80 pub table: String,
81 pub rows: Vec<Map<String, Value>>,
82}
83
84#[derive(Debug)]
86pub enum BackupError {
87 Io(std::io::Error),
88 Json(serde_json::Error),
89 Sqlx(sqlx::Error),
90 UnsupportedVersion(String),
93 UnknownColumn {
97 table: String,
98 column: String,
99 },
100 TypeMismatch {
105 table: String,
106 column: String,
107 expected: SqlType,
108 got: String,
109 },
110 DuplicateTable {
115 table: String,
116 },
117}
118
119impl std::fmt::Display for BackupError {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 match self {
122 BackupError::Io(e) => write!(f, "umbral backup: io: {e}"),
123 BackupError::Json(e) => write!(f, "umbral backup: json: {e}"),
124 BackupError::Sqlx(e) => write!(f, "umbral backup: sqlx: {e}"),
125 BackupError::UnsupportedVersion(v) => write!(
126 f,
127 "umbral backup: dump version `{v}` is not supported by this build \
128 (this build knows version `{DUMP_VERSION}`)"
129 ),
130 BackupError::UnknownColumn { table, column } => write!(
131 f,
132 "umbral backup: column `{table}.{column}` in the dump isn't in the \
133 current schema; run `umbral-cli migrate` first or update the dump"
134 ),
135 BackupError::TypeMismatch {
136 table,
137 column,
138 expected,
139 got,
140 } => write!(
141 f,
142 "umbral backup: column `{table}.{column}` expects {expected:?} but the \
143 dump has {got}"
144 ),
145 BackupError::DuplicateTable { table } => write!(
146 f,
147 "umbral backup: dump contains two entries for table `{table}`; a dump must \
148 carry one entry per table (was it merged or hand-edited?)"
149 ),
150 }
151 }
152}
153
154impl std::error::Error for BackupError {}
155
156impl From<std::io::Error> for BackupError {
157 fn from(e: std::io::Error) -> Self {
158 Self::Io(e)
159 }
160}
161
162impl From<serde_json::Error> for BackupError {
163 fn from(e: serde_json::Error) -> Self {
164 Self::Json(e)
165 }
166}
167
168impl From<sqlx::Error> for BackupError {
169 fn from(e: sqlx::Error) -> Self {
170 Self::Sqlx(e)
171 }
172}
173
174pub async fn dump() -> Result<Dump, BackupError> {
177 let pool = crate::db::pool_dispatched();
178 let mut models = crate::migrate::registered_models();
179 models.sort_by(|a, b| a.table.cmp(&b.table));
180
181 let mut out: Vec<ModelDump> = Vec::with_capacity(models.len());
182 for model in models {
183 out.push(dump_one(pool, &model).await?);
184 }
185 Ok(Dump {
186 umbral_dump_version: DUMP_VERSION.to_string(),
187 exported_at: Utc::now().to_rfc3339(),
188 models: out,
189 })
190}
191
192pub async fn dump_to_path(path: &Path) -> Result<(), BackupError> {
194 let dump = dump().await?;
195 let json = serde_json::to_string_pretty(&dump)?;
196 std::fs::write(path, json)?;
197 Ok(())
198}
199
200pub async fn load(dump: &Dump) -> Result<LoadReport, BackupError> {
204 if dump.umbral_dump_version != DUMP_VERSION {
205 return Err(BackupError::UnsupportedVersion(
206 dump.umbral_dump_version.clone(),
207 ));
208 }
209 let pool = crate::db::pool_dispatched();
210 let registered = crate::migrate::registered_models();
211 let by_table: std::collections::HashMap<String, ModelMeta> = registered
212 .into_iter()
213 .map(|m| (m.table.clone(), m))
214 .collect();
215
216 let mut report = LoadReport::default();
217
218 let mut resolved: Vec<ResolvedTable<'_>> = Vec::new();
225 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
226 for model in &dump.models {
227 let Some(meta) = by_table.get(&model.table) else {
228 report.skipped_tables.push(model.table.clone());
229 continue;
230 };
231 if !seen.insert(model.table.clone()) {
232 return Err(BackupError::DuplicateTable {
233 table: model.table.clone(),
234 });
235 }
236 resolved.push((meta.clone(), model.rows.as_slice()));
237 }
238
239 let ordered = topo_order_by_fk(resolved);
245
246 match pool {
249 DbPool::Sqlite(p) => load_all_sqlite(p, &ordered, &mut report).await?,
250 DbPool::Postgres(p) => load_all_postgres(p, &ordered, &mut report).await?,
251 }
252 Ok(report)
253}
254
255fn topo_order_by_fk(items: Vec<ResolvedTable<'_>>) -> Vec<ResolvedTable<'_>> {
265 let present: std::collections::HashSet<String> =
266 items.iter().map(|(m, _)| m.table.clone()).collect();
267
268 let mut deps: std::collections::HashMap<String, std::collections::HashSet<String>> =
269 std::collections::HashMap::new();
270 for (m, _) in &items {
271 let mut d = std::collections::HashSet::new();
272 for col in &m.fields {
273 if let Some(target) = &col.fk_target {
274 if target != &m.table && present.contains(target) {
275 d.insert(target.clone());
276 }
277 }
278 }
279 deps.insert(m.table.clone(), d);
280 }
281
282 let order_index: std::collections::HashMap<String, usize> = items
283 .iter()
284 .enumerate()
285 .map(|(i, (m, _))| (m.table.clone(), i))
286 .collect();
287
288 let mut emitted_set: std::collections::HashSet<String> = std::collections::HashSet::new();
289 let mut emitted: Vec<String> = Vec::new();
290 let mut remaining: Vec<String> = items.iter().map(|(m, _)| m.table.clone()).collect();
291
292 while !remaining.is_empty() {
293 let mut ready: Vec<String> = remaining
294 .iter()
295 .filter(|t| deps[*t].iter().all(|d| emitted_set.contains(d)))
296 .cloned()
297 .collect();
298 if ready.is_empty() {
299 let pick = remaining
301 .iter()
302 .min_by_key(|t| order_index[*t])
303 .cloned()
304 .expect("remaining is non-empty");
305 ready.push(pick);
306 }
307 ready.sort_by_key(|t| order_index[t]);
308 for t in ready {
309 emitted_set.insert(t.clone());
310 emitted.push(t.clone());
311 remaining.retain(|x| x != &t);
312 }
313 }
314
315 let mut by_table: std::collections::HashMap<String, ResolvedTable<'_>> = items
316 .into_iter()
317 .map(|(m, r)| (m.table.clone(), (m, r)))
318 .collect();
319 emitted
320 .into_iter()
321 .filter_map(|t| by_table.remove(&t))
322 .collect()
323}
324
325async fn load_all_sqlite(
326 pool: &sqlx::SqlitePool,
327 ordered: &[ResolvedTable<'_>],
328 report: &mut LoadReport,
329) -> Result<(), BackupError> {
330 let mut tx = pool.begin().await?;
331 for (meta, rows) in ordered {
332 let inserted = insert_rows_sqlite(&mut tx, meta, rows).await?;
333 report.rows_loaded += inserted;
334 report.tables_loaded.push(meta.table.clone());
335 }
336 tx.commit().await?;
337 Ok(())
338}
339
340async fn load_all_postgres(
341 pool: &sqlx::PgPool,
342 ordered: &[ResolvedTable<'_>],
343 report: &mut LoadReport,
344) -> Result<(), BackupError> {
345 let mut tx = pool.begin().await?;
346 for (meta, rows) in ordered {
347 let inserted = insert_rows_postgres(&mut tx, meta, rows).await?;
348 report.rows_loaded += inserted;
349 report.tables_loaded.push(meta.table.clone());
350 }
351 for (meta, _) in ordered {
356 reset_pg_sequence(&mut tx, meta).await?;
357 }
358 tx.commit().await?;
359 Ok(())
360}
361
362async fn reset_pg_sequence(
368 tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
369 meta: &ModelMeta,
370) -> Result<(), BackupError> {
371 let Some(pk) = meta.fields.iter().find(|c| c.primary_key) else {
372 return Ok(());
373 };
374 if !matches!(
375 pk.ty,
376 SqlType::BigInt | SqlType::Integer | SqlType::SmallInt
377 ) {
378 return Ok(());
379 }
380 let seq: Option<String> = sqlx::query_scalar("SELECT pg_get_serial_sequence($1, $2)")
381 .bind(&meta.table)
382 .bind(&pk.name)
383 .fetch_one(&mut **tx)
384 .await?;
385 let Some(seq) = seq else {
386 return Ok(());
387 };
388 let reset_sql = format!(
392 "SELECT setval($1, COALESCE((SELECT MAX({pk}) FROM {tbl}), 0) + 1, false)",
393 pk = quoted_ident(&pk.name),
394 tbl = quoted_ident(&meta.table),
395 );
396 sqlx::query(&reset_sql)
397 .bind(&seq)
398 .fetch_one(&mut **tx)
399 .await?;
400 Ok(())
401}
402
403pub async fn load_from_path(path: &Path) -> Result<LoadReport, BackupError> {
405 let text = std::fs::read_to_string(path)?;
406 let dump: Dump = serde_json::from_str(&text)?;
407 load(&dump).await
408}
409
410#[derive(Debug, Default, Clone)]
414pub struct LoadReport {
415 pub tables_loaded: Vec<String>,
416 pub skipped_tables: Vec<String>,
417 pub rows_loaded: u64,
418}
419
420async fn dump_one(pool: &DbPool, model: &ModelMeta) -> Result<ModelDump, BackupError> {
425 match pool {
426 DbPool::Sqlite(pool) => dump_one_sqlite(pool, model).await,
427 DbPool::Postgres(pool) => dump_one_postgres(pool, model).await,
428 }
429}
430
431async fn dump_one_sqlite(
432 pool: &sqlx::SqlitePool,
433 model: &ModelMeta,
434) -> Result<ModelDump, BackupError> {
435 let sql = format!(
436 "SELECT {} FROM {}",
437 column_list(model),
438 quoted_ident(&model.table)
439 );
440 let rows = sqlx::query(&sql).fetch_all(pool).await?;
441
442 let mut out: Vec<Map<String, Value>> = Vec::with_capacity(rows.len());
443 for row in rows {
444 let mut obj = Map::new();
445 for col in &model.fields {
446 obj.insert(col.name.clone(), column_to_json(&row, col)?);
447 }
448 out.push(obj);
449 }
450 Ok(ModelDump {
451 table: model.table.clone(),
452 rows: out,
453 })
454}
455
456async fn insert_rows_sqlite(
457 tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
458 model: &ModelMeta,
459 rows: &[Map<String, Value>],
460) -> Result<u64, BackupError> {
461 if rows.is_empty() {
462 return Ok(0);
463 }
464 let sql = format!(
465 "INSERT INTO {} ({}) VALUES ({})",
466 quoted_ident(&model.table),
467 column_list(model),
468 sqlite_placeholders(model.fields.len())
469 );
470
471 let mut count: u64 = 0;
472 for row in rows {
473 for k in row.keys() {
476 if !model.fields.iter().any(|c| &c.name == k) {
477 return Err(BackupError::UnknownColumn {
478 table: model.table.clone(),
479 column: k.clone(),
480 });
481 }
482 }
483 let mut q = sqlx::query(&sql);
484 for col in &model.fields {
485 let val = row.get(&col.name).cloned().unwrap_or(Value::Null);
486 q = bind_value(q, &model.table, col, val)?;
487 }
488 q.execute(&mut **tx).await?;
489 count += 1;
490 }
491 Ok(count)
492}
493
494async fn dump_one_postgres(
495 pool: &sqlx::PgPool,
496 model: &ModelMeta,
497) -> Result<ModelDump, BackupError> {
498 let sql = format!(
499 "SELECT {} FROM {}",
500 column_list_pg_select(model),
501 quoted_ident(&model.table)
502 );
503 let rows = sqlx::query(&sql).fetch_all(pool).await?;
504
505 let mut out: Vec<Map<String, Value>> = Vec::with_capacity(rows.len());
506 for row in rows {
507 let mut obj = Map::new();
508 for col in &model.fields {
509 obj.insert(col.name.clone(), column_to_json_pg(&row, col)?);
510 }
511 out.push(obj);
512 }
513 Ok(ModelDump {
514 table: model.table.clone(),
515 rows: out,
516 })
517}
518
519async fn insert_rows_postgres(
520 tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
521 model: &ModelMeta,
522 rows: &[Map<String, Value>],
523) -> Result<u64, BackupError> {
524 if rows.is_empty() {
525 return Ok(0);
526 }
527 let sql = format!(
528 "INSERT INTO {} ({}) VALUES ({})",
529 quoted_ident(&model.table),
530 column_list(model),
531 postgres_placeholders(model.fields.len())
532 );
533
534 let mut count: u64 = 0;
535 for row in rows {
536 for k in row.keys() {
537 if !model.fields.iter().any(|c| &c.name == k) {
538 return Err(BackupError::UnknownColumn {
539 table: model.table.clone(),
540 column: k.clone(),
541 });
542 }
543 }
544 let mut q = sqlx::query(&sql);
545 for col in &model.fields {
546 let val = row.get(&col.name).cloned().unwrap_or(Value::Null);
547 q = bind_value_pg(q, &model.table, col, val)?;
548 }
549 q.execute(&mut **tx).await?;
550 count += 1;
551 }
552 Ok(count)
553}
554
555fn quoted_ident(name: &str) -> String {
556 format!("\"{}\"", name.replace('"', "\"\""))
557}
558
559fn column_list(model: &ModelMeta) -> String {
560 model
561 .fields
562 .iter()
563 .map(|c| quoted_ident(&c.name))
564 .collect::<Vec<_>>()
565 .join(", ")
566}
567
568fn column_list_pg_select(model: &ModelMeta) -> String {
575 model
576 .fields
577 .iter()
578 .map(|c| {
579 if matches!(c.ty, SqlType::Xml | SqlType::Ltree | SqlType::Bit) {
580 let q = quoted_ident(&c.name);
581 format!("{q}::text AS {q}")
582 } else {
583 quoted_ident(&c.name)
584 }
585 })
586 .collect::<Vec<_>>()
587 .join(", ")
588}
589
590fn sqlite_placeholders(count: usize) -> String {
591 (0..count).map(|_| "?").collect::<Vec<_>>().join(", ")
592}
593
594fn postgres_placeholders(count: usize) -> String {
595 (1..=count)
596 .map(|idx| format!("${idx}"))
597 .collect::<Vec<_>>()
598 .join(", ")
599}
600
601fn column_to_json(row: &sqlx::sqlite::SqliteRow, col: &Column) -> Result<Value, BackupError> {
607 let name = col.name.as_str();
608 if col.nullable {
611 return Ok(match crate::migrate::fk_effective_type(col) {
612 SqlType::SmallInt | SqlType::Integer => row
613 .try_get::<Option<i32>, _>(name)?
614 .map_or(Value::Null, Value::from),
615 SqlType::BigInt => row
616 .try_get::<Option<i64>, _>(name)?
617 .map_or(Value::Null, Value::from),
618 SqlType::Real => row
619 .try_get::<Option<f32>, _>(name)?
620 .map_or(Value::Null, |v| Value::from(v as f64)),
621 SqlType::Double => row
622 .try_get::<Option<f64>, _>(name)?
623 .map_or(Value::Null, Value::from),
624 SqlType::Boolean => row
625 .try_get::<Option<bool>, _>(name)?
626 .map_or(Value::Null, Value::from),
627 SqlType::Text => row
628 .try_get::<Option<String>, _>(name)?
629 .map_or(Value::Null, Value::from),
630 SqlType::Date => row
631 .try_get::<Option<NaiveDate>, _>(name)?
632 .map_or(Value::Null, |v| Value::from(v.to_string())),
633 SqlType::Time => row
634 .try_get::<Option<NaiveTime>, _>(name)?
635 .map_or(Value::Null, |v| Value::from(v.to_string())),
636 SqlType::Timestamptz => row
637 .try_get::<Option<DateTime<Utc>>, _>(name)?
638 .map_or(Value::Null, |v| Value::from(v.to_rfc3339())),
639 SqlType::Uuid => row
640 .try_get::<Option<Uuid>, _>(name)?
641 .map_or(Value::Null, |v| Value::from(v.to_string())),
642 SqlType::Json => row
647 .try_get::<Option<Value>, _>(name)?
648 .unwrap_or(Value::Null),
649 SqlType::Array(_) => unreachable_array(&col.name),
654 SqlType::Inet | SqlType::Cidr | SqlType::MacAddr => unreachable_network(&col.name),
655 SqlType::FullText => unreachable_pg_only(&col.name, "FullText (tsvector)"),
656 SqlType::Xml => unreachable_pg_only(&col.name, "Xml"),
659 SqlType::Ltree => unreachable_pg_only(&col.name, "Ltree"),
660 SqlType::Bit => unreachable_pg_only(&col.name, "Bit"),
661 SqlType::ForeignKey => row
663 .try_get::<Option<i64>, _>(name)?
664 .map_or(Value::Null, Value::from),
665 SqlType::Bytes => row
669 .try_get::<Option<Vec<u8>>, _>(name)?
670 .map_or(Value::Null, |b| {
671 Value::Array(b.into_iter().map(Value::from).collect())
672 }),
673 SqlType::Decimal => unreachable_pg_only(&col.name, "Decimal"),
675 });
676 }
677 Ok(match crate::migrate::fk_effective_type(col) {
679 SqlType::SmallInt | SqlType::Integer => Value::from(row.try_get::<i32, _>(name)?),
680 SqlType::BigInt => Value::from(row.try_get::<i64, _>(name)?),
681 SqlType::Real => Value::from(row.try_get::<f32, _>(name)? as f64),
682 SqlType::Double => Value::from(row.try_get::<f64, _>(name)?),
683 SqlType::Boolean => Value::from(row.try_get::<bool, _>(name)?),
684 SqlType::Text => Value::from(row.try_get::<String, _>(name)?),
685 SqlType::Date => Value::from(row.try_get::<NaiveDate, _>(name)?.to_string()),
686 SqlType::Time => Value::from(row.try_get::<NaiveTime, _>(name)?.to_string()),
687 SqlType::Timestamptz => Value::from(row.try_get::<DateTime<Utc>, _>(name)?.to_rfc3339()),
688 SqlType::Uuid => Value::from(row.try_get::<Uuid, _>(name)?.to_string()),
689 SqlType::Json => row.try_get::<Value, _>(name)?,
690 SqlType::Array(_) => unreachable_array(&col.name),
691 SqlType::Inet | SqlType::Cidr | SqlType::MacAddr => unreachable_network(&col.name),
692 SqlType::FullText => unreachable_pg_only(&col.name, "FullText (tsvector)"),
693 SqlType::Xml => unreachable_pg_only(&col.name, "Xml"),
694 SqlType::Ltree => unreachable_pg_only(&col.name, "Ltree"),
695 SqlType::Bit => unreachable_pg_only(&col.name, "Bit"),
696 SqlType::ForeignKey => Value::from(row.try_get::<i64, _>(name)?),
698 SqlType::Bytes => {
699 let bytes: Vec<u8> = row.try_get(name)?;
700 Value::Array(bytes.into_iter().map(Value::from).collect())
701 }
702 SqlType::Decimal => unreachable_pg_only(&col.name, "Decimal"),
703 })
704}
705
706fn column_to_json_pg(row: &sqlx::postgres::PgRow, col: &Column) -> Result<Value, BackupError> {
707 let name = col.name.as_str();
708 if col.nullable {
709 return Ok(match crate::migrate::fk_effective_type(col) {
710 SqlType::SmallInt => row
711 .try_get::<Option<i16>, _>(name)?
712 .map_or(Value::Null, Value::from),
713 SqlType::Integer => row
714 .try_get::<Option<i32>, _>(name)?
715 .map_or(Value::Null, Value::from),
716 SqlType::BigInt | SqlType::ForeignKey => row
717 .try_get::<Option<i64>, _>(name)?
718 .map_or(Value::Null, Value::from),
719 SqlType::Real => row
720 .try_get::<Option<f32>, _>(name)?
721 .map_or(Value::Null, |v| Value::from(v as f64)),
722 SqlType::Double => row
723 .try_get::<Option<f64>, _>(name)?
724 .map_or(Value::Null, Value::from),
725 SqlType::Boolean => row
726 .try_get::<Option<bool>, _>(name)?
727 .map_or(Value::Null, Value::from),
728 SqlType::Text => row
729 .try_get::<Option<String>, _>(name)?
730 .map_or(Value::Null, Value::from),
731 SqlType::Date => row
732 .try_get::<Option<NaiveDate>, _>(name)?
733 .map_or(Value::Null, |v| Value::from(v.to_string())),
734 SqlType::Time => row
735 .try_get::<Option<NaiveTime>, _>(name)?
736 .map_or(Value::Null, |v| Value::from(v.to_string())),
737 SqlType::Timestamptz => row
738 .try_get::<Option<DateTime<Utc>>, _>(name)?
739 .map_or(Value::Null, |v| Value::from(v.to_rfc3339())),
740 SqlType::Uuid => row
741 .try_get::<Option<Uuid>, _>(name)?
742 .map_or(Value::Null, |v| Value::from(v.to_string())),
743 SqlType::Json => row
744 .try_get::<Option<Value>, _>(name)?
745 .unwrap_or(Value::Null),
746 SqlType::Array(elem) => pg_array_column_to_json_nullable(row, name, elem)?,
747 SqlType::Inet | SqlType::Cidr => row
748 .try_get::<Option<IpNetwork>, _>(name)?
749 .map_or(Value::Null, |v| Value::from(v.to_string())),
750 SqlType::MacAddr => row
751 .try_get::<Option<MacAddress>, _>(name)?
752 .map_or(Value::Null, |v| Value::from(v.to_string())),
753 SqlType::FullText => row
754 .try_get::<Option<TsVector>, _>(name)?
755 .map_or(Value::Null, |v| Value::from(v.into_inner())),
756 SqlType::Xml | SqlType::Ltree | SqlType::Bit => row
760 .try_get::<Option<String>, _>(name)?
761 .map_or(Value::Null, Value::from),
762 SqlType::Bytes => row
763 .try_get::<Option<Vec<u8>>, _>(name)?
764 .map_or(Value::Null, bytes_to_json),
765 SqlType::Decimal => row
766 .try_get::<Option<Decimal>, _>(name)?
767 .map_or(Value::Null, |v| Value::from(v.to_string())),
768 });
769 }
770 Ok(match crate::migrate::fk_effective_type(col) {
771 SqlType::SmallInt => Value::from(row.try_get::<i16, _>(name)?),
772 SqlType::Integer => Value::from(row.try_get::<i32, _>(name)?),
773 SqlType::BigInt | SqlType::ForeignKey => Value::from(row.try_get::<i64, _>(name)?),
774 SqlType::Real => Value::from(row.try_get::<f32, _>(name)? as f64),
775 SqlType::Double => Value::from(row.try_get::<f64, _>(name)?),
776 SqlType::Boolean => Value::from(row.try_get::<bool, _>(name)?),
777 SqlType::Text => Value::from(row.try_get::<String, _>(name)?),
778 SqlType::Date => Value::from(row.try_get::<NaiveDate, _>(name)?.to_string()),
779 SqlType::Time => Value::from(row.try_get::<NaiveTime, _>(name)?.to_string()),
780 SqlType::Timestamptz => Value::from(row.try_get::<DateTime<Utc>, _>(name)?.to_rfc3339()),
781 SqlType::Uuid => Value::from(row.try_get::<Uuid, _>(name)?.to_string()),
782 SqlType::Json => row.try_get::<Value, _>(name)?,
783 SqlType::Array(elem) => pg_array_column_to_json(row, name, elem)?,
784 SqlType::Inet | SqlType::Cidr => {
785 Value::from(row.try_get::<IpNetwork, _>(name)?.to_string())
786 }
787 SqlType::MacAddr => Value::from(row.try_get::<MacAddress, _>(name)?.to_string()),
788 SqlType::FullText => Value::from(row.try_get::<TsVector, _>(name)?.into_inner()),
789 SqlType::Xml | SqlType::Ltree | SqlType::Bit => {
791 Value::from(row.try_get::<String, _>(name)?)
792 }
793 SqlType::Bytes => bytes_to_json(row.try_get::<Vec<u8>, _>(name)?),
794 SqlType::Decimal => Value::from(row.try_get::<Decimal, _>(name)?.to_string()),
795 })
796}
797
798fn pg_array_column_to_json_nullable(
799 row: &sqlx::postgres::PgRow,
800 name: &str,
801 elem: ArrayElement,
802) -> Result<Value, BackupError> {
803 Ok(match elem {
804 ArrayElement::SmallInt => row
805 .try_get::<Option<Vec<i16>>, _>(name)?
806 .map_or(Value::Null, |values| array_to_json(values, Value::from)),
807 ArrayElement::Integer => row
808 .try_get::<Option<Vec<i32>>, _>(name)?
809 .map_or(Value::Null, |values| array_to_json(values, Value::from)),
810 ArrayElement::BigInt => row
811 .try_get::<Option<Vec<i64>>, _>(name)?
812 .map_or(Value::Null, |values| array_to_json(values, Value::from)),
813 ArrayElement::Real => row
814 .try_get::<Option<Vec<f32>>, _>(name)?
815 .map_or(Value::Null, |values| {
816 array_to_json(values, |v| Value::from(v as f64))
817 }),
818 ArrayElement::Double => row
819 .try_get::<Option<Vec<f64>>, _>(name)?
820 .map_or(Value::Null, |values| array_to_json(values, Value::from)),
821 ArrayElement::Boolean => row
822 .try_get::<Option<Vec<bool>>, _>(name)?
823 .map_or(Value::Null, |values| array_to_json(values, Value::from)),
824 ArrayElement::Text => row
825 .try_get::<Option<Vec<String>>, _>(name)?
826 .map_or(Value::Null, |values| array_to_json(values, Value::from)),
827 ArrayElement::Uuid => row
828 .try_get::<Option<Vec<Uuid>>, _>(name)?
829 .map_or(Value::Null, |values| {
830 array_to_json(values, |v| Value::from(v.to_string()))
831 }),
832 })
833}
834
835fn pg_array_column_to_json(
836 row: &sqlx::postgres::PgRow,
837 name: &str,
838 elem: ArrayElement,
839) -> Result<Value, BackupError> {
840 Ok(match elem {
841 ArrayElement::SmallInt => array_to_json(row.try_get::<Vec<i16>, _>(name)?, Value::from),
842 ArrayElement::Integer => array_to_json(row.try_get::<Vec<i32>, _>(name)?, Value::from),
843 ArrayElement::BigInt => array_to_json(row.try_get::<Vec<i64>, _>(name)?, Value::from),
844 ArrayElement::Real => {
845 array_to_json(row.try_get::<Vec<f32>, _>(name)?, |v| Value::from(v as f64))
846 }
847 ArrayElement::Double => array_to_json(row.try_get::<Vec<f64>, _>(name)?, Value::from),
848 ArrayElement::Boolean => array_to_json(row.try_get::<Vec<bool>, _>(name)?, Value::from),
849 ArrayElement::Text => array_to_json(row.try_get::<Vec<String>, _>(name)?, Value::from),
850 ArrayElement::Uuid => array_to_json(row.try_get::<Vec<Uuid>, _>(name)?, |v| {
851 Value::from(v.to_string())
852 }),
853 })
854}
855
856fn array_to_json<T>(values: Vec<T>, mut item: impl FnMut(T) -> Value) -> Value {
857 Value::Array(values.into_iter().map(&mut item).collect())
858}
859
860fn bytes_to_json(bytes: Vec<u8>) -> Value {
861 Value::Array(bytes.into_iter().map(Value::from).collect())
862}
863
864fn unreachable_array(column: &str) -> ! {
869 panic!(
870 "umbral backup: column `{column}` is a Postgres-only Array; \
871 the field.backend system check should have failed boot. \
872 For portable list storage use SqlType::Json instead."
873 )
874}
875
876fn unreachable_network(column: &str) -> ! {
878 panic!(
879 "umbral backup: column `{column}` is a Postgres-only network \
880 address type (Inet/Cidr/MacAddr); the field.backend system \
881 check should have failed boot."
882 )
883}
884
885fn unreachable_pg_only(column: &str, type_name: &str) -> ! {
887 panic!(
888 "umbral backup: column `{column}` is a Postgres-only {type_name} \
889 type; the field.backend system check should have failed boot."
890 )
891}
892
893fn bind_value<'q>(
894 q: SqliteQuery<'q>,
895 table: &str,
896 col: &Column,
897 val: Value,
898) -> Result<SqliteQuery<'q>, BackupError> {
899 if matches!(val, Value::Null) {
902 return Ok(match crate::migrate::fk_effective_type(col) {
903 SqlType::SmallInt | SqlType::Integer => q.bind(None::<i32>),
904 SqlType::BigInt => q.bind(None::<i64>),
905 SqlType::Real => q.bind(None::<f32>),
906 SqlType::Double => q.bind(None::<f64>),
907 SqlType::Boolean => q.bind(None::<bool>),
908 SqlType::Text => q.bind(None::<String>),
909 SqlType::Date => q.bind(None::<NaiveDate>),
910 SqlType::Time => q.bind(None::<NaiveTime>),
911 SqlType::Timestamptz => q.bind(None::<DateTime<Utc>>),
912 SqlType::Uuid => q.bind(None::<Uuid>),
913 SqlType::Json => q.bind(None::<Value>),
914 SqlType::Array(_) => unreachable_array(&col.name),
915 SqlType::Inet | SqlType::Cidr | SqlType::MacAddr => unreachable_network(&col.name),
916 SqlType::FullText => unreachable_pg_only(&col.name, "FullText (tsvector)"),
917 SqlType::Xml => unreachable_pg_only(&col.name, "Xml"),
920 SqlType::Ltree => unreachable_pg_only(&col.name, "Ltree"),
921 SqlType::Bit => unreachable_pg_only(&col.name, "Bit"),
922 SqlType::ForeignKey => q.bind(None::<i64>),
924 SqlType::Bytes => q.bind(None::<Vec<u8>>),
925 SqlType::Decimal => unreachable_pg_only(&col.name, "Decimal"),
926 });
927 }
928 let mismatch = |got: &str| BackupError::TypeMismatch {
929 table: table.to_string(),
930 column: col.name.clone(),
931 expected: col.ty,
932 got: got.to_string(),
933 };
934 Ok(match crate::migrate::fk_effective_type(col) {
935 SqlType::SmallInt | SqlType::Integer => {
936 q.bind(val.as_i64().ok_or_else(|| mismatch(json_type_name(&val)))? as i32)
937 }
938 SqlType::BigInt => q.bind(val.as_i64().ok_or_else(|| mismatch(json_type_name(&val)))?),
939 SqlType::Real => q.bind(val.as_f64().ok_or_else(|| mismatch(json_type_name(&val)))? as f32),
940 SqlType::Double => q.bind(val.as_f64().ok_or_else(|| mismatch(json_type_name(&val)))?),
941 SqlType::Boolean => q.bind(
942 val.as_bool()
943 .ok_or_else(|| mismatch(json_type_name(&val)))?,
944 ),
945 SqlType::Text => q.bind(
946 val.as_str()
947 .ok_or_else(|| mismatch(json_type_name(&val)))?
948 .to_string(),
949 ),
950 SqlType::Date => {
951 let s = val.as_str().ok_or_else(|| mismatch(json_type_name(&val)))?;
952 q.bind(
953 s.parse::<NaiveDate>()
954 .map_err(|_| mismatch("invalid date string"))?,
955 )
956 }
957 SqlType::Time => {
958 let s = val.as_str().ok_or_else(|| mismatch(json_type_name(&val)))?;
959 q.bind(
960 s.parse::<NaiveTime>()
961 .map_err(|_| mismatch("invalid time string"))?,
962 )
963 }
964 SqlType::Timestamptz => {
965 let s = val.as_str().ok_or_else(|| mismatch(json_type_name(&val)))?;
966 q.bind(
967 DateTime::parse_from_rfc3339(s)
968 .map_err(|_| mismatch("invalid rfc3339 timestamp"))?
969 .with_timezone(&Utc),
970 )
971 }
972 SqlType::Uuid => {
973 let s = val.as_str().ok_or_else(|| mismatch(json_type_name(&val)))?;
974 q.bind(Uuid::parse_str(s).map_err(|_| mismatch("invalid uuid string"))?)
975 }
976 SqlType::Json => q.bind(val),
981 SqlType::Array(_) => unreachable_array(&col.name),
982 SqlType::Inet | SqlType::Cidr | SqlType::MacAddr => unreachable_network(&col.name),
983 SqlType::FullText => unreachable_pg_only(&col.name, "FullText (tsvector)"),
984 SqlType::Xml => unreachable_pg_only(&col.name, "Xml"),
985 SqlType::Ltree => unreachable_pg_only(&col.name, "Ltree"),
986 SqlType::Bit => unreachable_pg_only(&col.name, "Bit"),
987 SqlType::ForeignKey => q.bind(val.as_i64().ok_or_else(|| mismatch(json_type_name(&val)))?),
989 SqlType::Bytes => q.bind(bytes_from_json(table, col, &val)?),
992 SqlType::Decimal => unreachable_pg_only(&col.name, "Decimal"),
993 })
994}
995
996type SqliteQuery<'q> = sqlx::query::Query<'q, sqlx::Sqlite, sqlx::sqlite::SqliteArguments<'q>>;
997type PgQuery<'q> = sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>;
998
999fn bind_value_pg<'q>(
1000 q: PgQuery<'q>,
1001 table: &str,
1002 col: &Column,
1003 val: Value,
1004) -> Result<PgQuery<'q>, BackupError> {
1005 if matches!(val, Value::Null) {
1006 return Ok(match crate::migrate::fk_effective_type(col) {
1007 SqlType::SmallInt => q.bind(None::<i16>),
1008 SqlType::Integer => q.bind(None::<i32>),
1009 SqlType::BigInt | SqlType::ForeignKey => q.bind(None::<i64>),
1010 SqlType::Real => q.bind(None::<f32>),
1011 SqlType::Double => q.bind(None::<f64>),
1012 SqlType::Boolean => q.bind(None::<bool>),
1013 SqlType::Text => q.bind(None::<String>),
1014 SqlType::Date => q.bind(None::<NaiveDate>),
1015 SqlType::Time => q.bind(None::<NaiveTime>),
1016 SqlType::Timestamptz => q.bind(None::<DateTime<Utc>>),
1017 SqlType::Uuid => q.bind(None::<Uuid>),
1018 SqlType::Json => q.bind(None::<Value>),
1019 SqlType::Array(elem) => bind_null_array_pg(q, elem),
1020 SqlType::Inet | SqlType::Cidr => q.bind(None::<IpNetwork>),
1021 SqlType::MacAddr => q.bind(None::<MacAddress>),
1022 SqlType::FullText => q.bind(None::<TsVector>),
1023 SqlType::Xml | SqlType::Ltree | SqlType::Bit => q.bind(None::<String>),
1026 SqlType::Bytes => q.bind(None::<Vec<u8>>),
1027 SqlType::Decimal => q.bind(None::<Decimal>),
1028 });
1029 }
1030 let mismatch = |got: &str| BackupError::TypeMismatch {
1031 table: table.to_string(),
1032 column: col.name.clone(),
1033 expected: col.ty,
1034 got: got.to_string(),
1035 };
1036 Ok(match crate::migrate::fk_effective_type(col) {
1037 SqlType::SmallInt => q.bind(
1038 i16::try_from(val.as_i64().ok_or_else(|| mismatch(json_type_name(&val)))?)
1039 .map_err(|_| mismatch("number out of i16 range"))?,
1040 ),
1041 SqlType::Integer => q.bind(
1042 i32::try_from(val.as_i64().ok_or_else(|| mismatch(json_type_name(&val)))?)
1043 .map_err(|_| mismatch("number out of i32 range"))?,
1044 ),
1045 SqlType::BigInt | SqlType::ForeignKey => {
1046 q.bind(val.as_i64().ok_or_else(|| mismatch(json_type_name(&val)))?)
1047 }
1048 SqlType::Real => q.bind(val.as_f64().ok_or_else(|| mismatch(json_type_name(&val)))? as f32),
1049 SqlType::Double => q.bind(val.as_f64().ok_or_else(|| mismatch(json_type_name(&val)))?),
1050 SqlType::Boolean => q.bind(
1051 val.as_bool()
1052 .ok_or_else(|| mismatch(json_type_name(&val)))?,
1053 ),
1054 SqlType::Text => q.bind(
1055 val.as_str()
1056 .ok_or_else(|| mismatch(json_type_name(&val)))?
1057 .to_string(),
1058 ),
1059 SqlType::Date => {
1060 let s = val.as_str().ok_or_else(|| mismatch(json_type_name(&val)))?;
1061 q.bind(
1062 s.parse::<NaiveDate>()
1063 .map_err(|_| mismatch("invalid date string"))?,
1064 )
1065 }
1066 SqlType::Time => {
1067 let s = val.as_str().ok_or_else(|| mismatch(json_type_name(&val)))?;
1068 q.bind(
1069 s.parse::<NaiveTime>()
1070 .map_err(|_| mismatch("invalid time string"))?,
1071 )
1072 }
1073 SqlType::Timestamptz => {
1074 let s = val.as_str().ok_or_else(|| mismatch(json_type_name(&val)))?;
1075 q.bind(
1076 DateTime::parse_from_rfc3339(s)
1077 .map_err(|_| mismatch("invalid rfc3339 timestamp"))?
1078 .with_timezone(&Utc),
1079 )
1080 }
1081 SqlType::Uuid => {
1082 let s = val.as_str().ok_or_else(|| mismatch(json_type_name(&val)))?;
1083 q.bind(Uuid::parse_str(s).map_err(|_| mismatch("invalid uuid string"))?)
1084 }
1085 SqlType::Json => q.bind(val),
1086 SqlType::Array(elem) => bind_array_pg(q, table, col, elem, &val)?,
1087 SqlType::Inet | SqlType::Cidr => {
1088 let s = val.as_str().ok_or_else(|| mismatch(json_type_name(&val)))?;
1089 q.bind(IpNetwork::from_str(s).map_err(|_| mismatch("invalid network string"))?)
1090 }
1091 SqlType::MacAddr => {
1092 let s = val.as_str().ok_or_else(|| mismatch(json_type_name(&val)))?;
1093 q.bind(MacAddress::from_str(s).map_err(|_| mismatch("invalid macaddr string"))?)
1094 }
1095 SqlType::FullText => {
1096 let s = val.as_str().ok_or_else(|| mismatch(json_type_name(&val)))?;
1097 q.bind(TsVector::from(s))
1098 }
1099 SqlType::Xml | SqlType::Ltree | SqlType::Bit => {
1103 let s = val.as_str().ok_or_else(|| mismatch(json_type_name(&val)))?;
1104 q.bind(s.to_string())
1105 }
1106 SqlType::Bytes => q.bind(bytes_from_json(table, col, &val)?),
1107 SqlType::Decimal => {
1108 let parsed = match &val {
1109 Value::String(s) => Decimal::from_str(s).ok(),
1110 Value::Number(n) => Decimal::from_str(&n.to_string()).ok(),
1111 _ => None,
1112 };
1113 q.bind(parsed.ok_or_else(|| mismatch(json_type_name(&val)))?)
1114 }
1115 })
1116}
1117
1118fn bind_null_array_pg<'q>(q: PgQuery<'q>, elem: ArrayElement) -> PgQuery<'q> {
1119 match elem {
1120 ArrayElement::SmallInt => q.bind(None::<Vec<i16>>),
1121 ArrayElement::Integer => q.bind(None::<Vec<i32>>),
1122 ArrayElement::BigInt => q.bind(None::<Vec<i64>>),
1123 ArrayElement::Real => q.bind(None::<Vec<f32>>),
1124 ArrayElement::Double => q.bind(None::<Vec<f64>>),
1125 ArrayElement::Boolean => q.bind(None::<Vec<bool>>),
1126 ArrayElement::Text => q.bind(None::<Vec<String>>),
1127 ArrayElement::Uuid => q.bind(None::<Vec<Uuid>>),
1128 }
1129}
1130
1131fn bind_array_pg<'q>(
1132 q: PgQuery<'q>,
1133 table: &str,
1134 col: &Column,
1135 elem: ArrayElement,
1136 val: &Value,
1137) -> Result<PgQuery<'q>, BackupError> {
1138 Ok(match elem {
1139 ArrayElement::SmallInt => q.bind(
1140 int_array_from_json(table, col, val)?
1141 .into_iter()
1142 .map(|n| {
1143 i16::try_from(n)
1144 .map_err(|_| type_mismatch(table, col, "element out of i16 range"))
1145 })
1146 .collect::<Result<Vec<_>, _>>()?,
1147 ),
1148 ArrayElement::Integer => q.bind(
1149 int_array_from_json(table, col, val)?
1150 .into_iter()
1151 .map(|n| {
1152 i32::try_from(n)
1153 .map_err(|_| type_mismatch(table, col, "element out of i32 range"))
1154 })
1155 .collect::<Result<Vec<_>, _>>()?,
1156 ),
1157 ArrayElement::BigInt => q.bind(int_array_from_json(table, col, val)?),
1158 ArrayElement::Real => q.bind(
1159 float_array_from_json(table, col, val)?
1160 .into_iter()
1161 .map(|n| n as f32)
1162 .collect::<Vec<_>>(),
1163 ),
1164 ArrayElement::Double => q.bind(float_array_from_json(table, col, val)?),
1165 ArrayElement::Boolean => q.bind(
1166 array_values(table, col, val)?
1167 .iter()
1168 .map(|v| {
1169 v.as_bool()
1170 .ok_or_else(|| type_mismatch(table, col, "non-boolean in array"))
1171 })
1172 .collect::<Result<Vec<_>, _>>()?,
1173 ),
1174 ArrayElement::Text => q.bind(
1175 array_values(table, col, val)?
1176 .iter()
1177 .map(|v| {
1178 v.as_str()
1179 .map(ToString::to_string)
1180 .ok_or_else(|| type_mismatch(table, col, "non-string in array"))
1181 })
1182 .collect::<Result<Vec<_>, _>>()?,
1183 ),
1184 ArrayElement::Uuid => q.bind(
1185 array_values(table, col, val)?
1186 .iter()
1187 .map(|v| {
1188 let s = v
1189 .as_str()
1190 .ok_or_else(|| type_mismatch(table, col, "non-string uuid in array"))?;
1191 Uuid::parse_str(s)
1192 .map_err(|_| type_mismatch(table, col, "invalid uuid string in array"))
1193 })
1194 .collect::<Result<Vec<_>, _>>()?,
1195 ),
1196 })
1197}
1198
1199fn array_values<'a>(
1200 table: &str,
1201 col: &Column,
1202 val: &'a Value,
1203) -> Result<&'a Vec<Value>, BackupError> {
1204 val.as_array()
1205 .ok_or_else(|| type_mismatch(table, col, json_type_name(val)))
1206}
1207
1208fn int_array_from_json(table: &str, col: &Column, val: &Value) -> Result<Vec<i64>, BackupError> {
1209 array_values(table, col, val)?
1210 .iter()
1211 .map(|v| {
1212 v.as_i64()
1213 .ok_or_else(|| type_mismatch(table, col, "non-integer in array"))
1214 })
1215 .collect()
1216}
1217
1218fn float_array_from_json(table: &str, col: &Column, val: &Value) -> Result<Vec<f64>, BackupError> {
1219 array_values(table, col, val)?
1220 .iter()
1221 .map(|v| {
1222 v.as_f64()
1223 .ok_or_else(|| type_mismatch(table, col, "non-number in array"))
1224 })
1225 .collect()
1226}
1227
1228fn bytes_from_json(table: &str, col: &Column, val: &Value) -> Result<Vec<u8>, BackupError> {
1229 let arr = val
1230 .as_array()
1231 .ok_or_else(|| type_mismatch(table, col, json_type_name(val)))?;
1232 let mut bytes: Vec<u8> = Vec::with_capacity(arr.len());
1233 for v in arr {
1234 let n = v
1235 .as_u64()
1236 .ok_or_else(|| type_mismatch(table, col, "non-number in bytes array"))?;
1237 if n > 255 {
1238 return Err(type_mismatch(table, col, "element out of u8 range"));
1239 }
1240 bytes.push(n as u8);
1241 }
1242 Ok(bytes)
1243}
1244
1245fn type_mismatch(table: &str, col: &Column, got: impl Into<String>) -> BackupError {
1246 BackupError::TypeMismatch {
1247 table: table.to_string(),
1248 column: col.name.clone(),
1249 expected: col.ty,
1250 got: got.into(),
1251 }
1252}
1253
1254fn json_type_name(v: &Value) -> &'static str {
1255 match v {
1256 Value::Null => "null",
1257 Value::Bool(_) => "boolean",
1258 Value::Number(_) => "number",
1259 Value::String(_) => "string",
1260 Value::Array(_) => "array",
1261 Value::Object(_) => "object",
1262 }
1263}
1264
1265#[cfg(test)]
1266mod tests {
1267 use super::*;
1268
1269 #[test]
1270 fn placeholder_generation_matches_backend_syntax() {
1271 assert_eq!(sqlite_placeholders(3), "?, ?, ?");
1272 assert_eq!(postgres_placeholders(3), "$1, $2, $3");
1273 }
1274
1275 #[test]
1276 fn quoted_ident_escapes_double_quotes() {
1277 assert_eq!(quoted_ident("plain"), "\"plain\"");
1278 assert_eq!(quoted_ident("weird\"name"), "\"weird\"\"name\"");
1279 }
1280}