sea_orm_cli/
cli.rs

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