use anyhow::{bail, Result};
use pushkin_core::manifest::{Db, DbDirection, Manifest};
use super::external;
use super::load_manifest;
pub fn run_drift() -> Result<i32> {
let manifest = load_manifest()?;
let db = require_db(&manifest)?;
let database_url = std::env::var("DATABASE_URL")
.map_err(|_| anyhow::anyhow!("DATABASE_URL must be set for `pushkin db drift`"))?;
let ddl = generated_sql_url(&manifest)?;
let (from, to) = match db.direction {
DbDirection::Contract => (database_url.as_str(), ddl.as_str()),
DbDirection::Database => (ddl.as_str(), database_url.as_str()),
};
let mut args = vec![
"schema",
"diff",
"--from",
from,
"--to",
to,
"--format",
"{{ sql . }}",
];
let dev_url = std::env::var("PUSHKIN_ATLAS_DEV_URL").ok();
if let Some(url) = dev_url.as_deref() {
args.extend_from_slice(&["--dev-url", url]);
}
let output = external::atlas(&args)?;
let stdout = String::from_utf8_lossy(&output.stdout);
if !output.status.success() {
bail!(
"atlas schema diff failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
);
}
if is_synced(&stdout) {
println!("pushkin db drift: schemas are synced.");
return Ok(0);
}
println!(
"pushkin db drift: schema drift detected (direction: {:?} is truth).\n\
The following changes would be required:\n{stdout}",
db.direction
);
Ok(2)
}
pub fn run_rls() -> Result<i32> {
let manifest = load_manifest()?;
require_db(&manifest)?;
let output = external::supabase(&["test", "db"])?;
let stdout = String::from_utf8_lossy(&output.stdout);
if output.status.success() {
println!("pushkin db rls: pgTAP suite passed.\n{stdout}");
return Ok(0);
}
println!("pushkin db rls: pgTAP suite FAILED.\n{stdout}");
Ok(2)
}
fn require_db(manifest: &Manifest) -> Result<&Db> {
manifest.db.as_ref().ok_or_else(|| {
anyhow::anyhow!(
"no [db] table in pushkin.toml — declare direction/provider \
before running the drift gates (spec §5.3)"
)
})
}
fn generated_sql_url(manifest: &Manifest) -> Result<String> {
let sql_contract = manifest
.contracts
.iter()
.find(|contract| contract.emit.iter().any(|emit| emit == "sql"));
let Some(contract) = sql_contract else {
bail!("no contract emits sql — nothing to diff against (add `emit = [\"sql\"]`)");
};
let path = format!("generated/{}.gen.sql", contract.name.as_str());
if !std::path::Path::new(&path).exists() {
bail!("generated DDL {path} not found — run `pushkin compile` first");
}
Ok(format!("file://{path}"))
}
fn is_synced(stdout: &str) -> bool {
let trimmed = stdout.trim();
trimmed.is_empty() || trimmed.to_ascii_lowercase().contains("synced")
}