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            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/// Use this to build a local, version-controlled `sea-orm-cli` in dependent projects
419/// (see [example use case](https://github.com/SeaQL/sea-orm/discussions/1889)).
420#[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}