1use clap::{ArgAction, ArgGroup, Parser, Subcommand, ValueEnum};
2#[cfg(feature = "codegen")]
3use dotenvy::dotenv;
4use std::ffi::OsStr;
5
6#[cfg(feature = "codegen")]
7use crate::{handle_error, run_generate_command, run_migrate_command};
8
9#[derive(Parser, Debug)]
10#[command(
11 version,
12 author,
13 help_template = r#"{before-help}{name} {version}
14{about-with-newline}
15
16{usage-heading} {usage}
17
18{all-args}{after-help}
19
20"#,
21 about = r#"
22 ____ ___ ____ __ __ /\
23 / ___| ___ __ _ / _ \ | _ \ | \/ | {.-}
24 \___ \ / _ \ / _` || | | || |_) || |\/| | ;_.-'\
25 ___) || __/| (_| || |_| || _ < | | | | { _.}_
26 |____/ \___| \__,_| \___/ |_| \_\|_| |_| \.-' / `,
27 \ | /
28 An async & dynamic ORM for Rust \ | ,/
29 =============================== \|_/
30
31 Getting Started
32 - Documentation: https://www.sea-ql.org/SeaORM
33 - Tutorial: https://www.sea-ql.org/sea-orm-tutorial
34 - Examples: https://github.com/SeaQL/sea-orm/tree/master/examples
35 - Cookbook: https://www.sea-ql.org/sea-orm-cookbook
36
37 Join our Discord server to chat with others in the SeaQL community!
38 - Invitation: https://discord.com/invite/uCPdDXzbdv
39
40 SeaQL Community Survey 2025
41 - Link: https://www.sea-ql.org/community-survey/
42
43 If you like what we do, consider starring, sharing and contributing!
44"#
45)]
46pub struct Cli {
47 #[arg(global = true, short, long, help = "Show debug messages")]
48 pub verbose: bool,
49
50 #[command(subcommand)]
51 pub command: Commands,
52}
53
54#[allow(clippy::large_enum_variant)]
55#[derive(Subcommand, PartialEq, Eq, Debug)]
56pub enum Commands {
57 #[command(
58 about = "Codegen related commands",
59 arg_required_else_help = true,
60 display_order = 10
61 )]
62 Generate {
63 #[command(subcommand)]
64 command: GenerateSubcommands,
65 },
66 #[command(about = "Migration related commands", display_order = 20)]
67 Migrate {
68 #[arg(
69 global = true,
70 short = 'd',
71 long,
72 env = "MIGRATION_DIR",
73 help = "Migration script directory.
74If your migrations are in their own crate,
75you can provide the root of that crate.
76If your migrations are in a submodule of your app,
77you should provide the directory of that submodule.",
78 default_value = "./migration"
79 )]
80 migration_dir: String,
81
82 #[arg(
83 global = true,
84 short = 's',
85 long,
86 env = "DATABASE_SCHEMA",
87 long_help = "Database schema\n \
88 - For MySQL and SQLite, this argument is ignored.\n \
89 - For PostgreSQL, this argument is optional with default value 'public'.\n"
90 )]
91 database_schema: Option<String>,
92
93 #[arg(
94 global = true,
95 short = 'u',
96 long,
97 env = "DATABASE_URL",
98 help = "Database URL",
99 hide_env_values = true
100 )]
101 database_url: Option<String>,
102
103 #[command(subcommand)]
104 command: Option<MigrateSubcommands>,
105 },
106}
107
108#[derive(Subcommand, PartialEq, Eq, Debug)]
109pub enum MigrateSubcommands {
110 #[command(about = "Initialize migration directory", display_order = 10)]
111 Init,
112 #[command(about = "Generate a new, empty migration", display_order = 20)]
113 Generate {
114 #[arg(required = true, help = "Name of the new migration")]
115 migration_name: String,
116
117 #[arg(
118 long,
119 default_value = "true",
120 help = "Generate migration file based on Utc time",
121 conflicts_with = "local_time",
122 display_order = 1001
123 )]
124 universal_time: bool,
125
126 #[arg(
127 long,
128 help = "Generate migration file based on Local time",
129 conflicts_with = "universal_time",
130 display_order = 1002
131 )]
132 local_time: bool,
133 },
134 #[command(
135 about = "Drop all tables from the database, then reapply all migrations",
136 display_order = 30
137 )]
138 Fresh,
139 #[command(
140 about = "Rollback all applied migrations, then reapply all migrations",
141 display_order = 40
142 )]
143 Refresh,
144 #[command(about = "Rollback all applied migrations", display_order = 50)]
145 Reset,
146 #[command(about = "Check the status of all migrations", display_order = 60)]
147 Status,
148 #[command(about = "Apply pending migrations", display_order = 70)]
149 Up {
150 #[arg(short, long, help = "Number of pending migrations to apply")]
151 num: Option<u32>,
152 },
153 #[command(about = "Rollback applied migrations", display_order = 80)]
154 Down {
155 #[arg(
156 short,
157 long,
158 default_value = "1",
159 help = "Number of applied migrations to be rolled back",
160 display_order = 90
161 )]
162 num: u32,
163 },
164}
165
166#[derive(Subcommand, PartialEq, Eq, Debug)]
167pub enum GenerateSubcommands {
168 #[command(about = "Generate entity")]
169 #[command(group(ArgGroup::new("formats").args(&["compact_format", "expanded_format", "frontend_format"])))]
170 #[command(group(ArgGroup::new("group-tables").args(&["tables", "include_hidden_tables"])))]
171 Entity {
172 #[arg(long, help = "Which format to generate entity files in")]
173 entity_format: Option<String>,
174
175 #[arg(long, help = "Generate entity file of compact format")]
176 compact_format: bool,
177
178 #[arg(long, help = "Generate entity file of expanded format")]
179 expanded_format: bool,
180
181 #[arg(long, help = "Generate entity file of frontend format")]
182 frontend_format: bool,
183
184 #[arg(
185 long,
186 help = "Generate entity file for hidden tables (i.e. table name starts with an underscore)"
187 )]
188 include_hidden_tables: bool,
189
190 #[arg(
191 short = 't',
192 long,
193 value_delimiter = ',',
194 help = "Generate entity file for specified tables only (comma separated)"
195 )]
196 tables: Vec<String>,
197
198 #[arg(
199 long,
200 value_delimiter = ',',
201 default_value = "seaql_migrations",
202 help = "Skip generating entity file for specified tables (comma separated)"
203 )]
204 ignore_tables: Vec<String>,
205
206 #[arg(
207 long,
208 default_value = "1",
209 help = "The maximum amount of connections to use when connecting to the database."
210 )]
211 max_connections: u32,
212
213 #[arg(
214 long,
215 default_value = "30",
216 long_help = "Acquire timeout in seconds of the connection used for schema discovery"
217 )]
218 acquire_timeout: u64,
219
220 #[arg(
221 short = 'o',
222 long,
223 default_value = "./",
224 help = "Entity file output directory"
225 )]
226 output_dir: String,
227
228 #[arg(
229 short = 's',
230 long,
231 env = "DATABASE_SCHEMA",
232 long_help = "Database schema\n \
233 - For MySQL, this argument is ignored.\n \
234 - For PostgreSQL, this argument is optional with default value 'public'."
235 )]
236 database_schema: Option<String>,
237
238 #[arg(
239 short = 'u',
240 long,
241 env = "DATABASE_URL",
242 help = "Database URL",
243 hide_env_values = true
244 )]
245 database_url: String,
246
247 #[arg(
248 long,
249 default_value = "all",
250 help = "Generate prelude.rs file (all, none, all-allow-unused-imports)"
251 )]
252 with_prelude: String,
253
254 #[arg(
255 long,
256 default_value = "none",
257 help = "Automatically derive serde Serialize / Deserialize traits for the entity (none, \
258 serialize, deserialize, both)"
259 )]
260 with_serde: String,
261
262 #[arg(
263 long,
264 help = "Generate a serde field attribute, '#[serde(skip_deserializing)]', for the primary key fields to skip them during deserialization, this flag will be affective only when '--with-serde' is 'both' or 'deserialize'"
265 )]
266 serde_skip_deserializing_primary_key: bool,
267
268 #[arg(
269 long,
270 default_value = "false",
271 help = "Opt-in to add skip attributes to hidden columns (i.e. when 'with-serde' enabled and column name starts with an underscore)"
272 )]
273 serde_skip_hidden_column: bool,
274
275 #[arg(
276 long,
277 default_value = "false",
278 long_help = "Automatically derive the Copy trait on generated enums.\n\
279 Enums generated from a database don't have associated data by default, and as such can \
280 derive Copy.
281 "
282 )]
283 with_copy_enums: bool,
284
285 #[arg(
286 long,
287 default_value_t,
288 value_enum,
289 help = "The datetime crate to use for generating entities."
290 )]
291 date_time_crate: DateTimeCrate,
292
293 #[arg(
294 long,
295 default_value_t,
296 value_enum,
297 help = "The primitive type to use for big integer."
298 )]
299 big_integer_type: BigIntegerType,
300
301 #[arg(
302 long,
303 short = 'l',
304 default_value = "false",
305 help = "Generate index file as `lib.rs` instead of `mod.rs`."
306 )]
307 lib: bool,
308
309 #[arg(
310 long,
311 value_delimiter = ',',
312 help = "Add extra derive macros to generated model struct (comma separated), e.g. `--model-extra-derives 'ts_rs::Ts','CustomDerive'`"
313 )]
314 model_extra_derives: Vec<String>,
315
316 #[arg(
317 long,
318 value_delimiter = ',',
319 help = r#"Add extra attributes to generated model struct, no need for `#[]` (comma separated), e.g. `--model-extra-attributes 'serde(rename_all = "camelCase")','ts(export)'`"#
320 )]
321 model_extra_attributes: Vec<String>,
322
323 #[arg(
324 long,
325 value_delimiter = ',',
326 help = "Add extra derive macros to generated enums (comma separated), e.g. `--enum-extra-derives 'ts_rs::Ts','CustomDerive'`"
327 )]
328 enum_extra_derives: Vec<String>,
329
330 #[arg(
331 long,
332 value_delimiter = ',',
333 help = r#"Add extra attributes to generated enums, no need for `#[]` (comma separated), e.g. `--enum-extra-attributes 'serde(rename_all = "camelCase")','ts(export)'`"#
334 )]
335 enum_extra_attributes: Vec<String>,
336
337 #[arg(
338 long,
339 value_delimiter = ',',
340 help = "Add extra derive macros to generated column enum (comma separated), e.g. `--column-extra-derives 'async_graphql::Enum','CustomDerive'`"
341 )]
342 column_extra_derives: Vec<String>,
343
344 #[arg(
345 long,
346 default_value = "false",
347 long_help = "Generate helper Enumerations that are used by Seaography."
348 )]
349 seaography: bool,
350
351 #[arg(
352 long,
353 default_value = "true",
354 default_missing_value = "true",
355 num_args = 0..=1,
356 require_equals = true,
357 action = ArgAction::Set,
358 long_help = "Generate empty ActiveModelBehavior impls."
359 )]
360 impl_active_model_behavior: bool,
361
362 #[arg(
363 long = "experimental-preserve-user-modifications",
364 alias = "preserve-user-modifications",
365 default_value = "false",
366 default_missing_value = "true",
367 num_args = 0..=1,
368 require_equals = true,
369 action = ArgAction::Set,
370 long_help = indoc::indoc! { "
371 Experimental!: Preserve user modifications when regenerating entity files.
372 Only supports:
373 - Extra derives and attributes of `Model` and `Relation`
374 - Impl blocks of `ActiveModelBehavior`
375 Deprecated alias: `--preserve-user-modifications`"
376 }
377 )]
378 preserve_user_modifications: bool,
379
380 #[arg(
381 long,
382 default_value_t,
383 value_enum,
384 help = "Control how the codegen version is displayed in the top banner of the generated file."
385 )]
386 banner_version: BannerVersion,
387 },
388}
389
390#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum, Default)]
391pub enum DateTimeCrate {
392 #[default]
393 Chrono,
394 Time,
395}
396
397#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum, Default)]
398pub enum BigIntegerType {
399 #[default]
400 I64,
401 I32,
402}
403
404#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum, Default)]
405pub enum BannerVersion {
406 Off,
407 Major,
408 #[default]
409 Minor,
410 Patch,
411}
412
413fn is_deprecated_preserve_user_modifications_flag(arg: &OsStr) -> bool {
414 arg.to_str()
415 .is_some_and(|arg| arg.starts_with("--preserve-user-modifications"))
416}
417
418#[cfg(feature = "codegen")]
421pub async fn main() {
422 dotenv().ok();
423
424 let deprecated_preserve_user_modifications_flag_used = std::env::args_os()
425 .skip(1)
426 .any(|arg| is_deprecated_preserve_user_modifications_flag(&arg));
427
428 let cli = Cli::parse();
429 if deprecated_preserve_user_modifications_flag_used {
430 eprintln!(
431 "warning: `--preserve-user-modifications` is deprecated; use `--experimental-preserve-user-modifications` instead."
432 );
433 }
434 let verbose = cli.verbose;
435
436 match cli.command {
437 Commands::Generate { command } => {
438 run_generate_command(command, verbose)
439 .await
440 .unwrap_or_else(handle_error);
441 }
442 Commands::Migrate {
443 migration_dir,
444 database_schema,
445 database_url,
446 command,
447 } => run_migrate_command(
448 command,
449 &migration_dir,
450 database_schema,
451 database_url,
452 verbose,
453 )
454 .unwrap_or_else(handle_error),
455 }
456}