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            short = 'l',
295            default_value = "false",
296            help = "Generate index file as `lib.rs` instead of `mod.rs`."
297        )]
298        lib: bool,
299
300        #[arg(
301            long,
302            value_delimiter = ',',
303            help = "Add extra derive macros to generated model struct (comma separated), e.g. `--model-extra-derives 'ts_rs::Ts','CustomDerive'`"
304        )]
305        model_extra_derives: Vec<String>,
306
307        #[arg(
308            long,
309            value_delimiter = ',',
310            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)'`"#
311        )]
312        model_extra_attributes: Vec<String>,
313
314        #[arg(
315            long,
316            value_delimiter = ',',
317            help = "Add extra derive macros to generated enums (comma separated), e.g. `--enum-extra-derives 'ts_rs::Ts','CustomDerive'`"
318        )]
319        enum_extra_derives: Vec<String>,
320
321        #[arg(
322            long,
323            value_delimiter = ',',
324            help = r#"Add extra attributes to generated enums, no need for `#[]` (comma separated), e.g. `--enum-extra-attributes 'serde(rename_all = "camelCase")','ts(export)'`"#
325        )]
326        enum_extra_attributes: Vec<String>,
327
328        #[arg(
329            long,
330            value_delimiter = ',',
331            help = "Add extra derive macros to generated column enum (comma separated), e.g. `--column-extra-derives 'async_graphql::Enum','CustomDerive'`"
332        )]
333        column_extra_derives: Vec<String>,
334
335        #[arg(
336            long,
337            default_value = "false",
338            long_help = "Generate helper Enumerations that are used by Seaography."
339        )]
340        seaography: bool,
341
342        #[arg(
343            long,
344            default_value = "true",
345            default_missing_value = "true",
346            num_args = 0..=1,
347            require_equals = true,
348            action = ArgAction::Set,
349            long_help = "Generate empty ActiveModelBehavior impls."
350        )]
351        impl_active_model_behavior: bool,
352
353        #[arg(
354            long,
355            default_value = "true",
356            default_missing_value = "true",
357            num_args = 0..=1,
358            require_equals = true,
359            action = ArgAction::Set,
360            long_help = indoc::indoc! { "
361                Preserve user modifications when regenerating entity files.
362                Only supports:
363                    - Extra derives and attributes of `Model` and `Relation`
364                    - Impl blocks of `ActiveModelBehavior`"
365            }
366        )]
367        preserve_user_modifications: bool,
368    },
369}
370
371#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum, Default)]
372pub enum DateTimeCrate {
373    #[default]
374    Chrono,
375    Time,
376}
377
378/// Use this to build a local, version-controlled `sea-orm-cli` in dependent projects
379/// (see [example use case](https://github.com/SeaQL/sea-orm/discussions/1889)).
380#[cfg(feature = "codegen")]
381pub async fn main() {
382    dotenv().ok();
383
384    let cli = Cli::parse();
385    let verbose = cli.verbose;
386
387    match cli.command {
388        Commands::Generate { command } => {
389            run_generate_command(command, verbose)
390                .await
391                .unwrap_or_else(handle_error);
392        }
393        Commands::Migrate {
394            migration_dir,
395            database_schema,
396            database_url,
397            command,
398        } => run_migrate_command(
399            command,
400            &migration_dir,
401            database_schema,
402            database_url,
403            verbose,
404        )
405        .unwrap_or_else(handle_error),
406    }
407}