commit 1ecff77ac9914f546df3c8bc846f90bc4c0b341f
parent c5515e143ca8ab95f82838ccb246fa51211a0cf8
Author: triesap <tyson@radroots.org>
Date: Thu, 20 Nov 2025 16:02:35 +0000
workspace: add `tangle-sql` database backup export and restore with wasm bindings
Diffstat:
21 files changed, 400 insertions(+), 71 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1870,6 +1870,7 @@ dependencies = [
"radroots-sql-core",
"radroots-tangle-schema",
"radroots-types",
+ "serde",
"serde_json",
]
diff --git a/Cargo.toml b/Cargo.toml
@@ -79,7 +79,7 @@ tracing-subscriber = { version = "0.3" }
ts-rs = { version = "11.1" }
typeshare = { version = "1" }
url = { version = "2" }
-uuid = { version = "1.16.0" }
+uuid = { version = "1.16.0", features = ["v4"] }
uniffi = { version = "0.29.4" }
wasm-bindgen = { version = "0.2" }
wasm-bindgen-futures = { version = "0.4" }
diff --git a/core/src/currency.rs b/core/src/currency.rs
@@ -2,7 +2,7 @@ use core::fmt;
use core::str::FromStr;
#[cfg(feature = "serde")]
-use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer};
+use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as DeError};
#[typeshare::typeshare]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
diff --git a/core/src/decimal.rs b/core/src/decimal.rs
@@ -1,11 +1,11 @@
use core::fmt;
use core::ops::{Add, Div, Mul, Sub};
use core::str::FromStr;
-use rust_decimal::prelude::ToPrimitive;
use rust_decimal::Decimal;
+use rust_decimal::prelude::ToPrimitive;
#[cfg(feature = "serde")]
-use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer};
+use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as DeError};
#[typeshare::typeshare]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
diff --git a/core/src/money.rs b/core/src/money.rs
@@ -1,7 +1,7 @@
use core::fmt;
-use rust_decimal::prelude::ToPrimitive;
use rust_decimal::Decimal;
use rust_decimal::RoundingStrategy;
+use rust_decimal::prelude::ToPrimitive;
#[typeshare::typeshare]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
diff --git a/core/src/percent.rs b/core/src/percent.rs
@@ -1,8 +1,8 @@
use core::fmt;
use core::str::FromStr;
-use crate::money::RadrootsCoreMoney;
use crate::RadrootsCoreDecimal;
+use crate::money::RadrootsCoreMoney;
#[typeshare::typeshare]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
diff --git a/core/src/quantity.rs b/core/src/quantity.rs
@@ -1,7 +1,7 @@
use core::fmt;
-use crate::unit::RadrootsCoreUnit;
use crate::RadrootsCoreDecimal;
+use crate::unit::RadrootsCoreUnit;
#[typeshare::typeshare]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
diff --git a/core/src/serde_ext.rs b/core/src/serde_ext.rs
@@ -1,6 +1,6 @@
#![cfg(feature = "serde")]
-use serde::{de::Error as DeError, Deserialize, Deserializer, Serializer};
+use serde::{Deserialize, Deserializer, Serializer, de::Error as DeError};
pub mod decimal_str {
use super::*;
diff --git a/core/src/unit.rs b/core/src/unit.rs
@@ -3,7 +3,7 @@ use core::str::FromStr;
use rust_decimal_macros::dec;
#[cfg(feature = "serde")]
-use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer};
+use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as DeError};
use crate::RadrootsCoreDecimal;
diff --git a/events-codec/src/lib.rs b/events-codec/src/lib.rs
@@ -3,4 +3,4 @@
extern crate alloc;
pub mod job;
-pub mod profile;
-\ No newline at end of file
+pub mod profile;
diff --git a/events-codec/src/profile/error.rs b/events-codec/src/profile/error.rs
@@ -3,7 +3,7 @@ use core::fmt;
#[derive(Debug)]
pub enum ProfileEncodeError {
InvalidUrl(&'static str, String),
- Json,
+ Json,
}
impl fmt::Display for ProfileEncodeError {
diff --git a/events-indexed/src/lib.rs b/events-indexed/src/lib.rs
@@ -9,7 +9,7 @@ pub mod types;
pub use checkpoint::{RadrootsEventsIndexedIndexCheckpoint, RadrootsEventsIndexedShardCheckpoint};
pub use manifest::{
- validate_manifest, RadrootsEventsIndexedManifest, RadrootsEventsIndexedManifestError,
- RadrootsEventsIndexedShardMetadata,
+ RadrootsEventsIndexedManifest, RadrootsEventsIndexedManifestError,
+ RadrootsEventsIndexedShardMetadata, validate_manifest,
};
pub use types::{RadrootsEventsIndexedIdRange, RadrootsEventsIndexedShardId};
diff --git a/events-indexed/src/serde_ext.rs b/events-indexed/src/serde_ext.rs
@@ -1,6 +1,6 @@
#[cfg(feature = "serde")]
pub mod epoch_seconds {
- use serde::{de::Error as DeError, Deserialize, Deserializer};
+ use serde::{Deserialize, Deserializer, de::Error as DeError};
pub fn de<'de, D>(de: D) -> Result<u32, D::Error>
where
diff --git a/package.json b/package.json
@@ -14,8 +14,8 @@
},
"workspaces": [
"*/bindings/ts",
- "../packages/dev",
- "../packages/tsconfig"
+ "../pwa/packages/dev",
+ "../pwa/packages/tsconfig"
],
"packageManager": "yarn@1.22.22"
}
\ No newline at end of file
diff --git a/runtime/src/config.rs b/runtime/src/config.rs
@@ -68,12 +68,10 @@ where
}
}
- let cfg = builder
- .build()
- .map_err(|source| RuntimeConfigError::Load {
- path: p.to_path_buf(),
- source,
- })?;
+ let cfg = builder.build().map_err(|source| RuntimeConfigError::Load {
+ path: p.to_path_buf(),
+ source,
+ })?;
try_deser::<T>(cfg, p)
}
diff --git a/runtime/src/tracing.rs b/runtime/src/tracing.rs
@@ -1,8 +1,8 @@
use std::fs;
use std::path::{Path, PathBuf};
use tracing_appender::rolling;
-use tracing_subscriber::{fmt, prelude::*, EnvFilter, Registry};
use tracing_subscriber::util::SubscriberInitExt;
+use tracing_subscriber::{EnvFilter, Registry, fmt, prelude::*};
use crate::error::RuntimeTracingError;
@@ -10,7 +10,10 @@ pub fn init() -> Result<(), RuntimeTracingError> {
init_with("logs", None)
}
-pub fn init_with(logs_dir: impl AsRef<Path>, default_level: Option<&str>) -> Result<(), RuntimeTracingError> {
+pub fn init_with(
+ logs_dir: impl AsRef<Path>,
+ default_level: Option<&str>,
+) -> Result<(), RuntimeTracingError> {
let logs_dir = logs_dir.as_ref();
ensure_dir(logs_dir)?;
@@ -18,9 +21,7 @@ pub fn init_with(logs_dir: impl AsRef<Path>, default_level: Option<&str>) -> Res
let (file_writer, guard) = tracing_appender::non_blocking(file_appender);
std::mem::forget(guard);
- let stdout_layer = fmt::layer()
- .with_writer(std::io::stdout)
- .with_target(false);
+ let stdout_layer = fmt::layer().with_writer(std::io::stdout).with_target(false);
let file_layer = fmt::layer()
.with_writer(file_writer)
diff --git a/tangle-sql-wasm/pkg/package.json b/tangle-sql-wasm/pkg/package.json
@@ -6,10 +6,13 @@
"files": [
"dist"
],
+ "main": "./dist/radroots_tangle_sql_wasm.js",
+ "types": "./dist/radroots_tangle_sql_wasm.d.ts",
"exports": {
".": {
"types": "./dist/radroots_tangle_sql_wasm.d.ts",
- "import": "./dist/radroots_tangle_sql_wasm.js"
+ "import": "./dist/radroots_tangle_sql_wasm.js",
+ "default": "./dist/radroots_tangle_sql_wasm.js"
}
},
"sideEffects": false
diff --git a/tangle-sql-wasm/src/lib.rs b/tangle-sql-wasm/src/lib.rs
@@ -2,9 +2,6 @@
use radroots_sql_core::WasmSqlExecutor;
use radroots_sql_wasm_core::{err_js, parse_json};
-use radroots_tangle_schema::log_error::{
- ILogErrorCreate, ILogErrorDelete, ILogErrorFindMany, ILogErrorFindOne, ILogErrorUpdate,
-};
use radroots_tangle_sql::migrations;
use wasm_bindgen::prelude::*;
@@ -24,6 +21,14 @@ use radroots_tangle_schema::location_gcs::{
ILocationGcsUpdate,
};
+use radroots_tangle_schema::log_error::{
+ ILogErrorCreate,
+ ILogErrorDelete,
+ ILogErrorFindMany,
+ ILogErrorFindOne,
+ ILogErrorUpdate,
+};
+
use radroots_tangle_schema::media_image::{
IMediaImageCreate,
IMediaImageDelete,
@@ -87,49 +92,17 @@ pub fn tangle_db_reset_database() -> Result<(), JsValue> {
migrations::run_all_down(&exec).map_err(err_js)
}
-#[wasm_bindgen(js_name = tangle_db_log_error_create)]
-pub fn tangle_db_log_error_create(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: ILogErrorCreate = parse_json(opts_json).map_err(err_js)?;
+#[wasm_bindgen(js_name = tangle_db_export_backup)]
+pub fn tangle_db_export_backup() -> Result<JsValue, JsValue> {
let exec = WasmSqlExecutor::new();
- let out =
- radroots_tangle_sql::log_error::create(&exec, &opts).map_err(|e| err_js(e.err))?;
- value_to_js(out)
+ let dump = radroots_tangle_sql::backup::export_database_backup(&exec).map_err(err_js)?;
+ value_to_js(dump)
}
-#[wasm_bindgen(js_name = tangle_db_log_error_find_one)]
-pub fn tangle_db_log_error_find_one(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: ILogErrorFindOne = parse_json(opts_json).map_err(err_js)?;
+#[wasm_bindgen(js_name = tangle_db_import_backup)]
+pub fn tangle_db_import_backup(dump_json: &str) -> Result<(), JsValue> {
let exec = WasmSqlExecutor::new();
- let out =
- radroots_tangle_sql::log_error::find_one(&exec, &opts).map_err(|e| err_js(e.err))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = tangle_db_log_error_find_many)]
-pub fn tangle_db_log_error_find_many(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: ILogErrorFindMany = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_tangle_sql::log_error::find_many(&exec, &opts).map_err(|e| err_js(e.err))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = tangle_db_log_error_update)]
-pub fn tangle_db_log_error_update(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: ILogErrorUpdate = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_tangle_sql::log_error::update(&exec, &opts).map_err(|e| err_js(e.err))?;
- value_to_js(out)
-}
-
-#[wasm_bindgen(js_name = tangle_db_log_error_delete)]
-pub fn tangle_db_log_error_delete(opts_json: &str) -> Result<JsValue, JsValue> {
- let opts: ILogErrorDelete = parse_json(opts_json).map_err(err_js)?;
- let exec = WasmSqlExecutor::new();
- let out =
- radroots_tangle_sql::log_error::delete(&exec, &opts).map_err(|e| err_js(e.err))?;
- value_to_js(out)
+ radroots_tangle_sql::backup::restore_database_backup_json(&exec, dump_json).map_err(err_js)
}
#[wasm_bindgen(js_name = tangle_db_farm_create)]
@@ -222,6 +195,51 @@ pub fn tangle_db_location_gcs_delete(opts_json: &str) -> Result<JsValue, JsValue
value_to_js(out)
}
+#[wasm_bindgen(js_name = tangle_db_log_error_create)]
+pub fn tangle_db_log_error_create(opts_json: &str) -> Result<JsValue, JsValue> {
+ let opts: ILogErrorCreate = parse_json(opts_json).map_err(err_js)?;
+ let exec = WasmSqlExecutor::new();
+ let out =
+ radroots_tangle_sql::log_error::create(&exec, &opts).map_err(|e| err_js(e.err))?;
+ value_to_js(out)
+}
+
+#[wasm_bindgen(js_name = tangle_db_log_error_find_one)]
+pub fn tangle_db_log_error_find_one(opts_json: &str) -> Result<JsValue, JsValue> {
+ let opts: ILogErrorFindOne = parse_json(opts_json).map_err(err_js)?;
+ let exec = WasmSqlExecutor::new();
+ let out =
+ radroots_tangle_sql::log_error::find_one(&exec, &opts).map_err(|e| err_js(e.err))?;
+ value_to_js(out)
+}
+
+#[wasm_bindgen(js_name = tangle_db_log_error_find_many)]
+pub fn tangle_db_log_error_find_many(opts_json: &str) -> Result<JsValue, JsValue> {
+ let opts: ILogErrorFindMany = parse_json(opts_json).map_err(err_js)?;
+ let exec = WasmSqlExecutor::new();
+ let out =
+ radroots_tangle_sql::log_error::find_many(&exec, &opts).map_err(|e| err_js(e.err))?;
+ value_to_js(out)
+}
+
+#[wasm_bindgen(js_name = tangle_db_log_error_update)]
+pub fn tangle_db_log_error_update(opts_json: &str) -> Result<JsValue, JsValue> {
+ let opts: ILogErrorUpdate = parse_json(opts_json).map_err(err_js)?;
+ let exec = WasmSqlExecutor::new();
+ let out =
+ radroots_tangle_sql::log_error::update(&exec, &opts).map_err(|e| err_js(e.err))?;
+ value_to_js(out)
+}
+
+#[wasm_bindgen(js_name = tangle_db_log_error_delete)]
+pub fn tangle_db_log_error_delete(opts_json: &str) -> Result<JsValue, JsValue> {
+ let opts: ILogErrorDelete = parse_json(opts_json).map_err(err_js)?;
+ let exec = WasmSqlExecutor::new();
+ let out =
+ radroots_tangle_sql::log_error::delete(&exec, &opts).map_err(|e| err_js(e.err))?;
+ value_to_js(out)
+}
+
#[wasm_bindgen(js_name = tangle_db_media_image_create)]
pub fn tangle_db_media_image_create(opts_json: &str) -> Result<JsValue, JsValue> {
let opts: IMediaImageCreate = parse_json(opts_json).map_err(err_js)?;
diff --git a/tangle-sql/Cargo.toml b/tangle-sql/Cargo.toml
@@ -19,4 +19,5 @@ embedded = ["radroots-sql-core/embedded"]
radroots-sql-core = { workspace = true }
radroots-tangle-schema = { workspace = true }
radroots-types = { workspace = true }
+serde = { workspace = true }
serde_json = { workspace = true }
diff --git a/tangle-sql/src/backup.rs b/tangle-sql/src/backup.rs
@@ -0,0 +1,290 @@
+use radroots_sql_core::{SqlExecutor, error::SqlError, utils};
+use serde::{Deserialize, Serialize};
+use serde_json::{Map, Value};
+use std::collections::{BTreeMap, HashMap};
+
+pub const DATABASE_BACKUP_VERSION: &str = "1.0.0";
+pub const TANGLE_SQL_VERSION: &str = env!("CARGO_PKG_VERSION");
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct SchemaEntry {
+ pub object_type: String,
+ pub name: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub table_name: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub sql: Option<String>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct TableData {
+ pub name: String,
+ pub rows: Vec<Map<String, Value>>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct MigrationBackup {
+ pub name: String,
+ pub up_sql: String,
+ pub down_sql: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct DatabaseBackup {
+ pub format_version: String,
+ pub tangle_sql_version: String,
+ pub schema: Vec<SchemaEntry>,
+ pub migrations: Vec<MigrationBackup>,
+ pub data: Vec<TableData>,
+}
+
+pub fn export_database_backup<E: SqlExecutor>(executor: &E) -> Result<DatabaseBackup, SqlError> {
+ let schema = load_schema(executor)?;
+ let data = read_tables_for_backup(executor, &schema)?;
+ let migrations = crate::migrations::MIGRATIONS
+ .iter()
+ .map(|m| MigrationBackup {
+ name: m.name.to_string(),
+ up_sql: m.up_sql.to_string(),
+ down_sql: m.down_sql.to_string(),
+ })
+ .collect();
+ Ok(DatabaseBackup {
+ format_version: DATABASE_BACKUP_VERSION.to_string(),
+ tangle_sql_version: TANGLE_SQL_VERSION.to_string(),
+ schema,
+ migrations,
+ data,
+ })
+}
+
+pub fn export_database_backup_json<E: SqlExecutor>(executor: &E) -> Result<String, SqlError> {
+ let backup = export_database_backup(executor)?;
+ serde_json::to_string(&backup).map_err(SqlError::from)
+}
+
+pub fn restore_database_backup<E: SqlExecutor>(
+ executor: &E,
+ backup: &DatabaseBackup,
+) -> Result<(), SqlError> {
+ validate_backup_version(backup)?;
+ executor.exec("PRAGMA foreign_keys = OFF;", "[]")?;
+ executor.begin()?;
+ let result = (|| {
+ drop_existing_objects(executor)?;
+ create_schema_from_backup(executor, &backup.schema)?;
+ insert_rows_from_backup(executor, backup)?;
+ Ok(())
+ })();
+
+ match result {
+ Ok(()) => {
+ executor.commit()?;
+ let _ = executor.exec("PRAGMA foreign_keys = ON;", "[]")?;
+ Ok(())
+ }
+ Err(err) => {
+ let _ = executor.rollback();
+ let _ = executor.exec("PRAGMA foreign_keys = ON;", "[]");
+ Err(err)
+ }
+ }
+}
+
+pub fn restore_database_backup_json<E: SqlExecutor>(
+ executor: &E,
+ backup_json: &str,
+) -> Result<(), SqlError> {
+ let backup: DatabaseBackup = serde_json::from_str(backup_json).map_err(SqlError::from)?;
+ restore_database_backup(executor, &backup)
+}
+
+fn drop_existing_objects<E: SqlExecutor>(executor: &E) -> Result<(), SqlError> {
+ #[derive(Deserialize)]
+ struct MasterRow {
+ #[serde(rename = "type")]
+ object_type: Option<String>,
+ name: Option<String>,
+ }
+ let json = executor.query_raw(
+ "select type, name from sqlite_master where name not like 'sqlite_%'",
+ "[]",
+ )?;
+ let rows: Vec<MasterRow> = utils::parse_json(&json)?;
+
+ let mut groups: HashMap<String, Vec<String>> = HashMap::new();
+ for row in rows.into_iter() {
+ let obj_type = row.object_type.unwrap_or_default();
+ let name = match row.name {
+ Some(n) => n,
+ None => continue,
+ };
+ groups.entry(obj_type).or_default().push(name);
+ }
+
+ for object_type in ["trigger", "view", "index", "table"] {
+ if let Some(names) = groups.get(object_type) {
+ for name in names {
+ let stmt = match object_type {
+ "trigger" => format!("DROP TRIGGER IF EXISTS {};", escape_identifier(name)),
+ "view" => format!("DROP VIEW IF EXISTS {};", escape_identifier(name)),
+ "index" => format!("DROP INDEX IF EXISTS {};", escape_identifier(name)),
+ _ => format!("DROP TABLE IF EXISTS {};", escape_identifier(name)),
+ };
+ let _ = executor.exec(&stmt, "[]")?;
+ }
+ }
+ }
+ Ok(())
+}
+
+fn create_schema_from_backup<E: SqlExecutor>(
+ executor: &E,
+ schema: &[SchemaEntry],
+) -> Result<(), SqlError> {
+ for entry in schema.iter().filter(|s| s.object_type == "table") {
+ if let Some(sql) = &entry.sql {
+ executor.exec(sql, "[]")?;
+ }
+ }
+ for entry in schema
+ .iter()
+ .filter(|s| s.object_type != "table" && s.sql.is_some())
+ {
+ if let Some(sql) = &entry.sql {
+ executor.exec(sql, "[]")?;
+ }
+ }
+ Ok(())
+}
+
+fn insert_rows_from_backup<E: SqlExecutor>(
+ executor: &E,
+ backup: &DatabaseBackup,
+) -> Result<(), SqlError> {
+ let mut row_sources: HashMap<&str, &Vec<Map<String, Value>>> = HashMap::new();
+ for table in &backup.data {
+ row_sources.insert(table.name.as_str(), &table.rows);
+ }
+ for entry in backup.schema.iter().filter(|s| s.object_type == "table") {
+ let rows = match row_sources.get(entry.name.as_str()) {
+ Some(r) => *r,
+ None => continue,
+ };
+ for row in rows {
+ insert_row(executor, &entry.name, row)?;
+ }
+ }
+ Ok(())
+}
+
+fn insert_row<E: SqlExecutor>(
+ executor: &E,
+ table: &str,
+ row: &Map<String, Value>,
+) -> Result<(), SqlError> {
+ if row.is_empty() {
+ return Ok(());
+ }
+
+ let mut cols: BTreeMap<String, &Value> = BTreeMap::new();
+ for (k, v) in row {
+ cols.insert(k.clone(), v);
+ }
+
+ let column_names: Vec<String> = cols.keys().cloned().collect();
+ let placeholders = (0..column_names.len())
+ .map(|_| "?")
+ .collect::<Vec<_>>()
+ .join(",");
+ let sql = format!(
+ "INSERT INTO {} ({}) VALUES ({});",
+ escape_identifier(table),
+ column_names
+ .iter()
+ .map(|c| escape_identifier(c))
+ .collect::<Vec<_>>()
+ .join(","),
+ placeholders
+ );
+
+ let binds: Vec<Value> = cols.values().map(|v| utils::to_db_bind_value(*v)).collect();
+ let params_json = serde_json::to_string(&binds).map_err(SqlError::from)?;
+ executor.exec(&sql, ¶ms_json)?;
+ Ok(())
+}
+
+fn load_schema<E: SqlExecutor>(executor: &E) -> Result<Vec<SchemaEntry>, SqlError> {
+ let json = executor.query_raw(
+ "select type, name, tbl_name as table_name, sql from sqlite_master where name not like 'sqlite_%' order by type, name",
+ "[]",
+ )?;
+ #[derive(Deserialize)]
+ struct RawSchema {
+ #[serde(rename = "type")]
+ object_type: Option<String>,
+ name: Option<String>,
+ table_name: Option<String>,
+ sql: Option<String>,
+ }
+ let rows: Vec<RawSchema> = utils::parse_json(&json)?;
+ Ok(rows
+ .into_iter()
+ .filter_map(|row| {
+ let name = row.name?;
+ let object_type = row.object_type.unwrap_or_default();
+ Some(SchemaEntry {
+ object_type,
+ name,
+ table_name: row.table_name,
+ sql: row.sql,
+ })
+ })
+ .collect())
+}
+
+fn read_tables_for_backup<E: SqlExecutor>(
+ executor: &E,
+ schema: &[SchemaEntry],
+) -> Result<Vec<TableData>, SqlError> {
+ let mut data = Vec::new();
+ for entry in schema.iter().filter(|s| s.object_type == "table") {
+ let select_sql = format!("SELECT * FROM {};", escape_identifier(&entry.name));
+ let json = executor.query_raw(&select_sql, "[]")?;
+ let rows: Vec<Map<String, Value>> = utils::parse_json(&json)?;
+ data.push(TableData {
+ name: entry.name.clone(),
+ rows,
+ });
+ }
+ Ok(data)
+}
+
+fn escape_identifier(name: &str) -> String {
+ let mut escaped = String::with_capacity(name.len() + 2);
+ escaped.push('"');
+ for c in name.chars() {
+ if c == '"' {
+ escaped.push('"');
+ }
+ escaped.push(c);
+ }
+ escaped.push('"');
+ escaped
+}
+
+fn validate_backup_version(backup: &DatabaseBackup) -> Result<(), SqlError> {
+ if backup.format_version != DATABASE_BACKUP_VERSION {
+ return Err(SqlError::InvalidArgument(format!(
+ "unsupported backup format {}, expected {}",
+ backup.format_version, DATABASE_BACKUP_VERSION
+ )));
+ }
+ if backup.tangle_sql_version != TANGLE_SQL_VERSION {
+ return Err(SqlError::InvalidArgument(format!(
+ "unsupported tangle-sql version {}, expected {}",
+ backup.tangle_sql_version, TANGLE_SQL_VERSION
+ )));
+ }
+ Ok(())
+}
diff --git a/tangle-sql/src/lib.rs b/tangle-sql/src/lib.rs
@@ -113,8 +113,10 @@ use radroots_tangle_schema::trade_product_media::{
ITradeProductMediaResolve,
};
+pub mod backup;
pub mod migrations;
pub mod models;
+pub use backup::{DatabaseBackup, MigrationBackup, SchemaEntry};
pub use models::*;
pub struct TangleSql<E: SqlExecutor> {
@@ -138,6 +140,22 @@ impl<E: SqlExecutor> TangleSql<E> {
crate::migrations::run_all_down(self.executor())
}
+ pub fn backup_database(&self) -> Result<DatabaseBackup, SqlError> {
+ crate::backup::export_database_backup(self.executor())
+ }
+
+ pub fn backup_database_json(&self) -> Result<String, SqlError> {
+ crate::backup::export_database_backup_json(self.executor())
+ }
+
+ pub fn restore_database(&self, backup: &DatabaseBackup) -> Result<(), SqlError> {
+ crate::backup::restore_database_backup(self.executor(), backup)
+ }
+
+ pub fn restore_database_json(&self, backup_json: &str) -> Result<(), SqlError> {
+ crate::backup::restore_database_backup_json(self.executor(), backup_json)
+ }
+
pub fn farm_create(
&self,
opts: &IFarmCreate,