use std::path::Path;
use crate::db::inspector::inspect_database;
use crate::db::types::{DatabaseSummary, SchemaDiff, TableDataDiff};
use crate::diff::{data, export, schema};
use crate::error::Result;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DiffProgressPhase {
InspectingLeft,
InspectingRight,
DiffingSchema,
DiffingTable {
table_name: String,
table_index: usize,
total_tables: usize,
},
GeneratingSqlExport,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DiffProgress {
pub phase: DiffProgressPhase,
pub completed_steps: usize,
pub total_steps: Option<usize>,
}
#[derive(Clone, Debug)]
pub struct DatabaseDiff {
pub left: DatabaseSummary,
pub right: DatabaseSummary,
pub schema: SchemaDiff,
pub data_diffs: Vec<TableDataDiff>,
pub sql_export: String,
}
pub fn diff_databases(left_path: &Path, right_path: &Path) -> Result<DatabaseDiff> {
diff_databases_with_progress(left_path, right_path, |_| {})
}
pub fn diff_databases_with_progress<F>(
left_path: &Path,
right_path: &Path,
mut on_progress: F,
) -> Result<DatabaseDiff>
where
F: FnMut(DiffProgress),
{
on_progress(DiffProgress {
phase: DiffProgressPhase::InspectingLeft,
completed_steps: 0,
total_steps: None,
});
let left = inspect_database(left_path)?;
on_progress(DiffProgress {
phase: DiffProgressPhase::InspectingRight,
completed_steps: 1,
total_steps: None,
});
let right = inspect_database(right_path)?;
let shared_table_count = left
.tables
.iter()
.filter(|left_table| {
right
.tables
.iter()
.any(|table| table.name == left_table.name)
})
.count();
let total_steps = shared_table_count + 4;
on_progress(DiffProgress {
phase: DiffProgressPhase::DiffingSchema,
completed_steps: 2,
total_steps: Some(total_steps),
});
let schema = schema::diff_schema(&left, &right);
let data_diffs =
data::diff_all_tables_with_progress(left_path, right_path, &left, &right, |progress| {
on_progress(DiffProgress {
phase: DiffProgressPhase::DiffingTable {
table_name: progress.table_name,
table_index: progress.table_index,
total_tables: progress.total_tables,
},
completed_steps: 3 + progress.table_index,
total_steps: Some(total_steps),
});
})?;
on_progress(DiffProgress {
phase: DiffProgressPhase::GeneratingSqlExport,
completed_steps: 3 + shared_table_count,
total_steps: Some(total_steps),
});
let sql_export = export::export_diff_as_sql(right_path, &left, &right, &schema, &data_diffs)?;
Ok(DatabaseDiff {
left,
right,
schema,
data_diffs,
sql_export,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use rusqlite::Connection;
use tempfile::TempDir;
#[test]
fn diff_databases_with_progress_reports_expected_stages() -> Result<()> {
let fixture = FixtureDbs::new()?;
let mut progress = Vec::new();
let diff = diff_databases_with_progress(&fixture.left, &fixture.right, |update| {
progress.push(update);
})?;
assert_eq!(diff.data_diffs.len(), 1);
assert_eq!(
progress,
vec![
DiffProgress {
phase: DiffProgressPhase::InspectingLeft,
completed_steps: 0,
total_steps: None,
},
DiffProgress {
phase: DiffProgressPhase::InspectingRight,
completed_steps: 1,
total_steps: None,
},
DiffProgress {
phase: DiffProgressPhase::DiffingSchema,
completed_steps: 2,
total_steps: Some(5),
},
DiffProgress {
phase: DiffProgressPhase::DiffingTable {
table_name: "widgets".to_owned(),
table_index: 0,
total_tables: 1,
},
completed_steps: 3,
total_steps: Some(5),
},
DiffProgress {
phase: DiffProgressPhase::GeneratingSqlExport,
completed_steps: 4,
total_steps: Some(5),
},
]
);
Ok(())
}
struct FixtureDbs {
_tempdir: TempDir,
left: PathBuf,
right: PathBuf,
}
impl FixtureDbs {
fn new() -> Result<Self> {
let tempdir = tempfile::tempdir()?;
let left = Self::create_db_at(
tempdir.path().join("left.sqlite"),
&[
"CREATE TABLE widgets (id INTEGER PRIMARY KEY, name TEXT NOT NULL);",
"INSERT INTO widgets (id, name) VALUES (1, 'left-a'), (2, 'left-b');",
"CREATE TABLE only_left (id INTEGER PRIMARY KEY);",
],
)?;
let right = Self::create_db_at(
tempdir.path().join("right.sqlite"),
&[
"CREATE TABLE widgets (id INTEGER PRIMARY KEY, name TEXT NOT NULL);",
"INSERT INTO widgets (id, name) VALUES (1, 'right');",
"CREATE TABLE only_right (id INTEGER PRIMARY KEY);",
],
)?;
Ok(Self {
_tempdir: tempdir,
left,
right,
})
}
fn create_db_at(path: PathBuf, statements: &[&str]) -> Result<PathBuf> {
let connection = Connection::open(&path)?;
for statement in statements {
connection.execute_batch(statement)?;
}
Ok(path)
}
}
}