use crate::bootstrap::{load_config_or_default, resolve_config_path};
use zeph_db::{DbConfig, redact_url};
pub(crate) async fn handle_db_migrate(config_path: Option<&std::path::Path>) -> anyhow::Result<()> {
let config_path = resolve_config_path(config_path);
let config = load_config_or_default(&config_path);
let db_url = crate::db_url::resolve_db_url(&config);
#[cfg(feature = "postgres")]
if !zeph_db::is_postgres_url(db_url) {
let safe = redact_url(db_url).unwrap_or_else(|| "[redacted]".to_owned());
anyhow::bail!(
"postgres build requires a postgres:// or postgresql:// URL, but got: {safe:?}. \
Set database_url in [memory] config or run: \
zeph vault set ZEPH_DATABASE_URL \"postgres://user:pass@localhost:5432/zeph\""
);
}
#[cfg(feature = "sqlite")]
if zeph_db::is_postgres_url(db_url) {
let safe = redact_url(db_url).unwrap_or_else(|| "[redacted]".to_owned());
anyhow::bail!(
"sqlite build cannot connect to a postgres:// URL: {safe:?}. \
Recompile with --features postgres or use a sqlite file path."
);
}
let display_url = redact_url(db_url).unwrap_or_else(|| "[redacted]".to_owned());
eprintln!("Running migrations on: {display_url}");
let db_config = DbConfig {
url: db_url.to_owned(),
pool_size: 1,
};
let _pool = db_config.connect().await?;
eprintln!("Migrations complete.");
Ok(())
}
#[cfg(test)]
mod tests {
use crate::cli::{Cli, Command, DbCommand};
use clap::Parser;
#[cfg(all(feature = "sqlite", not(feature = "postgres")))]
#[tokio::test]
async fn db_migrate_backend_mismatch_does_not_leak_raw_url() {
use super::handle_db_migrate;
let secret = "s3cr3t-token-do-not-leak";
let db_url = format!("postgres://db.example.com/zeph?token={secret}");
assert!(zeph_db::redact_url(&db_url).is_none());
let toml = zeph_config::Config::dump_defaults().expect("dump default config");
let toml = toml.replacen(
"[memory]\n",
&format!("[memory]\ndatabase_url = \"{db_url}\"\n"),
1,
);
let dir = tempfile::tempdir().expect("create temp dir");
let config_path = dir.path().join("config.toml");
std::fs::write(&config_path, toml).expect("write temp config");
let err = handle_db_migrate(Some(&config_path))
.await
.expect_err("sqlite build must reject a postgres:// URL");
let message = err.to_string();
assert!(
!message.contains(secret),
"error message must not leak the raw URL/secret: {message}"
);
assert!(
message.contains("[redacted]"),
"error message must show the redacted placeholder: {message}"
);
}
#[cfg(all(feature = "postgres", not(feature = "sqlite")))]
#[tokio::test]
async fn db_migrate_backend_mismatch_does_not_leak_raw_url_postgres_build() {
use super::handle_db_migrate;
let secret = "s3cr3t-token-do-not-leak";
let db_url = format!("sqlite:///data/zeph.db?token={secret}");
assert!(zeph_db::redact_url(&db_url).is_none());
let toml = zeph_config::Config::dump_defaults().expect("dump default config");
let toml = toml.replacen(
"[memory]\n",
&format!("[memory]\ndatabase_url = \"{db_url}\"\n"),
1,
);
let dir = tempfile::tempdir().expect("create temp dir");
let config_path = dir.path().join("config.toml");
std::fs::write(&config_path, toml).expect("write temp config");
let err = handle_db_migrate(Some(&config_path))
.await
.expect_err("postgres build must reject a non-postgres:// URL");
let message = err.to_string();
assert!(
!message.contains(secret),
"error message must not leak the raw URL/secret: {message}"
);
assert!(
message.contains("[redacted]"),
"error message must show the redacted placeholder: {message}"
);
}
#[test]
fn db_migrate_parses() {
let cli = Cli::try_parse_from(["zeph", "db", "migrate"]).expect("parse");
assert!(matches!(
cli.command,
Some(Command::Db {
command: DbCommand::Migrate
})
));
}
#[cfg(feature = "postgres")]
#[test]
fn is_postgres_url_accepts_postgres_schemes() {
assert!(zeph_db::is_postgres_url("postgres://localhost/test"));
assert!(zeph_db::is_postgres_url("postgresql://localhost/test"));
assert!(!zeph_db::is_postgres_url("/tmp/test.db"));
assert!(!zeph_db::is_postgres_url("sqlite:///tmp/test.db"));
}
#[cfg(feature = "sqlite")]
#[test]
fn is_postgres_url_rejects_sqlite_paths() {
assert!(!zeph_db::is_postgres_url("/tmp/test.db"));
assert!(!zeph_db::is_postgres_url("sqlite:///tmp/test.db"));
assert!(zeph_db::is_postgres_url("postgres://localhost/test"));
assert!(zeph_db::is_postgres_url("postgresql://localhost/test"));
}
}