Vendor dependencies

This commit is contained in:
2026-08-01 16:11:49 +03:00
parent 7f139a0241
commit 6b5e7f0f8b
29706 changed files with 9575646 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
#[cfg_attr(
any(
feature = "runtime-async-std",
feature = "runtime-async-std-native-tls",
feature = "runtime-async-std-rustls"
),
async_std::main
)]
#[cfg_attr(
any(
feature = "runtime-tokio",
feature = "runtime-tokio-native-tls",
feature = "runtime-tokio-rustls",
),
tokio::main
)]
async fn main() {
sea_orm_cli::main().await
}
+458
View File
@@ -0,0 +1,458 @@
use clap::{ArgAction, ArgGroup, Parser, Subcommand, ValueEnum};
#[cfg(feature = "codegen")]
use dotenvy::dotenv;
use std::ffi::OsStr;
#[cfg(feature = "codegen")]
use crate::{handle_error, run_generate_command, run_migrate_command};
#[derive(Parser, Debug)]
#[command(
version,
author,
help_template = r#"{before-help}{name} {version}
{about-with-newline}
{usage-heading} {usage}
{all-args}{after-help}
"#,
about = r#"
____ ___ ____ __ __ /\
/ ___| ___ __ _ / _ \ | _ \ | \/ | {.-}
\___ \ / _ \ / _` || | | || |_) || |\/| | ;_.-'\
___) || __/| (_| || |_| || _ < | | | | { _.}_
|____/ \___| \__,_| \___/ |_| \_\|_| |_| \.-' / `,
\ | /
An async & dynamic ORM for Rust \ | ,/
=============================== \|_/
Getting Started
- Documentation: https://www.sea-ql.org/SeaORM
- Tutorial: https://www.sea-ql.org/sea-orm-tutorial
- Examples: https://github.com/SeaQL/sea-orm/tree/master/examples
- Cookbook: https://www.sea-ql.org/sea-orm-cookbook
Join our Discord server to chat with others in the SeaQL community!
- Invitation: https://discord.com/invite/uCPdDXzbdv
SeaQL Community Survey 2025
- Link: https://www.sea-ql.org/community-survey/
If you like what we do, consider starring, sharing and contributing!
"#
)]
pub struct Cli {
#[arg(global = true, short, long, help = "Show debug messages")]
pub verbose: bool,
#[command(subcommand)]
pub command: Commands,
}
#[allow(clippy::large_enum_variant)]
#[derive(Subcommand, PartialEq, Eq, Debug)]
pub enum Commands {
#[command(
about = "Codegen related commands",
arg_required_else_help = true,
display_order = 10
)]
Generate {
#[command(subcommand)]
command: GenerateSubcommands,
},
#[command(about = "Migration related commands", display_order = 20)]
Migrate {
#[arg(
global = true,
short = 'd',
long,
env = "MIGRATION_DIR",
help = "Migration script directory.
If your migrations are in their own crate,
you can provide the root of that crate.
If your migrations are in a submodule of your app,
you should provide the directory of that submodule.",
default_value = "./migration"
)]
migration_dir: String,
#[arg(
global = true,
short = 's',
long,
env = "DATABASE_SCHEMA",
long_help = "Database schema\n \
- For MySQL and SQLite, this argument is ignored.\n \
- For PostgreSQL, this argument is optional with default value 'public'.\n"
)]
database_schema: Option<String>,
#[arg(
global = true,
short = 'u',
long,
env = "DATABASE_URL",
help = "Database URL",
hide_env_values = true
)]
database_url: Option<String>,
#[command(subcommand)]
command: Option<MigrateSubcommands>,
},
}
#[derive(Subcommand, PartialEq, Eq, Debug)]
pub enum MigrateSubcommands {
#[command(about = "Initialize migration directory", display_order = 10)]
Init,
#[command(about = "Generate a new, empty migration", display_order = 20)]
Generate {
#[arg(required = true, help = "Name of the new migration")]
migration_name: String,
#[arg(
long,
default_value = "true",
help = "Generate migration file based on Utc time",
conflicts_with = "local_time",
display_order = 1001
)]
universal_time: bool,
#[arg(
long,
help = "Generate migration file based on Local time",
conflicts_with = "universal_time",
display_order = 1002
)]
local_time: bool,
},
#[command(
about = "Drop all tables from the database, then reapply all migrations",
display_order = 30
)]
Fresh,
#[command(
about = "Rollback all applied migrations, then reapply all migrations",
display_order = 40
)]
Refresh,
#[command(about = "Rollback all applied migrations", display_order = 50)]
Reset,
#[command(about = "Check the status of all migrations", display_order = 60)]
Status,
#[command(about = "Apply pending migrations", display_order = 70)]
Up {
#[arg(short, long, help = "Number of pending migrations to apply")]
num: Option<u32>,
},
#[command(about = "Rollback applied migrations", display_order = 80)]
Down {
#[arg(
short,
long,
default_value = "1",
help = "Number of applied migrations to be rolled back",
display_order = 90
)]
num: u32,
},
}
#[derive(Subcommand, PartialEq, Eq, Debug)]
pub enum GenerateSubcommands {
#[command(about = "Generate entity")]
#[command(group(ArgGroup::new("formats").args(&["compact_format", "expanded_format", "frontend_format"])))]
#[command(group(ArgGroup::new("group-tables").args(&["tables", "include_hidden_tables"])))]
Entity {
#[arg(long, help = "Which format to generate entity files in")]
entity_format: Option<String>,
#[arg(long, help = "Generate entity file of compact format")]
compact_format: bool,
#[arg(long, help = "Generate entity file of expanded format")]
expanded_format: bool,
#[arg(long, help = "Generate entity file of frontend format")]
frontend_format: bool,
#[arg(
long,
help = "Generate entity file for hidden tables (i.e. table name starts with an underscore)"
)]
include_hidden_tables: bool,
#[arg(
short = 't',
long,
value_delimiter = ',',
help = "Generate entity file for specified tables only (comma separated)"
)]
tables: Vec<String>,
#[arg(
long,
value_delimiter = ',',
default_value = "seaql_migrations",
help = "Skip generating entity file for specified tables (comma separated)"
)]
ignore_tables: Vec<String>,
#[arg(
long,
default_value = "1",
help = "The maximum amount of connections to use when connecting to the database."
)]
max_connections: u32,
#[arg(
long,
default_value = "30",
long_help = "Acquire timeout in seconds of the connection used for schema discovery"
)]
acquire_timeout: u64,
#[arg(
short = 'o',
long,
default_value = "./",
help = "Entity file output directory"
)]
output_dir: String,
#[arg(
short = 's',
long,
env = "DATABASE_SCHEMA",
long_help = "Database schema\n \
- For MySQL, this argument is ignored.\n \
- For PostgreSQL, this argument is optional with default value 'public'."
)]
database_schema: Option<String>,
#[arg(
short = 'u',
long,
env = "DATABASE_URL",
help = "Database URL",
hide_env_values = true
)]
database_url: String,
#[arg(
long,
default_value = "all",
help = "Generate prelude.rs file (all, none, all-allow-unused-imports)"
)]
with_prelude: String,
#[arg(
long,
default_value = "none",
help = "Automatically derive serde Serialize / Deserialize traits for the entity (none, \
serialize, deserialize, both)"
)]
with_serde: String,
#[arg(
long,
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'"
)]
serde_skip_deserializing_primary_key: bool,
#[arg(
long,
default_value = "false",
help = "Opt-in to add skip attributes to hidden columns (i.e. when 'with-serde' enabled and column name starts with an underscore)"
)]
serde_skip_hidden_column: bool,
#[arg(
long,
default_value = "false",
long_help = "Automatically derive the Copy trait on generated enums.\n\
Enums generated from a database don't have associated data by default, and as such can \
derive Copy.
"
)]
with_copy_enums: bool,
#[arg(
long,
default_value_t,
value_enum,
help = "The datetime crate to use for generating entities."
)]
date_time_crate: DateTimeCrate,
#[arg(
long,
default_value_t,
value_enum,
help = "The primitive type to use for big integer."
)]
big_integer_type: BigIntegerType,
#[arg(
long,
short = 'l',
default_value = "false",
help = "Generate index file as `lib.rs` instead of `mod.rs`."
)]
lib: bool,
#[arg(
long,
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`"
)]
model_extra_derives: Vec<String>,
#[arg(
long,
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)'`"#
)]
model_extra_attributes: Vec<String>,
#[arg(
long,
help = "Add extra derive macros to generated enums, e.g. `--enum-extra-derives ts_rs::Ts` or `--enum-extra-derives ts_rs::Ts,CustomDerive`"
)]
enum_extra_derives: Vec<String>,
#[arg(
long,
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)'`"#
)]
enum_extra_attributes: Vec<String>,
#[arg(
long,
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`"
)]
column_extra_derives: Vec<String>,
#[arg(
long,
default_value = "false",
long_help = "Generate helper Enumerations that are used by Seaography."
)]
seaography: bool,
#[arg(
long,
default_value = "true",
default_missing_value = "true",
num_args = 0..=1,
require_equals = true,
action = ArgAction::Set,
long_help = "Generate empty ActiveModelBehavior impls."
)]
impl_active_model_behavior: bool,
#[arg(
long = "experimental-preserve-user-modifications",
alias = "preserve-user-modifications",
default_value = "false",
default_missing_value = "true",
num_args = 0..=1,
require_equals = true,
action = ArgAction::Set,
long_help = indoc::indoc! { "
Experimental!: Preserve user modifications when regenerating entity files.
Only supports:
- Extra derives and attributes of `Model` and `Relation`
- Impl blocks of `ActiveModelBehavior`
Deprecated alias: `--preserve-user-modifications`"
}
)]
preserve_user_modifications: bool,
#[arg(
long,
default_value_t,
value_enum,
help = "Control how the codegen version is displayed in the top banner of the generated file."
)]
banner_version: BannerVersion,
#[arg(
long,
default_value = "false",
help = "Also generate a Mermaid ER diagram as `entities.mermaid` in the output directory"
)]
er_diagram: bool,
},
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum, Default)]
pub enum DateTimeCrate {
#[default]
Chrono,
Time,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum, Default)]
pub enum BigIntegerType {
#[default]
I64,
I32,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum, Default)]
pub enum BannerVersion {
Off,
Major,
#[default]
Minor,
Patch,
}
fn is_deprecated_preserve_user_modifications_flag(arg: &OsStr) -> bool {
arg.to_str()
.is_some_and(|arg| arg.starts_with("--preserve-user-modifications"))
}
/// Use this to build a local, version-controlled `sea-orm-cli` in dependent projects
/// (see [example use case](https://github.com/SeaQL/sea-orm/discussions/1889)).
#[cfg(feature = "codegen")]
pub async fn main() {
dotenv().ok();
let deprecated_preserve_user_modifications_flag_used = std::env::args_os()
.skip(1)
.any(|arg| is_deprecated_preserve_user_modifications_flag(&arg));
let cli = Cli::parse();
if deprecated_preserve_user_modifications_flag_used {
eprintln!(
"warning: `--preserve-user-modifications` is deprecated; use `--experimental-preserve-user-modifications` instead."
);
}
let verbose = cli.verbose;
match cli.command {
Commands::Generate { command } => {
run_generate_command(command, verbose)
.await
.unwrap_or_else(handle_error);
}
Commands::Migrate {
migration_dir,
database_schema,
database_url,
command,
} => run_migrate_command(
command,
&migration_dir,
database_schema,
database_url,
verbose,
)
.unwrap_or_else(handle_error),
}
}
@@ -0,0 +1,753 @@
use crate::{BannerVersion, BigIntegerType, DateTimeCrate, GenerateSubcommands};
use core::time;
use sea_orm_codegen::{
BannerVersion as CodegenBannerVersion, BigIntegerType as CodegenBigIntegerType,
DateTimeCrate as CodegenDateTimeCrate, EntityFormat, EntityTransformer, EntityWriterContext,
MergeReport, OutputFile, WithPrelude, WithSerde, merge_entity_files,
};
use std::{error::Error, fs, path::Path, process::Command, str::FromStr};
use tracing_subscriber::{EnvFilter, prelude::*};
use url::Url;
/// Split a string by comma while respecting parentheses nesting.
/// This allows attributes like `test(a, b)` to be treated as a single value
/// instead of being split into `test(a` and ` b)`.
fn split_by_comma_ignoring_parentheses(s: &str) -> Vec<String> {
let mut result = Vec::new();
let mut current = String::new();
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
let mut brace_depth = 0usize;
for c in s.chars() {
match c {
'(' => {
paren_depth += 1;
current.push(c);
}
')' => {
paren_depth = paren_depth.saturating_sub(1);
current.push(c);
}
'[' => {
bracket_depth += 1;
current.push(c);
}
']' => {
bracket_depth = bracket_depth.saturating_sub(1);
current.push(c);
}
'{' => {
brace_depth += 1;
current.push(c);
}
'}' => {
brace_depth = brace_depth.saturating_sub(1);
current.push(c);
}
',' if paren_depth == 0 && bracket_depth == 0 && brace_depth == 0 => {
let trimmed = current.trim();
if !trimmed.is_empty() {
result.push(trimmed.to_string());
}
current.clear();
}
_ => {
current.push(c);
}
}
}
// Add the last segment
let trimmed = current.trim();
if !trimmed.is_empty() {
result.push(trimmed.to_string());
}
result
}
/// Process a vector of strings that may contain comma-separated values with nested parentheses.
/// This handles the case where clap no longer splits by comma, so we need to manually split
/// each string while respecting parentheses nesting.
fn process_comma_separated_values(values: Vec<String>) -> Vec<String> {
values
.into_iter()
.flat_map(|s| split_by_comma_ignoring_parentheses(&s))
.collect()
}
/// Whether a discovered SQLite column is a generated (computed) column.
///
/// Generated columns cannot be inserted or updated, so they are dropped from
/// generated entities — emitting them as ordinary fields makes every
/// `INSERT`/`UPDATE` fail with "cannot INSERT/UPDATE a generated column" (#3094).
#[cfg(feature = "sqlx-sqlite")]
fn sqlite_column_is_generated(col: &sea_schema::sqlite::def::ColumnInfo) -> bool {
use sea_schema::sqlite::def::ColumnVisibility;
matches!(
col.hidden,
ColumnVisibility::GeneratedVirtual | ColumnVisibility::GeneratedStored
)
}
pub async fn run_generate_command(
command: GenerateSubcommands,
verbose: bool,
) -> Result<(), Box<dyn Error>> {
match command {
GenerateSubcommands::Entity {
entity_format,
compact_format: _,
expanded_format,
frontend_format,
include_hidden_tables,
tables,
ignore_tables,
max_connections,
acquire_timeout,
output_dir,
database_schema,
database_url,
with_prelude,
with_serde,
serde_skip_deserializing_primary_key,
serde_skip_hidden_column,
with_copy_enums,
date_time_crate,
big_integer_type,
lib,
model_extra_derives,
model_extra_attributes,
enum_extra_derives,
enum_extra_attributes,
column_extra_derives,
seaography,
impl_active_model_behavior,
preserve_user_modifications,
banner_version,
er_diagram,
} => {
if verbose {
let _ = tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG)
.with_test_writer()
.try_init();
} else {
let filter_layer = EnvFilter::try_new("sea_orm_codegen=info").unwrap();
let fmt_layer = tracing_subscriber::fmt::layer()
.with_target(false)
.with_level(false)
.without_time();
let _ = tracing_subscriber::registry()
.with(filter_layer)
.with(fmt_layer)
.try_init();
}
// The database should be a valid URL that can be parsed
// protocol://username:password@host/database_name
let url = Url::parse(&database_url)?;
// Make sure we have all the required url components
//
// Missing scheme will have been caught by the Url::parse() call
// above
let is_sqlite = url.scheme() == "sqlite";
// Closures for filtering tables
let filter_tables =
|table: &String| -> bool { tables.is_empty() || tables.contains(table) };
let filter_hidden_tables = |table: &str| -> bool {
if include_hidden_tables {
true
} else {
!table.starts_with('_')
}
};
let filter_skip_tables = |table: &String| -> bool { !ignore_tables.contains(table) };
let _database_name = if !is_sqlite {
// The database name should be the first element of the path string
//
// Throwing an error if there is no database name since it might be
// accepted by the database without it, while we're looking to dump
// information from a particular database
let database_name = url
.path_segments()
.unwrap_or_else(|| {
panic!(
"There is no database name as part of the url path: {}",
url.as_str()
)
})
.next()
.unwrap();
// An empty string as the database name is also an error
if database_name.is_empty() {
panic!(
"There is no database name as part of the url path: {}",
url.as_str()
);
}
database_name
} else {
Default::default()
};
let (schema_name, table_stmts) = match url.scheme() {
"mysql" => {
#[cfg(not(feature = "sqlx-mysql"))]
{
panic!("mysql feature is off")
}
#[cfg(feature = "sqlx-mysql")]
{
use sea_schema::mysql::discovery::SchemaDiscovery;
use sqlx::MySql;
println!("Connecting to MySQL ...");
let connection = sqlx_connect::<MySql>(
max_connections,
acquire_timeout,
url.as_str(),
None,
)
.await?;
println!("Discovering schema ...");
let schema_discovery = SchemaDiscovery::new(connection, _database_name);
let schema = schema_discovery.discover().await?;
let table_stmts = schema
.tables
.into_iter()
.filter(|schema| filter_tables(&schema.info.name))
.filter(|schema| filter_hidden_tables(&schema.info.name))
.filter(|schema| filter_skip_tables(&schema.info.name))
.map(|mut schema| {
// Skip generated columns (see #3094).
schema.columns.retain(|col| !col.extra.generated);
schema.write()
})
.collect();
(None, table_stmts)
}
}
"sqlite" => {
#[cfg(not(feature = "sqlx-sqlite"))]
{
panic!("sqlite feature is off")
}
#[cfg(feature = "sqlx-sqlite")]
{
use sea_schema::sqlite::discovery::SchemaDiscovery;
use sqlx::Sqlite;
println!("Connecting to SQLite ...");
let connection = sqlx_connect::<Sqlite>(
max_connections,
acquire_timeout,
url.as_str(),
None,
)
.await?;
println!("Discovering schema ...");
let schema_discovery = SchemaDiscovery::new(connection);
let schema = schema_discovery
.discover()
.await?
.merge_indexes_into_table();
let table_stmts = schema
.tables
.into_iter()
.filter(|schema| filter_tables(&schema.name))
.filter(|schema| filter_hidden_tables(&schema.name))
.filter(|schema| filter_skip_tables(&schema.name))
.map(|mut schema| {
// Skip generated columns: codegen can't round-trip them, and
// emitting them as ordinary fields makes INSERT/UPDATE fail
// ("cannot INSERT/UPDATE a generated column"). See #3094.
schema
.columns
.retain(|col| !sqlite_column_is_generated(col));
schema.write()
})
.collect();
(None, table_stmts)
}
}
"postgres" | "postgresql" => {
#[cfg(not(feature = "sqlx-postgres"))]
{
panic!("postgres feature is off")
}
#[cfg(feature = "sqlx-postgres")]
{
use sea_schema::postgres::discovery::SchemaDiscovery;
use sqlx::Postgres;
println!("Connecting to Postgres ...");
let schema = database_schema.as_deref().unwrap_or("public");
let connection = sqlx_connect::<Postgres>(
max_connections,
acquire_timeout,
url.as_str(),
Some(schema),
)
.await?;
println!("Discovering schema ...");
let schema_discovery = SchemaDiscovery::new(connection, schema);
let schema = schema_discovery.discover().await?;
let table_stmts = schema
.tables
.into_iter()
.filter(|schema| filter_tables(&schema.info.name))
.filter(|schema| filter_hidden_tables(&schema.info.name))
.filter(|schema| filter_skip_tables(&schema.info.name))
.map(|mut schema| {
// Skip generated columns (see #3094).
schema.columns.retain(|col| col.generated.is_none());
schema.write()
})
.collect();
(database_schema, table_stmts)
}
}
_ => unimplemented!("{} is not supported", url.scheme()),
};
println!("... discovered.");
// Process extra derives and attributes, splitting by comma while respecting parentheses
// This handles cases like `--model-extra-attributes 'cfg_attr(debug_assertions, derive(Debug))'`
// which should be treated as a single attribute, not split into `cfg_attr(debug_assertions` and ` derive(Debug))`
let model_extra_derives = process_comma_separated_values(model_extra_derives);
let model_extra_attributes = process_comma_separated_values(model_extra_attributes);
let enum_extra_derives = process_comma_separated_values(enum_extra_derives);
let enum_extra_attributes = process_comma_separated_values(enum_extra_attributes);
let column_extra_derives = process_comma_separated_values(column_extra_derives);
let writer_context = EntityWriterContext::new(
if expanded_format {
EntityFormat::Expanded
} else if frontend_format {
EntityFormat::Frontend
} else if let Some(entity_format) = entity_format {
EntityFormat::from_str(&entity_format).expect("Invalid entity-format option")
} else {
EntityFormat::default()
},
WithPrelude::from_str(&with_prelude).expect("Invalid prelude option"),
WithSerde::from_str(&with_serde).expect("Invalid serde derive option"),
with_copy_enums,
date_time_crate.into(),
big_integer_type.into(),
schema_name,
lib,
serde_skip_deserializing_primary_key,
serde_skip_hidden_column,
model_extra_derives,
model_extra_attributes,
enum_extra_derives,
enum_extra_attributes,
column_extra_derives,
seaography,
impl_active_model_behavior,
banner_version.into(),
);
let entity_writer = EntityTransformer::transform(table_stmts)?;
let dir = Path::new(&output_dir);
fs::create_dir_all(dir)?;
if er_diagram {
let diagram = entity_writer.generate_er_diagram();
let diagram_path = dir.join("entities.mermaid");
fs::write(&diagram_path, &diagram)?;
println!("Writing {}", diagram_path.display());
}
let output = entity_writer.generate(&writer_context);
let mut merge_fallback_files: Vec<String> = Vec::new();
for OutputFile { name, content } in output.files.iter() {
let file_path = dir.join(name);
println!("Writing {}", file_path.display());
if !matches!(
name.as_str(),
"mod.rs" | "lib.rs" | "prelude.rs" | "sea_orm_active_enums.rs"
) && file_path.exists()
&& preserve_user_modifications
{
let prev_content = fs::read_to_string(&file_path)?;
match merge_entity_files(&prev_content, content) {
Ok(merged) => {
fs::write(file_path, merged)?;
}
Err(MergeReport {
output,
warnings,
fallback_applied,
}) => {
for message in warnings {
eprintln!("{message}");
}
fs::write(file_path, output)?;
if fallback_applied {
merge_fallback_files.push(name.clone());
}
}
}
} else {
fs::write(file_path, content)?;
};
}
// Format each of the files
for OutputFile { name, .. } in output.files.iter() {
let exit_status = Command::new("rustfmt").arg(dir.join(name)).status()?; // Get the status code
if !exit_status.success() {
// Propagate the error if any
return Err(format!("Fail to format file `{name}`").into());
}
}
if merge_fallback_files.is_empty() {
println!("... Done.");
} else {
return Err(format!(
"Merge fallback applied for {} file(s): \n{}",
merge_fallback_files.len(),
merge_fallback_files.join("\n")
)
.into());
}
}
}
Ok(())
}
async fn sqlx_connect<DB>(
max_connections: u32,
acquire_timeout: u64,
url: &str,
schema: Option<&str>,
) -> Result<sqlx::Pool<DB>, Box<dyn Error>>
where
DB: sqlx::Database,
for<'a> &'a mut <DB as sqlx::Database>::Connection: sqlx::Executor<'a>,
{
let mut pool_options = sqlx::pool::PoolOptions::<DB>::new()
.max_connections(max_connections)
.acquire_timeout(time::Duration::from_secs(acquire_timeout));
// Set search_path for Postgres, E.g. Some("public") by default
// MySQL & SQLite connection initialize with schema `None`
if let Some(schema) = schema {
let sql = format!("SET search_path = '{schema}'");
pool_options = pool_options.after_connect(move |conn, _| {
let sql = sql.clone();
Box::pin(async move {
sqlx::Executor::execute(conn, sqlx::AssertSqlSafe(sql))
.await
.map(|_| ())
})
});
}
pool_options.connect(url).await.map_err(Into::into)
}
impl From<DateTimeCrate> for CodegenDateTimeCrate {
fn from(date_time_crate: DateTimeCrate) -> CodegenDateTimeCrate {
match date_time_crate {
DateTimeCrate::Chrono => CodegenDateTimeCrate::Chrono,
DateTimeCrate::Time => CodegenDateTimeCrate::Time,
}
}
}
impl From<BigIntegerType> for CodegenBigIntegerType {
fn from(date_time_crate: BigIntegerType) -> CodegenBigIntegerType {
match date_time_crate {
BigIntegerType::I64 => CodegenBigIntegerType::I64,
BigIntegerType::I32 => CodegenBigIntegerType::I32,
}
}
}
impl From<BannerVersion> for CodegenBannerVersion {
fn from(banner_version: BannerVersion) -> CodegenBannerVersion {
match banner_version {
BannerVersion::Off => CodegenBannerVersion::Off,
BannerVersion::Major => CodegenBannerVersion::Major,
BannerVersion::Minor => CodegenBannerVersion::Minor,
BannerVersion::Patch => CodegenBannerVersion::Patch,
}
}
}
#[cfg(test)]
mod tests {
use clap::Parser;
use super::*;
use crate::{Cli, Commands};
#[test]
#[should_panic(
expected = "called `Result::unwrap()` on an `Err` value: RelativeUrlWithoutBase"
)]
fn test_generate_entity_no_protocol() {
let cli = Cli::parse_from([
"sea-orm-cli",
"generate",
"entity",
"--database-url",
"://root:root@localhost:3306/database",
]);
match cli.command {
Commands::Generate { command } => {
smol::block_on(run_generate_command(command, cli.verbose)).unwrap();
}
_ => unreachable!(),
}
}
#[test]
#[should_panic(
expected = "There is no database name as part of the url path: postgresql://root:root@localhost:3306"
)]
fn test_generate_entity_no_database_section() {
let cli = Cli::parse_from([
"sea-orm-cli",
"generate",
"entity",
"--database-url",
"postgresql://root:root@localhost:3306",
]);
match cli.command {
Commands::Generate { command } => {
smol::block_on(run_generate_command(command, cli.verbose)).unwrap();
}
_ => unreachable!(),
}
}
#[test]
#[should_panic(
expected = "There is no database name as part of the url path: mysql://root:root@localhost:3306/"
)]
fn test_generate_entity_no_database_path() {
let cli = Cli::parse_from([
"sea-orm-cli",
"generate",
"entity",
"--database-url",
"mysql://root:root@localhost:3306/",
]);
match cli.command {
Commands::Generate { command } => {
smol::block_on(run_generate_command(command, cli.verbose)).unwrap();
}
_ => unreachable!(),
}
}
#[test]
#[should_panic(expected = "called `Result::unwrap()` on an `Err` value: EmptyHost")]
fn test_generate_entity_no_host() {
let cli = Cli::parse_from([
"sea-orm-cli",
"generate",
"entity",
"--database-url",
"postgres://root:root@/database",
]);
match cli.command {
Commands::Generate { command } => {
smol::block_on(run_generate_command(command, cli.verbose)).unwrap();
}
_ => unreachable!(),
}
}
#[test]
fn test_split_by_comma_simple() {
// Simple comma-separated values should split normally
let result = super::split_by_comma_ignoring_parentheses("a,b,c");
assert_eq!(result, vec!["a", "b", "c"]);
}
#[test]
fn test_split_by_comma_with_parentheses() {
// Comma inside parentheses should NOT split
let result = super::split_by_comma_ignoring_parentheses("test(a, b)");
assert_eq!(result, vec!["test(a, b)"]);
// Multiple values, one with parentheses containing comma
let result = super::split_by_comma_ignoring_parentheses("attr1,test(a, b)");
assert_eq!(result, vec!["attr1", "test(a, b)"]);
}
#[test]
fn test_split_by_comma_with_nested_parentheses() {
// Nested parentheses with commas
let result =
super::split_by_comma_ignoring_parentheses("cfg_attr(debug_assertions, derive(Debug))");
assert_eq!(result, vec!["cfg_attr(debug_assertions, derive(Debug))"]);
// Multiple nested parentheses
let result = super::split_by_comma_ignoring_parentheses(
"cfg_attr(feature1, attr(a, b)),cfg_attr(feature2, attr(c, d))",
);
assert_eq!(
result,
vec![
"cfg_attr(feature1, attr(a, b))",
"cfg_attr(feature2, attr(c, d))"
]
);
}
#[test]
fn test_split_by_comma_with_brackets() {
// Brackets should also be respected
let result = super::split_by_comma_ignoring_parentheses(
"serde(rename_all = \"camelCase\"),ts(export)",
);
assert_eq!(
result,
vec!["serde(rename_all = \"camelCase\")", "ts(export)"]
);
// Brackets with commas
let result = super::split_by_comma_ignoring_parentheses("attr[key, value],other");
assert_eq!(result, vec!["attr[key, value]", "other"]);
}
#[test]
fn test_split_by_comma_with_braces() {
// Braces should also be respected
let result = super::split_by_comma_ignoring_parentheses("derive{a, b},other");
assert_eq!(result, vec!["derive{a, b}", "other"]);
}
#[test]
fn test_split_by_comma_empty() {
// Empty string should return empty vec
let result = super::split_by_comma_ignoring_parentheses("");
assert!(result.is_empty());
// Only whitespace should return empty vec
let result = super::split_by_comma_ignoring_parentheses(" ");
assert!(result.is_empty());
}
#[test]
fn test_split_by_comma_whitespace_handling() {
// Whitespace around values should be trimmed
let result = super::split_by_comma_ignoring_parentheses(" a , b ");
assert_eq!(result, vec!["a", "b"]);
// Whitespace inside parentheses should be preserved
let result = super::split_by_comma_ignoring_parentheses("test( a , b )");
assert_eq!(result, vec!["test( a , b )"]);
}
#[test]
fn test_process_comma_separated_values() {
// Process multiple strings, each potentially containing comma-separated values
let input = vec![
"attr1,attr2".to_string(),
"test(a, b)".to_string(),
"attr3".to_string(),
];
let result = super::process_comma_separated_values(input);
assert_eq!(result, vec!["attr1", "attr2", "test(a, b)", "attr3"]);
}
#[test]
fn test_split_by_comma_real_world_examples() {
// Real-world example: cfg_attr with derive
let result = super::split_by_comma_ignoring_parentheses(
"cfg_attr(debug_assertions, derive(Debug)),serde(rename_all = \"camelCase\")",
);
assert_eq!(
result,
vec![
"cfg_attr(debug_assertions, derive(Debug))",
"serde(rename_all = \"camelCase\")"
]
);
// Real-world example: multiple derives
let result = super::split_by_comma_ignoring_parentheses(
"derive(Debug, Clone),derive(Serialize, Deserialize)",
);
assert_eq!(
result,
vec!["derive(Debug, Clone)", "derive(Serialize, Deserialize)"]
);
}
// Regression test for #3094: generated columns must be dropped during
// `generate entity`, otherwise they are emitted as ordinary writable fields
// and every INSERT/UPDATE fails ("cannot INSERT/UPDATE a generated column").
#[cfg(feature = "sqlx-sqlite")]
#[test]
fn test_generate_entity_skips_sqlite_generated_columns() {
use sea_schema::sea_query::ColumnType;
use sea_schema::sqlite::def::{ColumnInfo, ColumnVisibility, DefaultType, TableDef};
let col = |cid, name: &str, hidden| ColumnInfo {
cid,
name: name.to_owned(),
r#type: ColumnType::Integer,
not_null: true,
default_value: DefaultType::Unspecified,
primary_key: cid == 0,
hidden,
};
let mut table = TableDef {
name: "widget".to_owned(),
foreign_keys: vec![],
indexes: vec![],
constraints: vec![],
columns: vec![
col(0, "id", ColumnVisibility::Visible),
col(1, "w", ColumnVisibility::Visible),
col(2, "area", ColumnVisibility::GeneratedVirtual),
col(3, "area_stored", ColumnVisibility::GeneratedStored),
],
auto_increment: false,
};
// The predicate flags only the generated columns.
assert!(!super::sqlite_column_is_generated(&table.columns[0]));
assert!(!super::sqlite_column_is_generated(&table.columns[1]));
assert!(super::sqlite_column_is_generated(&table.columns[2]));
assert!(super::sqlite_column_is_generated(&table.columns[3]));
// After filtering + write(), generated columns are absent from the DDL.
table
.columns
.retain(|col| !super::sqlite_column_is_generated(col));
let stmt = table.write();
let names: Vec<String> = stmt
.get_columns()
.iter()
.map(|c| c.get_column_name())
.collect();
assert_eq!(names, ["id", "w"]);
}
}
+379
View File
@@ -0,0 +1,379 @@
use chrono::{Local, Utc};
use regex::Regex;
use std::{
error::Error,
fmt::Display,
fs,
io::Write,
path::{Path, PathBuf},
process::Command,
};
#[cfg(feature = "cli")]
use crate::MigrateSubcommands;
#[cfg(feature = "cli")]
pub fn run_migrate_command(
command: Option<MigrateSubcommands>,
migration_dir: &str,
database_schema: Option<String>,
database_url: Option<String>,
verbose: bool,
) -> Result<(), Box<dyn Error>> {
match command {
Some(MigrateSubcommands::Init) => run_migrate_init(migration_dir)?,
Some(MigrateSubcommands::Generate {
migration_name,
universal_time: _,
local_time,
}) => run_migrate_generate(migration_dir, &migration_name, !local_time)?,
_ => {
let (subcommand, migration_dir, steps, verbose) = match command {
Some(MigrateSubcommands::Fresh) => ("fresh", migration_dir, None, verbose),
Some(MigrateSubcommands::Refresh) => ("refresh", migration_dir, None, verbose),
Some(MigrateSubcommands::Reset) => ("reset", migration_dir, None, verbose),
Some(MigrateSubcommands::Status) => ("status", migration_dir, None, verbose),
Some(MigrateSubcommands::Up { num }) => ("up", migration_dir, num, verbose),
Some(MigrateSubcommands::Down { num }) => {
("down", migration_dir, Some(num), verbose)
}
_ => ("up", migration_dir, None, verbose),
};
// Construct the `--manifest-path`
let manifest_path = if migration_dir.ends_with('/') {
format!("{migration_dir}Cargo.toml")
} else {
format!("{migration_dir}/Cargo.toml")
};
// Construct the arguments that will be supplied to `cargo` command
let mut args = vec!["run", "--manifest-path", &manifest_path, "--", subcommand];
let mut envs = vec![];
let mut num: String = "".to_string();
if let Some(steps) = steps {
num = steps.to_string();
}
if !num.is_empty() {
args.extend(["-n", &num])
}
if let Some(database_url) = &database_url {
envs.push(("DATABASE_URL", database_url));
}
if let Some(database_schema) = &database_schema {
envs.push(("DATABASE_SCHEMA", database_schema));
}
if verbose {
args.push("-v");
}
// Run migrator CLI on user's behalf
println!("Running `cargo {}`", args.join(" "));
let exit_status = Command::new("cargo").args(args).envs(envs).status()?; // Get the status code
if !exit_status.success() {
// Propagate the error if any
return Err("Fail to run migration".into());
}
}
}
Ok(())
}
pub fn run_migrate_init(migration_dir: &str) -> Result<(), Box<dyn Error>> {
let migration_dir = match migration_dir.ends_with('/') {
true => migration_dir.to_string(),
false => format!("{migration_dir}/"),
};
println!("Initializing migration directory...");
macro_rules! write_file {
($filename: literal) => {
let fn_content = |content: String| content;
write_file!($filename, $filename, fn_content);
};
($filename: literal, $template: literal) => {
let fn_content = |content: String| content;
write_file!($filename, $template, fn_content);
};
($filename: literal, $template: literal, $fn_content: expr) => {
let filepath = [&migration_dir, $filename].join("");
println!("Creating file `{}`", filepath);
let path = Path::new(&filepath);
let prefix = path.parent().unwrap();
fs::create_dir_all(prefix).unwrap();
let mut file = fs::File::create(path)?;
let content = include_str!(concat!("../../template/migration/", $template));
let content = $fn_content(content.to_string());
file.write_all(content.as_bytes())?;
};
}
write_file!("src/lib.rs");
write_file!("src/m20220101_000001_create_table.rs");
write_file!("src/main.rs");
write_file!("Cargo.toml", "_Cargo.toml", |content: String| {
let ver = format!(
"{}.{}.0",
env!("CARGO_PKG_VERSION_MAJOR"),
env!("CARGO_PKG_VERSION_MINOR")
);
content.replace("<sea-orm-migration-version>", &ver)
});
write_file!("README.md");
if glob::glob(&format!("{migration_dir}**/.git"))?.count() > 0 {
write_file!(".gitignore", "_gitignore");
}
println!("Done!");
Ok(())
}
pub fn run_migrate_generate(
migration_dir: &str,
migration_name: &str,
universal_time: bool,
) -> Result<(), Box<dyn Error>> {
// Make sure the migration name doesn't contain any characters that
// are invalid module names in Rust.
if migration_name.contains('-') {
return Err(Box::new(MigrationCommandError::InvalidName(
"Hyphen `-` cannot be used in migration name".to_string(),
)));
}
println!("Generating new migration...");
// build new migration filename
const FMT: &str = "%Y%m%d_%H%M%S";
let formatted_now = if universal_time {
Utc::now().format(FMT)
} else {
Local::now().format(FMT)
};
let migration_name = migration_name.trim().replace(' ', "_");
let migration_name = format!("m{formatted_now}_{migration_name}");
create_new_migration(&migration_name, migration_dir)?;
update_migrator(&migration_name, migration_dir)?;
Ok(())
}
/// `get_full_migration_dir` looks for a `src` directory
/// inside of `migration_dir` and appends that to the returned path if found.
///
/// Otherwise, `migration_dir` can point directly to a directory containing the
/// migrations. In that case, nothing is appended.
///
/// This way, `src` doesn't need to be appended in the standard case where
/// migrations are in their own crate. If the migrations are in a submodule
/// of another crate, `migration_dir` can point directly to that module.
fn get_full_migration_dir(migration_dir: &str) -> PathBuf {
let without_src = Path::new(migration_dir).to_owned();
let with_src = without_src.join("src");
match () {
_ if with_src.is_dir() => with_src,
_ => without_src,
}
}
fn create_new_migration(migration_name: &str, migration_dir: &str) -> Result<(), Box<dyn Error>> {
let migration_filepath =
get_full_migration_dir(migration_dir).join(format!("{}.rs", migration_name));
println!("Creating migration file `{}`", migration_filepath.display());
let migration_template = fmt_migration_template(migration_name);
let mut migration_file = fs::File::create(migration_filepath)?;
migration_file.write_all(migration_template.as_bytes())?;
Ok(())
}
fn fmt_migration_template(migration_name: &str) -> String {
format! {
r#"use sea_orm_migration::{{prelude::*, schema::*}};
pub struct Migration;
impl MigrationName for Migration {{
fn name(&self) -> &str {{
"{migration_name}"
}}
}}
#[async_trait::async_trait]
impl MigrationTrait for Migration {{
async fn up(&self, _manager: &SchemaManager) -> Result<(), DbErr> {{
// Replace the sample below with your own migration scripts
todo!();
}}
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {{
// Replace the sample below with your own migration scripts
todo!();
}}
}}
"#
}
}
/// `get_migrator_filepath` looks for a file `migration_dir/src/lib.rs`
/// and returns that path if found.
///
/// If `src` is not found, it will look directly in `migration_dir` for `lib.rs`.
///
/// If `lib.rs` is not found, it will look for `mod.rs` instead,
/// e.g. `migration_dir/mod.rs`.
///
/// This way, `src` doesn't need to be appended in the standard case where
/// migrations are in their own crate (with a file `lib.rs`). If the
/// migrations are in a submodule of another crate (with a file `mod.rs`),
/// `migration_dir` can point directly to that module.
fn get_migrator_filepath(migration_dir: &str) -> PathBuf {
let full_migration_dir = get_full_migration_dir(migration_dir);
let with_lib = full_migration_dir.join("lib.rs");
match () {
_ if with_lib.is_file() => with_lib,
_ => full_migration_dir.join("mod.rs"),
}
}
fn update_migrator(migration_name: &str, migration_dir: &str) -> Result<(), Box<dyn Error>> {
let migrator_filepath = get_migrator_filepath(migration_dir);
println!(
"Adding migration `{}` to `{}`",
migration_name,
migrator_filepath.display()
);
let migrator_content = fs::read_to_string(&migrator_filepath)?;
let mut updated_migrator_content = migrator_content.clone();
// create a backup of the migrator file in case something goes wrong
let migrator_backup_filepath = migrator_filepath.with_extension("rs.bak");
fs::copy(&migrator_filepath, &migrator_backup_filepath)?;
let mut migrator_file = fs::File::create(&migrator_filepath)?;
// find existing mod declarations, add new line
let mod_regex = Regex::new(r"mod\s+(?P<name>m\d{8}_\d{6}_\w+);")?;
let mods: Vec<_> = mod_regex.captures_iter(&migrator_content).collect();
let mods_end = if let Some(last_match) = mods.last() {
last_match.get(0).unwrap().end() + 1
} else {
migrator_content.len()
};
updated_migrator_content.insert_str(mods_end, format!("mod {migration_name};\n").as_str());
// build new vector from declared migration modules
let mut migrations: Vec<&str> = mods
.iter()
.map(|cap| cap.name("name").unwrap().as_str())
.collect();
migrations.push(migration_name);
let mut boxed_migrations = migrations
.iter()
.map(|migration| format!(" Box::new({migration}::Migration),"))
.collect::<Vec<String>>()
.join("\n");
boxed_migrations.push('\n');
let boxed_migrations = format!("vec![\n{boxed_migrations} ]\n");
let vec_regex = Regex::new(r"vec!\[[\s\S]+\]\n")?;
let updated_migrator_content = vec_regex.replace(&updated_migrator_content, &boxed_migrations);
migrator_file.write_all(updated_migrator_content.as_bytes())?;
fs::remove_file(&migrator_backup_filepath)?;
Ok(())
}
#[derive(Debug)]
enum MigrationCommandError {
InvalidName(String),
}
impl Display for MigrationCommandError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MigrationCommandError::InvalidName(name) => {
write!(f, "Invalid migration name: {name}")
}
}
}
}
impl Error for MigrationCommandError {}
#[cfg(test)]
mod tests {
use super::*;
const EXPECTED_TEMPLATE: &str = r#"use sea_orm_migration::{prelude::*, schema::*};
pub struct Migration;
impl MigrationName for Migration {
fn name(&self) -> &str {
"test_name"
}
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
// Replace the sample below with your own migration scripts
todo!();
}
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
// Replace the sample below with your own migration scripts
todo!();
}
}
"#;
#[test]
fn test_create_new_migration() {
let migration_name = "test_name";
let migration_dir = "/tmp/sea_orm_cli_test_new_migration/";
fs::create_dir_all(format!("{migration_dir}src")).unwrap();
create_new_migration(migration_name, migration_dir).unwrap();
let migration_filepath = Path::new(migration_dir)
.join("src")
.join(format!("{migration_name}.rs"));
assert!(migration_filepath.exists());
let migration_content = fs::read_to_string(migration_filepath).unwrap();
assert_eq!(&migration_content, EXPECTED_TEMPLATE);
fs::remove_dir_all("/tmp/sea_orm_cli_test_new_migration/").unwrap();
}
#[test]
fn test_update_migrator() {
let migration_name = "test_name";
let migration_dir = "/tmp/sea_orm_cli_test_update_migrator/";
fs::create_dir_all(format!("{migration_dir}src")).unwrap();
let migrator_filepath = Path::new(migration_dir).join("src").join("lib.rs");
fs::copy("./template/migration/src/lib.rs", &migrator_filepath).unwrap();
update_migrator(migration_name, migration_dir).unwrap();
assert!(&migrator_filepath.exists());
let migrator_content = fs::read_to_string(&migrator_filepath).unwrap();
let mod_regex = Regex::new(r"mod (?P<name>\w+);").unwrap();
let migrations: Vec<&str> = mod_regex
.captures_iter(&migrator_content)
.map(|cap| cap.name("name").unwrap().as_str())
.collect();
assert_eq!(migrations.len(), 2);
assert_eq!(
*migrations.first().unwrap(),
"m20220101_000001_create_table"
);
assert_eq!(migrations.last().unwrap(), &migration_name);
let boxed_regex = Regex::new(r"Box::new\((?P<name>\S+)::Migration\)").unwrap();
let migrations: Vec<&str> = boxed_regex
.captures_iter(&migrator_content)
.map(|cap| cap.name("name").unwrap().as_str())
.collect();
assert_eq!(migrations.len(), 2);
assert_eq!(
*migrations.first().unwrap(),
"m20220101_000001_create_table"
);
assert_eq!(migrations.last().unwrap(), &migration_name);
fs::remove_dir_all("/tmp/sea_orm_cli_test_update_migrator/").unwrap();
}
}
+17
View File
@@ -0,0 +1,17 @@
use std::fmt::Display;
#[cfg(feature = "codegen")]
pub mod generate;
pub mod migrate;
#[cfg(feature = "codegen")]
pub use generate::*;
pub use migrate::*;
pub fn handle_error<E>(error: E)
where
E: Display,
{
eprintln!("{error}");
::std::process::exit(1);
}
+7
View File
@@ -0,0 +1,7 @@
#[cfg(feature = "cli")]
pub mod cli;
pub mod commands;
#[cfg(feature = "cli")]
pub use cli::*;
pub use commands::*;