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 crate::commands::sync_model_extensions_at(Path::new("."));
293}
294
295fn default_entity_generate_command(database_url: String) -> GenerateSubcommands {
296 GenerateSubcommands::Entity {
297 entity_format: None,
298 compact_format: false,
299 expanded_format: false,
300 frontend_format: false,
301 include_hidden_tables: false,
302 tables: Vec::new(),
303 ignore_tables: vec!["seaql_migrations".to_string()],
304 max_connections: 1,
305 acquire_timeout: 30,
306 output_dir: SEA_ORM_CLI_DEFAULT_OUTPUT_DIR.to_string(),
307 database_schema: None,
308 database_url,
309 with_prelude: "all".to_string(),
310 with_serde: "both".to_string(),
311 serde_skip_deserializing_primary_key: false,
312 serde_skip_hidden_column: false,
313 with_copy_enums: false,
314 date_time_crate: DateTimeCrate::Chrono,
315 big_integer_type: BigIntegerType::I64,
316 lib: false,
317 model_extra_derives: Vec::new(),
318 model_extra_attributes: Vec::new(),
319 enum_extra_derives: Vec::new(),
320 enum_extra_attributes: Vec::new(),
321 column_extra_derives: Vec::new(),
322 seaography: false,
323 impl_active_model_behavior: false,
324 preserve_user_modifications: false,
325 banner_version: BannerVersion::Minor,
326 er_diagram: false,
327 }
328}
329
330fn override_migration_dir(migration_dir: String) -> String {
332 if migration_dir == SEA_ORM_CLI_DEFAULT_MIGRATION_DIR {
333 DEFAULT_MIGRATION_DIR.to_string()
334 } else {
335 migration_dir
336 }
337}
338
339fn apply_entity_output_default(command: &mut GenerateSubcommands) {
341 let GenerateSubcommands::Entity { output_dir, .. } = command;
342 if output_dir == SEA_ORM_CLI_DEFAULT_OUTPUT_DIR {
343 *output_dir = DEFAULT_ENTITY_OUTPUT_DIR.to_string();
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350
351 #[test]
352 fn schema_changing_migrate_commands_export_entities() {
353 assert!(should_export_entities(None));
354 assert!(should_export_entities(Some(&MigrateSubcommands::Up {
355 num: None
356 })));
357 assert!(should_export_entities(Some(&MigrateSubcommands::Down {
358 num: 1
359 })));
360 assert!(should_export_entities(Some(&MigrateSubcommands::Fresh)));
361 assert!(!should_export_entities(Some(&MigrateSubcommands::Status)));
362 assert!(!should_export_entities(Some(&MigrateSubcommands::Init)));
363 }
364
365 #[test]
366 fn apply_entity_output_default_rewrites_sea_orm_default() {
367 let mut command = default_entity_generate_command("sqlite://x".into());
368 apply_entity_output_default(&mut command);
369 let GenerateSubcommands::Entity { output_dir, .. } = command;
370 assert_eq!(output_dir, DEFAULT_ENTITY_OUTPUT_DIR);
371 }
372}