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