use clap::Subcommand;
use sea_orm_cli::{
handle_error, run_generate_command, run_migrate_command, Commands, GenerateSubcommands,
};
#[derive(Subcommand)]
#[allow(clippy::large_enum_variant)]
pub enum DbCommand {
Create,
Reset,
Prepare,
Seed,
Schema {
#[command(subcommand)]
action: SchemaCommand,
},
#[command(flatten)]
SeaOrm(Commands),
}
#[derive(Subcommand)]
pub enum SchemaCommand {
Dump,
Load,
}
const DEFAULT_MIGRATION_DIR: &str = "db/migration";
const DEFAULT_ENTITY_OUTPUT_DIR: &str = "app/models/_entities";
const SCHEMA_FILE: &str = "db/schema.sql";
const SEEDS_FILE: &str = "db/seeds.sql";
const SEA_ORM_CLI_DEFAULT_MIGRATION_DIR: &str = "./migration";
const SEA_ORM_CLI_DEFAULT_OUTPUT_DIR: &str = "./";
pub fn ensure_database_url_from_config() {
if std::env::var_os("DATABASE_URL").is_some() {
return;
}
if let Ok(config) = doido_model::config::YamlConfig::load() {
std::env::set_var("DATABASE_URL", config.database.url);
}
}
pub async fn run(command: DbCommand, verbose: bool) {
match command {
DbCommand::Create => create().await,
DbCommand::Reset => reset().await,
DbCommand::Prepare => prepare().await,
DbCommand::Seed => seed().await,
DbCommand::Schema { action } => schema(action).await,
DbCommand::SeaOrm(command) => run_sea_orm(command, verbose).await,
}
}
async fn connect() -> doido_model::DatabaseConnection {
let url = database_url();
match doido_model::connect_with_url(&url).await {
Ok(conn) => conn,
Err(e) => {
doido_core::tracing::error!("failed to connect to {url}: {e}");
std::process::exit(1);
}
}
}
fn read_sql_file(path: &str) -> Option<String> {
match std::fs::read_to_string(path) {
Ok(contents) => Some(contents),
Err(e) => {
doido_core::tracing::error!("could not read {path}: {e}");
None
}
}
}
async fn reset() {
let Some(schema) = read_sql_file(SCHEMA_FILE) else {
return;
};
let conn = connect().await;
match doido_model::tasks::reset(&conn, &schema).await {
Ok(()) => doido_core::tracing::info!("reset database from {SCHEMA_FILE}"),
Err(e) => doido_core::tracing::error!("db reset failed: {e}"),
}
}
async fn prepare() {
let Some(schema) = read_sql_file(SCHEMA_FILE) else {
return;
};
let conn = connect().await;
match doido_model::tasks::prepare(&conn, &schema).await {
Ok(()) => doido_core::tracing::info!("prepared database from {SCHEMA_FILE}"),
Err(e) => doido_core::tracing::error!("db prepare failed: {e}"),
}
}
async fn seed() {
let Some(sql) = read_sql_file(SEEDS_FILE) else {
return;
};
let conn = connect().await;
match doido_model::schema::load(&conn, &sql).await {
Ok(()) => doido_core::tracing::info!("seeded database from {SEEDS_FILE}"),
Err(e) => doido_core::tracing::error!("db seed failed: {e}"),
}
}
async fn schema(action: SchemaCommand) {
let conn = connect().await;
match action {
SchemaCommand::Dump => match doido_model::schema::dump(&conn).await {
Ok(sql) => {
if let Some(parent) = std::path::Path::new(SCHEMA_FILE).parent() {
let _ = std::fs::create_dir_all(parent);
}
match std::fs::write(SCHEMA_FILE, sql) {
Ok(()) => doido_core::tracing::info!("wrote schema to {SCHEMA_FILE}"),
Err(e) => doido_core::tracing::error!("could not write {SCHEMA_FILE}: {e}"),
}
}
Err(e) => doido_core::tracing::error!("schema dump failed: {e}"),
},
SchemaCommand::Load => {
let Some(sql) = read_sql_file(SCHEMA_FILE) else {
return;
};
match doido_model::schema::load(&conn, &sql).await {
Ok(()) => doido_core::tracing::info!("loaded schema from {SCHEMA_FILE}"),
Err(e) => doido_core::tracing::error!("schema load failed: {e}"),
}
}
}
}
async fn create() {
let url = database_url();
match doido_model::create_database(&url).await {
Ok(()) => doido_core::tracing::info!("created database: {url}"),
Err(e) if e.to_string().contains("already exists") => {
doido_core::tracing::info!("database already exists: {url}");
}
Err(e) => handle_error(e),
}
}
fn database_url() -> String {
if let Ok(url) = std::env::var("DATABASE_URL") {
return url;
}
if let Ok(config) = doido_model::config::YamlConfig::load() {
return config.database.url;
}
doido_core::tracing::error!("DATABASE_URL is not set and config/<env>.yml could not be read");
std::process::exit(1);
}
async fn run_sea_orm(command: Commands, verbose: bool) {
match command {
Commands::Generate { mut command } => {
apply_entity_output_default(&mut command);
run_generate_command(command, verbose)
.await
.unwrap_or_else(handle_error);
}
Commands::Migrate {
migration_dir,
database_schema,
database_url,
command,
} => {
let migration_dir = override_migration_dir(migration_dir);
run_migrate_command(
command,
&migration_dir,
database_schema,
database_url,
verbose,
)
.unwrap_or_else(handle_error);
}
}
}
fn override_migration_dir(migration_dir: String) -> String {
if migration_dir == SEA_ORM_CLI_DEFAULT_MIGRATION_DIR {
DEFAULT_MIGRATION_DIR.to_string()
} else {
migration_dir
}
}
fn apply_entity_output_default(command: &mut GenerateSubcommands) {
let GenerateSubcommands::Entity { output_dir, .. } = command;
if output_dir == SEA_ORM_CLI_DEFAULT_OUTPUT_DIR {
*output_dir = DEFAULT_ENTITY_OUTPUT_DIR.to_string();
}
}