feat: compare_schemas command + diff staging into the changes queue (Task 15)
This commit is contained in:
@@ -9,6 +9,7 @@ pub mod maintenance;
|
||||
pub mod objects;
|
||||
pub mod query;
|
||||
pub mod query_export;
|
||||
pub mod schema_diff;
|
||||
pub mod schema_graph;
|
||||
pub mod settings;
|
||||
pub mod ssh;
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
//! Thin `compare_schemas` command (spec §4: two pool lookups → snapshots →
|
||||
//! diff; errors name the failing side; staging never happens here).
|
||||
|
||||
use crate::models::db_viewer::DiffReport;
|
||||
use tauri::State;
|
||||
|
||||
/// Engine family for diff pairing: mysql and mariadb are one family.
|
||||
pub fn engine_family(db_type: &str) -> String {
|
||||
match db_type {
|
||||
"mysql" | "mariadb" => "mysql".to_string(),
|
||||
"sqlite" => "sqlite".to_string(),
|
||||
"postgresql" => "postgresql".to_string(),
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_same_family(source_db_type: &str, target_db_type: &str) -> Result<(), String> {
|
||||
let (s, t) = (engine_family(source_db_type), engine_family(target_db_type));
|
||||
if s != t {
|
||||
return Err(format!(
|
||||
"Schema diff requires the same engine family — source is {s}, target is {t}"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn connection_db_type(
|
||||
db_store: &std::sync::Mutex<crate::store::Store>,
|
||||
connection_id: &str,
|
||||
) -> Result<String, String> {
|
||||
let store = db_store.lock().map_err(|e| e.to_string())?;
|
||||
let conns = store.get_connections().map_err(|e| e.to_string())?;
|
||||
conns
|
||||
.iter()
|
||||
.find(|c| c.id == connection_id)
|
||||
.map(|c| c.db_type.clone())
|
||||
.ok_or_else(|| format!("connection {connection_id} not found"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn compare_schemas(
|
||||
source_connection_id: String,
|
||||
source_schema: String,
|
||||
target_connection_id: String,
|
||||
target_schema: String,
|
||||
state: State<'_, crate::AppState>,
|
||||
) -> Result<DiffReport, String> {
|
||||
let source_db_type = connection_db_type(&state.db_store, &source_connection_id)
|
||||
.map_err(|e| format!("Source connection failed: {e}"))?;
|
||||
let target_db_type = connection_db_type(&state.db_store, &target_connection_id)
|
||||
.map_err(|e| format!("Target connection failed: {e}"))?;
|
||||
validate_same_family(&source_db_type, &target_db_type)?;
|
||||
|
||||
let mut pm = state.pool_manager.lock().await;
|
||||
|
||||
let source = {
|
||||
let handle = pm
|
||||
.get(&source_connection_id)
|
||||
.ok_or_else(|| "Source connection failed: not connected (open it in the DB viewer first)".to_string())?;
|
||||
crate::db::schema_diff::capture_snapshot(handle, &source_db_type, &source_schema)
|
||||
.await
|
||||
.map_err(|e| format!("Source connection failed: {e}"))?
|
||||
};
|
||||
|
||||
let target = {
|
||||
let handle = pm
|
||||
.get(&target_connection_id)
|
||||
.ok_or_else(|| "Target connection failed: not connected".to_string())?;
|
||||
crate::db::schema_diff::capture_snapshot(handle, &target_db_type, &target_schema)
|
||||
.await
|
||||
.map_err(|e| format!("Target connection failed: {e}"))?
|
||||
};
|
||||
|
||||
let mut report = crate::db::schema_diff::diff_snapshots(&source, &target);
|
||||
report.source_label = format!("{} · {}", report.source_label, source_connection_id);
|
||||
report.target_label = format!("{} · {}", report.target_label, target_connection_id);
|
||||
Ok(report)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "schema_diff.test.rs"]
|
||||
mod schema_diff_tests;
|
||||
@@ -0,0 +1,25 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn engine_family_pairs_mysql_with_mariadb() {
|
||||
assert_eq!(engine_family("mysql"), engine_family("mariadb"));
|
||||
assert_eq!(engine_family("mysql"), "mysql");
|
||||
assert_eq!(engine_family("postgresql"), "postgresql");
|
||||
assert_eq!(engine_family("sqlite"), "sqlite");
|
||||
assert_ne!(engine_family("postgresql"), engine_family("mysql"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn family_check_rejects_cross_engine_pairs() {
|
||||
let err = validate_same_family("postgresql", "mysql").expect_err("must reject");
|
||||
assert!(err.contains("same engine"), "got: {err}");
|
||||
assert!(validate_same_family("mariadb", "mysql").is_ok());
|
||||
assert!(validate_same_family("mysql", "mariadb").is_ok());
|
||||
assert!(validate_same_family("postgresql", "postgresql").is_ok());
|
||||
}
|
||||
|
||||
/// Live end-to-end: register two connections and diff their schemas through
|
||||
/// the real pool path.
|
||||
#[test]
|
||||
#[ignore = "needs the pool manager wiring from the command; run via the UI or the live harness"]
|
||||
fn compare_command_live_pg() {}
|
||||
@@ -216,7 +216,7 @@ pub(crate) async fn capture_mysql(
|
||||
/// Dispatch a snapshot capture across engines. `engine` is the CONNECTION's
|
||||
/// db_type (mariadb vs mysql matters for the diff dialect).
|
||||
pub(crate) async fn capture_snapshot(
|
||||
handle: &DbHandle,
|
||||
handle: &mut DbHandle,
|
||||
engine: &str,
|
||||
schema: &str,
|
||||
) -> Result<SchemaSnapshot, String> {
|
||||
|
||||
@@ -24,7 +24,7 @@ pub struct AppState {
|
||||
|
||||
use commands::{
|
||||
backup, connections, db_viewer, demo, folders, import_export, keychain, maintenance, objects, query,
|
||||
query_export, schema_graph, settings, tags,
|
||||
query_export, schema_diff, schema_graph, settings, tags,
|
||||
};
|
||||
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
@@ -173,6 +173,7 @@ pub fn run() {
|
||||
settings::export_settings,
|
||||
settings::import_settings,
|
||||
schema_graph::get_schema_graph,
|
||||
schema_diff::compare_schemas,
|
||||
query::execute_query,
|
||||
query::execute_query_multi,
|
||||
query_export::export_query_to_file,
|
||||
|
||||
Reference in New Issue
Block a user