lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

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:
MCargo.lock | 1+
MCargo.toml | 2+-
Mcore/src/currency.rs | 2+-
Mcore/src/decimal.rs | 4++--
Mcore/src/money.rs | 2+-
Mcore/src/percent.rs | 2+-
Mcore/src/quantity.rs | 2+-
Mcore/src/serde_ext.rs | 2+-
Mcore/src/unit.rs | 2+-
Mevents-codec/src/lib.rs | 3+--
Mevents-codec/src/profile/error.rs | 2+-
Mevents-indexed/src/lib.rs | 4++--
Mevents-indexed/src/serde_ext.rs | 2+-
Mpackage.json | 4++--
Mruntime/src/config.rs | 10++++------
Mruntime/src/tracing.rs | 11++++++-----
Mtangle-sql-wasm/pkg/package.json | 5++++-
Mtangle-sql-wasm/src/lib.rs | 102++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mtangle-sql/Cargo.toml | 1+
Atangle-sql/src/backup.rs | 290+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtangle-sql/src/lib.rs | 18++++++++++++++++++
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, &params_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,