Skip to main content

sea_orm_cli/
cli.rs

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            help = "Add extra derive macros to generated model struct, e.g. `--model-extra-derives ts_rs::Ts` or `--model-extra-derives ts_rs::Ts,CustomDerive`"
312        )]
313        model_extra_derives: Vec<String>,
314
315        #[arg(
316            long,
317            help = r#"Add extra attributes to generated model struct, no need for `#[]`, e.g. `--model-extra-attributes 'serde(rename_all = "camelCase")'` or pass multiple attributes in one argument: `--model-extra-attributes 'serde(rename_all = "camelCase"),ts(export)'`"#
318        )]
319        model_extra_attributes: Vec<String>,
320
321        #[arg(
322            long,
323            help = "Add extra derive macros to generated enums, e.g. `--enum-extra-derives ts_rs::Ts` or `--enum-extra-derives ts_rs::Ts,CustomDerive`"
324        )]
325        enum_extra_derives: Vec<String>,
326
327        #[arg(
328            long,
329            help = r#"Add extra attributes to generated enums, no need for `#[]`, e.g. `--enum-extra-attributes 'serde(rename_all = "camelCase")'` or pass multiple attributes in one argument: `--enum-extra-attributes 'serde(rename_all = "camelCase"),ts(export)'`"#
330        )]
331        enum_extra_attributes: Vec<String>,
332
333        #[arg(
334            long,
335            help = "Add extra derive macros to generated column enum, e.g. `--column-extra-derives async_graphql::Enum` or `--column-extra-derives async_graphql::Enum,Eq,PartialEq`"
336        )]
337        column_extra_derives: Vec<String>,
338
339        #[arg(
340            long,
341            default_value = "false",
342            long_help = "Generate helper Enumerations that are used by Seaography."
343        )]
344        seaography: bool,
345
346        #[arg(
347            long,
348            default_value = "true",
349            default_missing_value = "true",
350            num_args = 0..=1,
351            require_equals = true,
352            action = ArgAction::Set,
353            long_help = "Generate empty ActiveModelBehavior impls."
354        )]
355        impl_active_model_behavior: bool,
356
357        #[arg(
358            long = "experimental-preserve-user-modifications",
359            alias = "preserve-user-modifications",
360            default_value = "false",
361            default_missing_value = "true",
362            num_args = 0..=1,
363            require_equals = true,
364            action = ArgAction::Set,
365            long_help = indoc::indoc! { "
366                Experimental!: Preserve user modifications when regenerating entity files.
367                Only supports:
368                    - Extra derives and attributes of `Model` and `Relation`
369                    - Impl blocks of `ActiveModelBehavior`
370                Deprecated alias: `--preserve-user-modifications`"
371            }
372        )]
373        preserve_user_modifications: bool,
374
375        #[arg(
376            long,
377            default_value_t,
378            value_enum,
379            help = "Control how the codegen version is displayed in the top banner of the generated file."
380        )]
381        banner_version: BannerVersion,
382
383        #[arg(
384            long,
385            default_value = "false",
386            help = "Also generate a Mermaid ER diagram as `entities.mermaid` in the output directory"
387        )]
388        er_diagram: bool,
389    },
390}
391
392#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum, Default)]
393pub enum DateTimeCrate {
394    #[default]
395    Chrono,
396    Time,
397}
398
399#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum, Default)]
400pub enum BigIntegerType {
401    #[default]
402    I64,
403    I32,
404}
405
406#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum, Default)]
407pub enum BannerVersion {
408    Off,
409    Major,
410    #[default]
411    Minor,
412    Patch,
413}
414
415fn is_deprecated_preserve_user_modifications_flag(arg: &OsStr) -> bool {
416    arg.to_str()
417        .is_some_and(|arg| arg.starts_with("--preserve-user-modifications"))
418}
419
420/// Use this to build a local, version-controlled `sea-orm-cli` in dependent projects
421/// (see [example use case](https://github.com/SeaQL/sea-orm/discussions/1889)).
422#[cfg(feature = "codegen")]
423pub async fn main() {
424    dotenv().ok();
425
426    let deprecated_preserve_user_modifications_flag_used = std::env::args_os()
427        .skip(1)
428        .any(|arg| is_deprecated_preserve_user_modifications_flag(&arg));
429
430    let cli = Cli::parse();
431    if deprecated_preserve_user_modifications_flag_used {
432        eprintln!(
433            "warning: `--preserve-user-modifications` is deprecated; use `--experimental-preserve-user-modifications` instead."
434        );
435    }
436    let verbose = cli.verbose;
437
438    match cli.command {
439        Commands::Generate { command } => {
440            run_generate_command(command, verbose)
441                .await
442                .unwrap_or_else(handle_error);
443        }
444        Commands::Migrate {
445            migration_dir,
446            database_schema,
447            database_url,
448            command,
449        } => run_migrate_command(
450            command,
451            &migration_dir,
452            database_schema,
453            database_url,
454            verbose,
455        )
456        .unwrap_or_else(handle_error),
457    }
458}