doido_generators/commands/
db.rs1use clap::Subcommand;
14use doido_model::sea_orm_cli::{
15 handle_error, run_generate_command, run_migrate_command, Commands, GenerateSubcommands,
16};
17
18#[derive(Subcommand)]
20#[allow(clippy::large_enum_variant)]
23pub enum DbCommand {
24 Create,
26 Reset,
28 Prepare,
30 Seed,
32 Schema {
34 #[command(subcommand)]
35 action: SchemaCommand,
36 },
37 #[command(flatten)]
39 SeaOrm(Commands),
40}
41
42#[derive(Subcommand)]
44pub enum SchemaCommand {
45 Dump,
47 Load,
49}
50
51const DEFAULT_MIGRATION_DIR: &str = "db/migration";
53const DEFAULT_SEED_DIR: &str = "db/seed";
55const DEFAULT_ENTITY_OUTPUT_DIR: &str = "app/models/_entities";
57const SCHEMA_FILE: &str = "db/schema.sql";
59const SEA_ORM_CLI_DEFAULT_MIGRATION_DIR: &str = "./migration";
61const SEA_ORM_CLI_DEFAULT_OUTPUT_DIR: &str = "./";
62
63pub fn ensure_database_url_from_config() {
72 if std::env::var_os("DATABASE_URL").is_some() {
73 return;
74 }
75 if let Ok(config) = doido_model::config::YamlConfig::load() {
79 std::env::set_var("DATABASE_URL", config.database.url);
80 }
81}
82
83pub async fn run(command: DbCommand, verbose: bool) {
85 match command {
86 DbCommand::Create => create().await,
87 DbCommand::Reset => reset().await,
88 DbCommand::Prepare => prepare().await,
89 DbCommand::Seed => seed().await,
90 DbCommand::Schema { action } => schema(action).await,
91 DbCommand::SeaOrm(command) => run_sea_orm(command, verbose).await,
92 }
93}
94
95async fn connect() -> doido_model::DatabaseConnection {
97 let url = database_url();
98 match doido_model::connect_with_url(&url).await {
99 Ok(conn) => conn,
100 Err(e) => {
101 doido_core::tracing::error!("failed to connect to {url}: {e}");
102 std::process::exit(1);
103 }
104 }
105}
106
107fn read_sql_file(path: &str) -> Option<String> {
109 match std::fs::read_to_string(path) {
110 Ok(contents) => Some(contents),
111 Err(e) => {
112 doido_core::tracing::error!("could not read {path}: {e}");
113 None
114 }
115 }
116}
117
118async fn reset() {
120 let Some(schema) = read_sql_file(SCHEMA_FILE) else {
121 return;
122 };
123 let conn = connect().await;
124 match doido_model::tasks::reset(&conn, &schema).await {
125 Ok(()) => doido_core::tracing::info!("reset database from {SCHEMA_FILE}"),
126 Err(e) => doido_core::tracing::error!("db reset failed: {e}"),
127 }
128}
129
130async fn prepare() {
132 let Some(schema) = read_sql_file(SCHEMA_FILE) else {
133 return;
134 };
135 let conn = connect().await;
136 match doido_model::tasks::prepare(&conn, &schema).await {
137 Ok(()) => doido_core::tracing::info!("prepared database from {SCHEMA_FILE}"),
138 Err(e) => doido_core::tracing::error!("db prepare failed: {e}"),
139 }
140}
141
142pub fn seed_command() -> (String, Vec<String>) {
144 (
145 "cargo".to_string(),
146 vec![
147 "run".to_string(),
148 "--quiet".to_string(),
149 "--manifest-path".to_string(),
150 format!("{DEFAULT_SEED_DIR}/Cargo.toml"),
151 ],
152 )
153}
154
155async fn seed() {
158 let (program, args) = seed_command();
159 match std::process::Command::new(&program).args(&args).status() {
160 Ok(status) if status.success() => {
161 doido_core::tracing::info!("seeded database via {DEFAULT_SEED_DIR}");
162 }
163 Ok(status) => {
164 doido_core::tracing::error!(
165 "db seed failed: cargo exited with {}",
166 status.code().unwrap_or(-1)
167 );
168 }
169 Err(e) => doido_core::tracing::error!("db seed failed: {e}"),
170 }
171}
172
173async fn schema(action: SchemaCommand) {
175 let conn = connect().await;
176 match action {
177 SchemaCommand::Dump => match doido_model::schema::dump(&conn).await {
178 Ok(sql) => {
179 if let Some(parent) = std::path::Path::new(SCHEMA_FILE).parent() {
180 let _ = std::fs::create_dir_all(parent);
181 }
182 match std::fs::write(SCHEMA_FILE, sql) {
183 Ok(()) => doido_core::tracing::info!("wrote schema to {SCHEMA_FILE}"),
184 Err(e) => doido_core::tracing::error!("could not write {SCHEMA_FILE}: {e}"),
185 }
186 }
187 Err(e) => doido_core::tracing::error!("schema dump failed: {e}"),
188 },
189 SchemaCommand::Load => {
190 let Some(sql) = read_sql_file(SCHEMA_FILE) else {
191 return;
192 };
193 match doido_model::schema::load(&conn, &sql).await {
194 Ok(()) => doido_core::tracing::info!("loaded schema from {SCHEMA_FILE}"),
195 Err(e) => doido_core::tracing::error!("schema load failed: {e}"),
196 }
197 }
198 }
199}
200
201async fn create() {
203 let url = database_url();
204 match doido_model::create_database(&url).await {
205 Ok(()) => doido_core::tracing::info!("created database: {url}"),
206 Err(e) if e.to_string().contains("already exists") => {
207 doido_core::tracing::info!("database already exists: {url}");
208 }
209 Err(e) => handle_error(e),
210 }
211}
212
213fn database_url() -> String {
216 if let Ok(url) = std::env::var("DATABASE_URL") {
217 return url;
218 }
219 if let Ok(config) = doido_model::config::YamlConfig::load() {
220 return config.database.url;
221 }
222 doido_core::tracing::error!("DATABASE_URL is not set and config/<env>.yml could not be read");
223 std::process::exit(1);
224}
225
226async fn run_sea_orm(command: Commands, verbose: bool) {
228 match command {
229 Commands::Generate { mut command } => {
230 apply_entity_output_default(&mut command);
231 run_generate_command(command, verbose)
232 .await
233 .unwrap_or_else(handle_error);
234 }
235 Commands::Migrate {
236 migration_dir,
237 database_schema,
238 database_url,
239 command,
240 } => {
241 let migration_dir = override_migration_dir(migration_dir);
242 run_migrate_command(
243 command,
244 &migration_dir,
245 database_schema,
246 database_url,
247 verbose,
248 )
249 .unwrap_or_else(handle_error);
250 }
251 }
252}
253
254fn override_migration_dir(migration_dir: String) -> String {
256 if migration_dir == SEA_ORM_CLI_DEFAULT_MIGRATION_DIR {
257 DEFAULT_MIGRATION_DIR.to_string()
258 } else {
259 migration_dir
260 }
261}
262
263fn apply_entity_output_default(command: &mut GenerateSubcommands) {
265 let GenerateSubcommands::Entity { output_dir, .. } = command;
266 if output_dir == SEA_ORM_CLI_DEFAULT_OUTPUT_DIR {
267 *output_dir = DEFAULT_ENTITY_OUTPUT_DIR.to_string();
268 }
269}