use std::path::Path;
#[cfg(feature = "postgres")]
use cratefield_introspect::{drift as catalog_drift, unseen};
use cratefield_tables::Step;
pub fn diff(previous: &Path, current: &Path) -> Result<(), String> {
match diff_report(previous, current)? {
0 => Ok(()),
count => Err(format!("{count} change(s) to the declaration (above)")),
}
}
pub fn diff_report(previous: &Path, current: &Path) -> Result<usize, String> {
let before = load(previous)?;
let after = load(current)?;
let changes = cratefield_tables::diff(&before.tables, &after.tables);
let declared = cratefield_manifest::declaration_diff(
&before.table_access,
&after.table_access,
&before.table_privacy,
&after.table_privacy,
);
if changes.is_empty() && declared.is_empty() {
println!(
"No change: both declare the same {} table(s), with the same access and privacy.",
after.tables.tables.len()
);
return Ok(0);
}
if !declared.is_empty() {
println!(
"{} change(s) to who may reach these tables and what they hold:\n",
declared.len()
);
for change in &declared {
println!(" {}", change.line());
}
let widened = declared
.iter()
.filter(|change| {
matches!(
change,
cratefield_manifest::DeclarationChange::Access {
direction: cratefield_manifest::Move::Widens,
..
}
)
})
.count();
if widened > 0 {
println!("\n {widened} of them widen reach. Read those before shipping.");
}
println!();
}
if changes.is_empty() {
return Ok(declared.len());
}
println!("{} change(s) to the declaration:\n", changes.len());
for change in &changes {
println!(" [{}] {}", change.step, change.line());
}
println!();
for step in Step::ALL.iter().copied() {
let count = changes.iter().filter(|change| change.step == step).count();
if count > 0 {
println!(" {count} {step}");
}
}
println!(
"\n expand: additive, a rollback stays free\n contract: safe once nothing deployed reads it\n rewrite: cannot be applied forward-only to rows that already exist"
);
Ok(changes.len() + declared.len())
}
fn load(path: &Path) -> Result<cratefield_manifest::VentureManifest, String> {
let raw = std::fs::read_to_string(path)
.map_err(|err| format!("cannot read {}: {err}", path.display()))?;
let manifest = crate::build::parse(path, &raw)?;
manifest
.validate()
.map_err(|err| format!("{}: {err}", path.display()))?;
Ok(manifest)
}
pub fn drift(manifest_path: &Path, dialect: &str, url: &str) -> Result<(), String> {
match drift_report(manifest_path, dialect, url)? {
0 => Ok(()),
count => Err(format!(
"{count} difference(s) between the declaration and the database (above)"
)),
}
}
pub fn drift_report(manifest_path: &Path, dialect: &str, url: &str) -> Result<usize, String> {
#[cfg(not(feature = "postgres"))]
{
let _ = (manifest_path, url);
Err(format!(
"dialect {dialect:?} needs an `fz` built with the `postgres` feature (cargo install cratefield-cli --features postgres)"
))
}
#[cfg(feature = "postgres")]
{
report(manifest_path, dialect, url)
}
}
#[cfg(feature = "postgres")]
fn report(manifest_path: &Path, dialect: &str, url: &str) -> Result<usize, String> {
if dialect != "postgres" {
return Err(format!(
"dialect {dialect:?} cannot be reached from here (a D1 database is read through \
wrangler, not a connection string); `--dialect postgres --url ...` is the \
direct-connection flow, the same one `fz migrations apply` uses"
));
}
let raw = std::fs::read_to_string(manifest_path)
.map_err(|err| format!("cannot read {}: {err}", manifest_path.display()))?;
let manifest = crate::build::parse(manifest_path, &raw)?;
manifest.validate().map_err(|err| err.to_string())?;
if manifest.tables.is_empty() {
println!("{} declares no tables of its own.", manifest_path.display());
return Ok(0);
}
let changes = pollster::block_on(async {
let db = cratefield_adapter_postgres::Postgres::connect(url)
.await
.map_err(|err| format!("cannot reach the database: {err}"))?;
let report = catalog_drift(&db, &manifest.tables)
.await
.map_err(|err| format!("cannot read the catalog: {err}"));
db.close().await.ok();
report
})?;
if changes.is_empty() {
println!(
"No drift: the database matches all {} declared table(s).",
manifest.tables.tables.len()
);
} else {
println!(
"{} difference(s) between the declaration and the database:\n",
changes.len()
);
for change in &changes {
println!(" [{}] {}", change.step, change.line());
}
println!();
for step in Step::ALL.iter().copied() {
let count = changes.iter().filter(|change| change.step == step).count();
if count > 0 {
println!(" {count} {step}");
}
}
}
println!("\n{}", unseen());
Ok(changes.len())
}