Shared helpers for project-local db_init binaries that use PostgreSQL and SQLx migrations.
The crate is intentionally small. It does not try to own your migration path, your config schema, or your project-specific post-migration work. Instead, it gives each repository a reusable toolkit for:
- optional
.envloading - database URL resolution with explicit priority
- PostgreSQL connection setup
- SQLx migration execution
- optional post-migration hooks
Different projects still need to keep some ownership:
sqlx::migrate!()paths are compile-time and should stay in the calling repository- env/config priority often varies by application
- seed, backfill, and repair steps are business logic and usually should not live inside a generic migration helper
use anyhow::{Context, Result, anyhow};
use cloudiful_db_init::{
DbInitOptions, init_database_with_hook, load_dotenv_if_exists, resolve_database_url,
};
use config::{ReadOptions, read};
use serde::{Deserialize, Serialize};
use sqlx::migrate::Migrator;
static MIGRATOR: Migrator = sqlx::migrate!("../migrations");
#[derive(Debug, Default, Deserialize, Serialize)]
struct DbInitConfig {
app_db: Option<AppDbConfig>,
}
#[derive(Debug, Default, Deserialize, Serialize)]
struct AppDbConfig {
url: Option<String>,
}
#[tokio::main]
async fn main() {
if let Err(error) = run().await {
eprintln!("fatal: {error:#}");
std::process::exit(1);
}
}
async fn run() -> Result<()> {
load_dotenv_if_exists(".env")?;
let resolution = resolve_database_url(
None,
&["CONTEXT69_APP_DB__URL", "DATABASE_URL"],
|| {
let config: DbInitConfig = read(
"context69",
Some(ReadOptions::with_env_prefix("CONTEXT69_")),
)
.map_err(|error| anyhow!(error).context("failed to load application config"))?;
Ok(config.app_db.and_then(|app_db| app_db.url))
},
)?;
init_database_with_hook(
&resolution.database_url,
&MIGRATOR,
DbInitOptions::default(),
|_pool| async { Ok(()) },
)
.await?;
println!("database init completed");
Ok(())
}This repository publishes to crates.io from the v* tag workflow in .github/workflows/publish.yml.