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
19AUTHORS:
20 {author}
21"#,
22 about = r#"
23 ____ ___ ____ __ __ /\
24 / ___| ___ __ _ / _ \ | _ \ | \/ | {.-}
25 \___ \ / _ \ / _` || | | || |_) || |\/| | ;_.-'\
26 ___) || __/| (_| || |_| || _ < | | | | { _.}_
27 |____/ \___| \__,_| \___/ |_| \_\|_| |_| \.-' / `,
28 \ | /
29 An async & dynamic ORM for Rust \ | ,/
30 =============================== \|_/
31
32 Getting Started
33 - Documentation: https://www.sea-ql.org/SeaORM
34 - Tutorial: https://www.sea-ql.org/sea-orm-tutorial
35 - Examples: https://github.com/SeaQL/sea-orm/tree/master/examples
36 - Cookbook: https://www.sea-ql.org/sea-orm-cookbook
37
38 Join our Discord server to chat with others in the SeaQL community!
39 - Invitation: https://discord.com/invite/uCPdDXzbdv
40
41 SeaQL Community Survey 2025
42 - Link: https://www.sea-ql.org/community-survey
43
44 If you like what we do, consider starring, sharing and contributing!
45"#
46)]
47pub struct Cli {
48 #[arg(global = true, short, long, help = "Show debug messages")]
49 pub verbose: bool,
50
51 #[command(subcommand)]
52 pub command: Commands,
53}
54
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 = "Generate entity file of compact format")]
173 compact_format: bool,
174
175 #[arg(long, help = "Generate entity file of expanded format")]
176 expanded_format: bool,
177
178 #[arg(long, help = "Generate entity file of frontend format")]
179 frontend_format: bool,
180
181 #[arg(
182 long,
183 help = "Generate entity file for hidden tables (i.e. table name starts with an underscore)"
184 )]
185 include_hidden_tables: bool,
186
187 #[arg(
188 short = 't',
189 long,
190 value_delimiter = ',',
191 help = "Generate entity file for specified tables only (comma separated)"
192 )]
193 tables: Vec<String>,
194
195 #[arg(
196 long,
197 value_delimiter = ',',
198 default_value = "seaql_migrations",
199 help = "Skip generating entity file for specified tables (comma separated)"
200 )]
201 ignore_tables: Vec<String>,
202
203 #[arg(
204 long,
205 default_value = "1",
206 help = "The maximum amount of connections to use when connecting to the database."
207 )]
208 max_connections: u32,
209
210 #[arg(
211 long,
212 default_value = "30",
213 long_help = "Acquire timeout in seconds of the connection used for schema discovery"
214 )]
215 acquire_timeout: u64,
216
217 #[arg(
218 short = 'o',
219 long,
220 default_value = "./",
221 help = "Entity file output directory"
222 )]
223 output_dir: String,
224
225 #[arg(
226 short = 's',
227 long,
228 env = "DATABASE_SCHEMA",
229 long_help = "Database schema\n \
230 - For MySQL, this argument is ignored.\n \
231 - For PostgreSQL, this argument is optional with default value 'public'."
232 )]
233 database_schema: Option<String>,
234
235 #[arg(
236 short = 'u',
237 long,
238 env = "DATABASE_URL",
239 help = "Database URL",
240 hide_env_values = true
241 )]
242 database_url: String,
243
244 #[arg(
245 long,
246 default_value = "all",
247 help = "Generate prelude.rs file (all, none, all-allow-unused-imports)"
248 )]
249 with_prelude: String,
250
251 #[arg(
252 long,
253 default_value = "none",
254 help = "Automatically derive serde Serialize / Deserialize traits for the entity (none, \
255 serialize, deserialize, both)"
256 )]
257 with_serde: String,
258
259 #[arg(
260 long,
261 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'"
262 )]
263 serde_skip_deserializing_primary_key: bool,
264
265 #[arg(
266 long,
267 default_value = "false",
268 help = "Opt-in to add skip attributes to hidden columns (i.e. when 'with-serde' enabled and column name starts with an underscore)"
269 )]
270 serde_skip_hidden_column: bool,
271
272 #[arg(
273 long,
274 default_value = "false",
275 long_help = "Automatically derive the Copy trait on generated enums.\n\
276 Enums generated from a database don't have associated data by default, and as such can \
277 derive Copy.
278 "
279 )]
280 with_copy_enums: bool,
281
282 #[arg(
283 long,
284 default_value_t,
285 value_enum,
286 help = "The datetime crate to use for generating entities."
287 )]
288 date_time_crate: DateTimeCrate,
289
290 #[arg(
291 long,
292 short = 'l',
293 default_value = "false",
294 help = "Generate index file as `lib.rs` instead of `mod.rs`."
295 )]
296 lib: bool,
297
298 #[arg(
299 long,
300 value_delimiter = ',',
301 help = "Add extra derive macros to generated model struct (comma separated), e.g. `--model-extra-derives 'ts_rs::Ts','CustomDerive'`"
302 )]
303 model_extra_derives: Vec<String>,
304
305 #[arg(
306 long,
307 value_delimiter = ',',
308 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)'`"#
309 )]
310 model_extra_attributes: Vec<String>,
311
312 #[arg(
313 long,
314 value_delimiter = ',',
315 help = "Add extra derive macros to generated enums (comma separated), e.g. `--enum-extra-derives 'ts_rs::Ts','CustomDerive'`"
316 )]
317 enum_extra_derives: Vec<String>,
318
319 #[arg(
320 long,
321 value_delimiter = ',',
322 help = r#"Add extra attributes to generated enums, no need for `#[]` (comma separated), e.g. `--enum-extra-attributes 'serde(rename_all = "camelCase")','ts(export)'`"#
323 )]
324 enum_extra_attributes: Vec<String>,
325
326 #[arg(
327 long,
328 default_value = "false",
329 long_help = "Generate helper Enumerations that are used by Seaography."
330 )]
331 seaography: bool,
332
333 #[arg(
334 long,
335 default_value = "true",
336 default_missing_value = "true",
337 num_args = 0..=1,
338 require_equals = true,
339 action = ArgAction::Set,
340 long_help = "Generate empty ActiveModelBehavior impls."
341 )]
342 impl_active_model_behavior: bool,
343 },
344}
345
346#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum, Default)]
347pub enum DateTimeCrate {
348 #[default]
349 Chrono,
350 Time,
351}
352
353#[cfg(feature = "codegen")]
356pub async fn main() {
357 dotenv().ok();
358
359 let cli = Cli::parse();
360 let verbose = cli.verbose;
361
362 match cli.command {
363 Commands::Generate { command } => {
364 run_generate_command(command, verbose)
365 .await
366 .unwrap_or_else(handle_error);
367 }
368 Commands::Migrate {
369 migration_dir,
370 database_schema,
371 database_url,
372 command,
373 } => run_migrate_command(
374 command,
375 &migration_dir,
376 database_schema,
377 database_url,
378 verbose,
379 )
380 .unwrap_or_else(handle_error),
381 }
382}