doido_generators/commands/
db.rs1use clap::Subcommand;
18use doido_model::sea_orm_cli::{
19 handle_error, run_generate_command, run_migrate_command, BannerVersion, BigIntegerType,
20 Commands, DateTimeCrate, GenerateSubcommands, MigrateSubcommands,
21};
22use std::path::Path;
23
24#[derive(Subcommand)]
26#[allow(clippy::large_enum_variant)]
29pub enum DbCommand {
30 Create,
32 Reset,
34 Prepare,
36 Seed,
38 Schema {
40 #[command(subcommand)]
41 action: SchemaCommand,
42 },
43 #[command(flatten)]
45 SeaOrm(Commands),
46}
47
48#[derive(Subcommand)]
50pub enum SchemaCommand {
51 Dump,
53 Load,
55}
56
57const DEFAULT_MIGRATION_DIR: &str = "db/migration";
59const DEFAULT_SEED_DIR: &str = "db/seed";
61const DEFAULT_ENTITY_OUTPUT_DIR: &str = "app/models/_entities";
63const SCHEMA_FILE: &str = "db/schema.sql";
65const SEA_ORM_CLI_DEFAULT_MIGRATION_DIR: &str = "./migration";
67const SEA_ORM_CLI_DEFAULT_OUTPUT_DIR: &str = "./";
68
69pub fn ensure_database_url_from_config() {
78 if std::env::var_os("DATABASE_URL").is_some() {
79 return;
80 }
81 if let Ok(config) = doido_model::config::YamlConfig::load() {
85 std::env::set_var("DATABASE_URL", config.database.url);
86 }
87}
88
89pub async fn run(command: DbCommand, verbose: bool) {
91 match command {
92 DbCommand::Create => create().await,
93 DbCommand::Reset => reset().await,
94 DbCommand::Prepare => prepare().await,
95 DbCommand::Seed => seed().await,
96 DbCommand::Schema { action } => schema(action).await,
97 DbCommand::SeaOrm(command) => run_sea_orm(command, verbose).await,
98 }
99}
100
101async fn connect() -> doido_model::DatabaseConnection {
103 let url = database_url();
104 match doido_model::connect_with_url(&url).await {
105 Ok(conn) => conn,
106 Err(e) => {
107 doido_core::tracing::error!("failed to connect to {url}: {e}");
108 std::process::exit(1);
109 }
110 }
111}
112
113fn read_sql_file(path: &str) -> Option<String> {
115 match std::fs::read_to_string(path) {
116 Ok(contents) => Some(contents),
117 Err(e) => {
118 doido_core::tracing::error!("could not read {path}: {e}");
119 None
120 }
121 }
122}
123
124async fn reset() {
126 let Some(schema) = read_sql_file(SCHEMA_FILE) else {
127 return;
128 };
129 let conn = connect().await;
130 match doido_model::tasks::reset(&conn, &schema).await {
131 Ok(()) => doido_core::tracing::info!("reset database from {SCHEMA_FILE}"),
132 Err(e) => doido_core::tracing::error!("db reset failed: {e}"),
133 }
134}
135
136async fn prepare() {
138 let Some(schema) = read_sql_file(SCHEMA_FILE) else {
139 return;
140 };
141 let conn = connect().await;
142 match doido_model::tasks::prepare(&conn, &schema).await {
143 Ok(()) => doido_core::tracing::info!("prepared database from {SCHEMA_FILE}"),
144 Err(e) => doido_core::tracing::error!("db prepare failed: {e}"),
145 }
146}
147
148pub fn seed_command() -> (String, Vec<String>) {
150 (
151 "cargo".to_string(),
152 vec![
153 "run".to_string(),
154 "--quiet".to_string(),
155 "--manifest-path".to_string(),
156 format!("{DEFAULT_SEED_DIR}/Cargo.toml"),
157 ],
158 )
159}
160
161async fn seed() {
164 let (program, args) = seed_command();
165 match std::process::Command::new(&program).args(&args).status() {
166 Ok(status) if status.success() => {
167 doido_core::tracing::info!("seeded database via {DEFAULT_SEED_DIR}");
168 }
169 Ok(status) => {
170 doido_core::tracing::error!(
171 "db seed failed: cargo exited with {}",
172 status.code().unwrap_or(-1)
173 );
174 }
175 Err(e) => doido_core::tracing::error!("db seed failed: {e}"),
176 }
177}
178
179async fn schema(action: SchemaCommand) {
181 let conn = connect().await;
182 match action {
183 SchemaCommand::Dump => match doido_model::schema::dump(&conn).await {
184 Ok(sql) => {
185 if let Some(parent) = std::path::Path::new(SCHEMA_FILE).parent() {
186 let _ = std::fs::create_dir_all(parent);
187 }
188 match std::fs::write(SCHEMA_FILE, sql) {
189 Ok(()) => doido_core::tracing::info!("wrote schema to {SCHEMA_FILE}"),
190 Err(e) => doido_core::tracing::error!("could not write {SCHEMA_FILE}: {e}"),
191 }
192 }
193 Err(e) => doido_core::tracing::error!("schema dump failed: {e}"),
194 },
195 SchemaCommand::Load => {
196 let Some(sql) = read_sql_file(SCHEMA_FILE) else {
197 return;
198 };
199 match doido_model::schema::load(&conn, &sql).await {
200 Ok(()) => doido_core::tracing::info!("loaded schema from {SCHEMA_FILE}"),
201 Err(e) => doido_core::tracing::error!("schema load failed: {e}"),
202 }
203 }
204 }
205}
206
207async fn create() {
209 let url = database_url();
210 match doido_model::create_database(&url).await {
211 Ok(()) => doido_core::tracing::info!("created database: {url}"),
212 Err(e) if e.to_string().contains("already exists") => {
213 doido_core::tracing::info!("database already exists: {url}");
214 }
215 Err(e) => handle_error(e),
216 }
217}
218
219fn database_url() -> String {
222 if let Ok(url) = std::env::var("DATABASE_URL") {
223 return url;
224 }
225 if let Ok(config) = doido_model::config::YamlConfig::load() {
226 return config.database.url;
227 }
228 doido_core::tracing::error!("DATABASE_URL is not set and config/<env>.yml could not be read");
229 std::process::exit(1);
230}
231
232async fn run_sea_orm(command: Commands, verbose: bool) {
234 match command {
235 Commands::Generate { mut command } => {
236 apply_entity_output_default(&mut command);
237 let is_entity = matches!(&command, GenerateSubcommands::Entity { .. });
238 run_generate_command(command, verbose)
239 .await
240 .unwrap_or_else(handle_error);
241 if is_entity {
242 sync_model_extensions();
243 }
244 }
245 Commands::Migrate {
246 migration_dir,
247 database_schema,
248 database_url,
249 command,
250 } => {
251 let migration_dir = override_migration_dir(migration_dir);
252 let export = should_export_entities(command.as_ref());
253 run_migrate_command(
254 command,
255 &migration_dir,
256 database_schema,
257 database_url,
258 verbose,
259 )
260 .unwrap_or_else(handle_error);
261 if export {
262 export_entities_from_database(verbose).await;
263 }
264 }
265 }
266}
267
268fn should_export_entities(command: Option<&MigrateSubcommands>) -> bool {
270 matches!(
271 command,
272 None | Some(MigrateSubcommands::Up { .. })
273 | Some(MigrateSubcommands::Down { .. })
274 | Some(MigrateSubcommands::Fresh)
275 | Some(MigrateSubcommands::Refresh)
276 | Some(MigrateSubcommands::Reset)
277 )
278}
279
280async fn export_entities_from_database(verbose: bool) {
282 ensure_database_url_from_config();
283 let mut command = default_entity_generate_command(database_url());
284 apply_entity_output_default(&mut command);
285 if let Err(e) = run_generate_command(command, verbose).await {
286 handle_error(e);
287 }
288 sync_model_extensions();
289}
290
291fn sync_model_extensions() {
292 let entities_dir = Path::new(DEFAULT_ENTITY_OUTPUT_DIR);
293 let models_dir = Path::new("app/models");
294 match doido_model::entities::postprocess_entity_export(entities_dir, models_dir) {
295 Ok(()) => doido_core::tracing::info!("post-processed exported entities"),
296 Err(e) => doido_core::tracing::error!("entity post-process failed: {e}"),
297 }
298}
299
300fn default_entity_generate_command(database_url: String) -> GenerateSubcommands {
301 GenerateSubcommands::Entity {
302 entity_format: None,
303 compact_format: false,
304 expanded_format: false,
305 frontend_format: false,
306 include_hidden_tables: false,
307 tables: Vec::new(),
308 ignore_tables: vec!["seaql_migrations".to_string()],
309 max_connections: 1,
310 acquire_timeout: 30,
311 output_dir: SEA_ORM_CLI_DEFAULT_OUTPUT_DIR.to_string(),
312 database_schema: None,
313 database_url,
314 with_prelude: "all".to_string(),
315 with_serde: "both".to_string(),
316 serde_skip_deserializing_primary_key: false,
317 serde_skip_hidden_column: false,
318 with_copy_enums: false,
319 date_time_crate: DateTimeCrate::Chrono,
320 big_integer_type: BigIntegerType::I64,
321 lib: false,
322 model_extra_derives: Vec::new(),
323 model_extra_attributes: Vec::new(),
324 enum_extra_derives: Vec::new(),
325 enum_extra_attributes: Vec::new(),
326 column_extra_derives: Vec::new(),
327 seaography: false,
328 impl_active_model_behavior: true,
329 preserve_user_modifications: false,
330 banner_version: BannerVersion::Minor,
331 er_diagram: false,
332 }
333}
334
335fn override_migration_dir(migration_dir: String) -> String {
337 if migration_dir == SEA_ORM_CLI_DEFAULT_MIGRATION_DIR {
338 DEFAULT_MIGRATION_DIR.to_string()
339 } else {
340 migration_dir
341 }
342}
343
344fn apply_entity_output_default(command: &mut GenerateSubcommands) {
346 let GenerateSubcommands::Entity { output_dir, .. } = command;
347 if output_dir == SEA_ORM_CLI_DEFAULT_OUTPUT_DIR {
348 *output_dir = DEFAULT_ENTITY_OUTPUT_DIR.to_string();
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn schema_changing_migrate_commands_export_entities() {
358 assert!(should_export_entities(None));
359 assert!(should_export_entities(Some(&MigrateSubcommands::Up {
360 num: None
361 })));
362 assert!(should_export_entities(Some(&MigrateSubcommands::Down {
363 num: 1
364 })));
365 assert!(should_export_entities(Some(&MigrateSubcommands::Fresh)));
366 assert!(!should_export_entities(Some(&MigrateSubcommands::Status)));
367 assert!(!should_export_entities(Some(&MigrateSubcommands::Init)));
368 }
369
370 #[test]
371 fn apply_entity_output_default_rewrites_sea_orm_default() {
372 let mut command = default_entity_generate_command("sqlite://x".into());
373 apply_entity_output_default(&mut command);
374 let GenerateSubcommands::Entity { output_dir, .. } = command;
375 assert_eq!(output_dir, DEFAULT_ENTITY_OUTPUT_DIR);
376 }
377}