From cab4b5a3f7bd0e1bbf8f35d71aa3df32931e97f8 Mon Sep 17 00:00:00 2001 From: Billy Chan <30400950+billy1624@users.noreply.github.com> Date: Sat, 19 Jun 2021 19:47:59 +0800 Subject: [PATCH] Codegen: Entity Generator (#23) --- Cargo.toml | 4 +- examples/codegen/Cargo.toml | 14 + examples/codegen/README.md | 12 + examples/codegen/src/main.rs | 16 ++ examples/codegen/src/out/cake.rs | 89 +++++++ examples/codegen/src/out/cake_filling.rs | 90 +++++++ examples/codegen/src/out/filling.rs | 75 ++++++ examples/codegen/src/out/fruit.rs | 78 ++++++ examples/codegen/src/out/fruit_copy.rs | 63 +++++ examples/codegen/src/out/mod.rs | 7 + examples/codegen/src/out/prelude.rs | 7 + sea-orm-cli/Cargo.toml | 21 ++ sea-orm-cli/README.md | 13 + sea-orm-cli/src/cli.rs | 41 +++ sea-orm-cli/src/main.rs | 42 +++ sea-orm-codegen/Cargo.toml | 26 ++ sea-orm-codegen/README.md | 1 + sea-orm-codegen/src/entity/column.rs | 155 +++++++++++ sea-orm-codegen/src/entity/entity.rs | 121 +++++++++ sea-orm-codegen/src/entity/generator.rs | 15 ++ sea-orm-codegen/src/entity/mod.rs | 15 ++ sea-orm-codegen/src/entity/primary_key.rs | 18 ++ sea-orm-codegen/src/entity/relation.rs | 60 +++++ sea-orm-codegen/src/entity/transformer.rs | 86 +++++++ sea-orm-codegen/src/entity/writer.rs | 297 ++++++++++++++++++++++ sea-orm-codegen/src/error.rs | 40 +++ sea-orm-codegen/src/lib.rs | 5 + src/entity/relation.rs | 2 +- 28 files changed, 1411 insertions(+), 2 deletions(-) create mode 100644 examples/codegen/Cargo.toml create mode 100644 examples/codegen/README.md create mode 100644 examples/codegen/src/main.rs create mode 100644 examples/codegen/src/out/cake.rs create mode 100644 examples/codegen/src/out/cake_filling.rs create mode 100644 examples/codegen/src/out/filling.rs create mode 100644 examples/codegen/src/out/fruit.rs create mode 100644 examples/codegen/src/out/fruit_copy.rs create mode 100644 examples/codegen/src/out/mod.rs create mode 100644 examples/codegen/src/out/prelude.rs create mode 100644 sea-orm-cli/Cargo.toml create mode 100644 sea-orm-cli/README.md create mode 100644 sea-orm-cli/src/cli.rs create mode 100644 sea-orm-cli/src/main.rs create mode 100644 sea-orm-codegen/Cargo.toml create mode 100644 sea-orm-codegen/README.md create mode 100644 sea-orm-codegen/src/entity/column.rs create mode 100644 sea-orm-codegen/src/entity/entity.rs create mode 100644 sea-orm-codegen/src/entity/generator.rs create mode 100644 sea-orm-codegen/src/entity/mod.rs create mode 100644 sea-orm-codegen/src/entity/primary_key.rs create mode 100644 sea-orm-codegen/src/entity/relation.rs create mode 100644 sea-orm-codegen/src/entity/transformer.rs create mode 100644 sea-orm-codegen/src/entity/writer.rs create mode 100644 sea-orm-codegen/src/error.rs create mode 100644 sea-orm-codegen/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 0cc21a12..9e950495 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,10 @@ members = [ ".", "sea-orm-macros", + "sea-orm-codegen", + "sea-orm-cli", "examples/sqlx-mysql", + "examples/codegen", ] [package] @@ -28,7 +31,6 @@ futures = { version = "^0.3" } futures-util = { version = "^0.3" } sea-query = { path = "../sea-query", version = "^0.12" } sea-orm-macros = { path = "sea-orm-macros", optional = true } -# sea-schema = { path = "../sea-schema" } serde = { version = "^1.0", features = [ "derive" ] } sqlx = { version = "^0.5", optional = true } strum = { version = "^0.20", features = [ "derive" ] } diff --git a/examples/codegen/Cargo.toml b/examples/codegen/Cargo.toml new file mode 100644 index 00000000..2328ba1f --- /dev/null +++ b/examples/codegen/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "sea-orm-codegen-example" +version = "0.1.0" +edition = "2018" +publish = false + +[dependencies] +async-std = { version = "^1.9", features = [ "attributes" ] } +sea-orm = { path = "../../", features = [ "sqlx-mysql", "runtime-async-std-native-tls", "debug-print" ] } +sea-orm-codegen = { path = "../../sea-orm-codegen" } +sea-query = { path = "../../../sea-query" } +strum = { version = "^0.20", features = [ "derive" ] } +serde_json = { version = "^1" } +quote = "1" diff --git a/examples/codegen/README.md b/examples/codegen/README.md new file mode 100644 index 00000000..24bcd77e --- /dev/null +++ b/examples/codegen/README.md @@ -0,0 +1,12 @@ +# SeaORM Entity Generator Example + +Prepare: + +Setup a test database and configure the connection string in `main.rs`. +Run `bakery.sql` to setup the test table and data. + +Running: + +```sh +cargo run +``` diff --git a/examples/codegen/src/main.rs b/examples/codegen/src/main.rs new file mode 100644 index 00000000..dcc034b2 --- /dev/null +++ b/examples/codegen/src/main.rs @@ -0,0 +1,16 @@ +mod out; + +use sea_orm_codegen::{EntityGenerator, Error}; + +#[async_std::main] +async fn main() -> Result<(), Error> { + let uri = "mysql://sea:sea@localhost/bakery"; + let schema = "bakery"; + + let _generator = EntityGenerator::discover(uri, schema) + .await? + .transform()? + .generate("src/out")?; + + Ok(()) +} diff --git a/examples/codegen/src/out/cake.rs b/examples/codegen/src/out/cake.rs new file mode 100644 index 00000000..436753bd --- /dev/null +++ b/examples/codegen/src/out/cake.rs @@ -0,0 +1,89 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[derive(Copy, Clone, Default, Debug, DeriveEntity)] +pub struct Entity; + +impl EntityName for Entity { + fn table_name(&self) -> &str { + "cake" + } +} + +#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)] +pub struct Model { + pub id: i32, + pub name: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +pub enum Column { + Id, + Name, +} + +#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] +pub enum PrimaryKey { + Id, +} + +impl PrimaryKeyTrait for PrimaryKey { + fn auto_increment() -> bool { + true + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + CakeFilling, + Fruit, +} + +impl ColumnTrait for Column { + type EntityName = Entity; + fn def(&self) -> ColumnType { + match self { + Self::Id => ColumnType::Integer(Some(11u32)), + Self::Name => ColumnType::String(Some(255u32)), + } + } +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::CakeFilling => Entity::has_many(super::cake_filling::Entity) + .from(Column::Id) + .to(super::cake_filling::Column::CakeId) + .into(), + Self::Fruit => Entity::has_many(super::fruit::Entity) + .from(Column::Id) + .to(super::fruit::Column::CakeId) + .into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::CakeFilling.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Fruit.def() + } +} + +impl Model { + pub fn find_cake_filling(&self) -> Select { + Entity::find_related().belongs_to::(self) + } + pub fn find_fruit(&self) -> Select { + Entity::find_related().belongs_to::(self) + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/codegen/src/out/cake_filling.rs b/examples/codegen/src/out/cake_filling.rs new file mode 100644 index 00000000..7febba4e --- /dev/null +++ b/examples/codegen/src/out/cake_filling.rs @@ -0,0 +1,90 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[derive(Copy, Clone, Default, Debug, DeriveEntity)] +pub struct Entity; + +impl EntityName for Entity { + fn table_name(&self) -> &str { + "cake_filling" + } +} + +#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)] +pub struct Model { + pub cake_id: i32, + pub filling_id: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +pub enum Column { + CakeId, + FillingId, +} + +#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] +pub enum PrimaryKey { + CakeId, + FillingId, +} + +impl PrimaryKeyTrait for PrimaryKey { + fn auto_increment() -> bool { + false + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Cake, + Filling, +} + +impl ColumnTrait for Column { + type EntityName = Entity; + fn def(&self) -> ColumnType { + match self { + Self::CakeId => ColumnType::Integer(Some(11u32)), + Self::FillingId => ColumnType::Integer(Some(11u32)), + } + } +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Cake => Entity::has_one(super::cake::Entity) + .from(Column::CakeId) + .to(super::cake::Column::Id) + .into(), + Self::Filling => Entity::has_one(super::filling::Entity) + .from(Column::FillingId) + .to(super::filling::Column::Id) + .into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Cake.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Filling.def() + } +} + +impl Model { + pub fn find_cake(&self) -> Select { + Entity::find_related().belongs_to::(self) + } + pub fn find_filling(&self) -> Select { + Entity::find_related().belongs_to::(self) + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/codegen/src/out/filling.rs b/examples/codegen/src/out/filling.rs new file mode 100644 index 00000000..7e5a5bd8 --- /dev/null +++ b/examples/codegen/src/out/filling.rs @@ -0,0 +1,75 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[derive(Copy, Clone, Default, Debug, DeriveEntity)] +pub struct Entity; + +impl EntityName for Entity { + fn table_name(&self) -> &str { + "filling" + } +} + +#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)] +pub struct Model { + pub id: i32, + pub name: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +pub enum Column { + Id, + Name, +} + +#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] +pub enum PrimaryKey { + Id, +} + +impl PrimaryKeyTrait for PrimaryKey { + fn auto_increment() -> bool { + true + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + CakeFilling, +} + +impl ColumnTrait for Column { + type EntityName = Entity; + fn def(&self) -> ColumnType { + match self { + Self::Id => ColumnType::Integer(Some(11u32)), + Self::Name => ColumnType::String(Some(255u32)), + } + } +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::CakeFilling => Entity::has_many(super::cake_filling::Entity) + .from(Column::Id) + .to(super::cake_filling::Column::FillingId) + .into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::CakeFilling.def() + } +} + +impl Model { + pub fn find_cake_filling(&self) -> Select { + Entity::find_related().belongs_to::(self) + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/codegen/src/out/fruit.rs b/examples/codegen/src/out/fruit.rs new file mode 100644 index 00000000..c2d2a7b5 --- /dev/null +++ b/examples/codegen/src/out/fruit.rs @@ -0,0 +1,78 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[derive(Copy, Clone, Default, Debug, DeriveEntity)] +pub struct Entity; + +impl EntityName for Entity { + fn table_name(&self) -> &str { + "fruit" + } +} + +#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)] +pub struct Model { + pub id: i32, + pub name: Option, + pub cake_id: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +pub enum Column { + Id, + Name, + CakeId, +} + +#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] +pub enum PrimaryKey { + Id, +} + +impl PrimaryKeyTrait for PrimaryKey { + fn auto_increment() -> bool { + true + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Cake, +} + +impl ColumnTrait for Column { + type EntityName = Entity; + fn def(&self) -> ColumnType { + match self { + Self::Id => ColumnType::Integer(Some(11u32)), + Self::Name => ColumnType::String(Some(255u32)), + Self::CakeId => ColumnType::Integer(Some(11u32)), + } + } +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Cake => Entity::has_one(super::cake::Entity) + .from(Column::CakeId) + .to(super::cake::Column::Id) + .into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Cake.def() + } +} + +impl Model { + pub fn find_cake(&self) -> Select { + Entity::find_related().belongs_to::(self) + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/codegen/src/out/fruit_copy.rs b/examples/codegen/src/out/fruit_copy.rs new file mode 100644 index 00000000..e3cdc525 --- /dev/null +++ b/examples/codegen/src/out/fruit_copy.rs @@ -0,0 +1,63 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[derive(Copy, Clone, Default, Debug, DeriveEntity)] +pub struct Entity; + +impl EntityName for Entity { + fn table_name(&self) -> &str { + "fruit_copy" + } +} + +#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)] +pub struct Model { + pub id: i32, + pub name: Option, + pub cake_id: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +pub enum Column { + Id, + Name, + CakeId, +} + +#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] +pub enum PrimaryKey { + Id, +} + +impl PrimaryKeyTrait for PrimaryKey { + fn auto_increment() -> bool { + true + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation {} + +impl ColumnTrait for Column { + type EntityName = Entity; + fn def(&self) -> ColumnType { + match self { + Self::Id => ColumnType::Integer(Some(11u32)), + Self::Name => ColumnType::String(Some(255u32)), + Self::CakeId => ColumnType::Integer(Some(11u32)), + } + } +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + _ => panic!("No RelationDef"), + } + } +} + +impl Model {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/codegen/src/out/mod.rs b/examples/codegen/src/out/mod.rs new file mode 100644 index 00000000..2644c7e7 --- /dev/null +++ b/examples/codegen/src/out/mod.rs @@ -0,0 +1,7 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +pub mod cake; +pub mod cake_filling; +pub mod filling; +pub mod fruit; +pub mod fruit_copy; diff --git a/examples/codegen/src/out/prelude.rs b/examples/codegen/src/out/prelude.rs new file mode 100644 index 00000000..a4d3cd7e --- /dev/null +++ b/examples/codegen/src/out/prelude.rs @@ -0,0 +1,7 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +pub use super::cake::Entity as Cake; +pub use super::cake_filling::Entity as CakeFilling; +pub use super::filling::Entity as Filling; +pub use super::fruit::Entity as Fruit; +pub use super::fruit_copy::Entity as FruitCopy; diff --git a/sea-orm-cli/Cargo.toml b/sea-orm-cli/Cargo.toml new file mode 100644 index 00000000..88a69588 --- /dev/null +++ b/sea-orm-cli/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "sea-orm-cli" +version = "0.1.0" +authors = [ "Billy Chan " ] +edition = "2018" +description = "" +license = "MIT OR Apache-2.0" +documentation = "https://docs.rs/sea-orm" +repository = "https://github.com/SeaQL/sea-orm" +categories = [ "database" ] +keywords = [ "orm", "database", "sql", "mysql", "postgres", "sqlite", "cli" ] +publish = false + +[[bin]] +name = "sea-orm" +path = "src/main.rs" + +[dependencies] +clap = { version = "^2.33.3" } +async-std = { version = "^1.9", features = [ "attributes" ] } +sea-orm-codegen = { path = "../sea-orm-codegen" } diff --git a/sea-orm-cli/README.md b/sea-orm-cli/README.md new file mode 100644 index 00000000..3d2acd8c --- /dev/null +++ b/sea-orm-cli/README.md @@ -0,0 +1,13 @@ +# SeaORM CLI + +Getting Help: + +```sh +cargo run -- -h +``` + +Running Entity Generator: + +```sh +cargo run -- entity generate -u mysql://sea:sea@localhost/bakery -s bakery -o out +``` diff --git a/sea-orm-cli/src/cli.rs b/sea-orm-cli/src/cli.rs new file mode 100644 index 00000000..7bedbb90 --- /dev/null +++ b/sea-orm-cli/src/cli.rs @@ -0,0 +1,41 @@ +use clap::{App, AppSettings, Arg, SubCommand}; + +pub fn build_cli() -> App<'static, 'static> { + let entity_subcommand = SubCommand::with_name("entity") + .about("Entity related commands") + .setting(AppSettings::VersionlessSubcommands) + .subcommand( + SubCommand::with_name("generate") + .about("Generate entity") + .arg( + Arg::with_name("DATABASE_URI") + .long("uri") + .short("u") + .help("Database URI") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("DATABASE_SCHEMA") + .long("schema") + .short("s") + .help("Database schema") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("OUTPUT_DIR") + .long("output_dir") + .short("o") + .help("Entity file output directory") + .takes_value(true) + .default_value("./"), + ), + ) + .setting(AppSettings::SubcommandRequiredElseHelp); + + App::new("sea-orm") + .version(env!("CARGO_PKG_VERSION")) + .setting(AppSettings::VersionlessSubcommands) + .subcommand(entity_subcommand) +} diff --git a/sea-orm-cli/src/main.rs b/sea-orm-cli/src/main.rs new file mode 100644 index 00000000..ad0451ad --- /dev/null +++ b/sea-orm-cli/src/main.rs @@ -0,0 +1,42 @@ +use clap::ArgMatches; +use sea_orm_codegen::EntityGenerator; +use std::{error::Error, fmt::Display}; + +mod cli; + +#[async_std::main] +async fn main() { + let matches = cli::build_cli().get_matches(); + + match matches.subcommand() { + ("entity", Some(matches)) => run_entity_command(matches) + .await + .unwrap_or_else(handle_error), + _ => unreachable!("You should never see this message"), + } +} + +async fn run_entity_command(matches: &ArgMatches<'_>) -> Result<(), Box> { + match matches.subcommand() { + ("generate", Some(args)) => { + let uri = args.value_of("DATABASE_URI").unwrap(); + let schema = args.value_of("DATABASE_SCHEMA").unwrap(); + let output_dir = args.value_of("OUTPUT_DIR").unwrap(); + EntityGenerator::discover(uri, schema) + .await? + .transform()? + .generate(output_dir)?; + } + _ => unreachable!("You should never see this message"), + }; + + Ok(()) +} + +fn handle_error(error: E) +where + E: Display, +{ + eprintln!("{}", error); + ::std::process::exit(1); +} diff --git a/sea-orm-codegen/Cargo.toml b/sea-orm-codegen/Cargo.toml new file mode 100644 index 00000000..f3285484 --- /dev/null +++ b/sea-orm-codegen/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "sea-orm-codegen" +version = "0.1.0" +authors = [ "Billy Chan " ] +edition = "2018" +description = "" +license = "MIT OR Apache-2.0" +documentation = "https://docs.rs/sea-orm" +repository = "https://github.com/SeaQL/sea-orm" +categories = [ "database" ] +keywords = [ "orm", "database", "sql", "mysql", "postgres", "sqlite" ] +publish = false + +[lib] +name = "sea_orm_codegen" +path = "src/lib.rs" + +[dependencies] +sea-orm = { path = "../", features = [ "sqlx-mysql", "runtime-async-std-native-tls", "debug-print", "with-json", "macros" ], default-features = false } +sea-schema = { path = "../../sea-schema", default-features = false, features = [ "sqlx-mysql", "runtime-async-std-native-tls", "discovery", "writer" ] } +sea-query = { path = "../../sea-query", version = "^0.12" } +sqlx = { version = "^0.5", features = [ "mysql", "runtime-async-std-native-tls" ] } +syn = { version = "1", default-features = false, features = [ "derive", "parsing", "proc-macro", "printing" ] } +quote = "1" +heck = "0.3" +proc-macro2 = "1" diff --git a/sea-orm-codegen/README.md b/sea-orm-codegen/README.md new file mode 100644 index 00000000..ad80f596 --- /dev/null +++ b/sea-orm-codegen/README.md @@ -0,0 +1 @@ +# SeaORM Codegen diff --git a/sea-orm-codegen/src/entity/column.rs b/sea-orm-codegen/src/entity/column.rs new file mode 100644 index 00000000..e5db112e --- /dev/null +++ b/sea-orm-codegen/src/entity/column.rs @@ -0,0 +1,155 @@ +use heck::{CamelCase, SnakeCase}; +use proc_macro2::{Ident, TokenStream}; +use quote::{format_ident, quote}; +use sea_query::{ColumnDef, ColumnSpec, ColumnType}; + +#[derive(Clone, Debug)] +pub struct Column { + pub(crate) name: String, + pub(crate) col_type: ColumnType, + pub(crate) auto_increment: bool, + pub(crate) not_null: bool, +} + +impl Column { + pub fn get_name_snake_case(&self) -> Ident { + format_ident!("{}", self.name.to_snake_case()) + } + + pub fn get_name_camel_case(&self) -> Ident { + format_ident!("{}", self.name.to_camel_case()) + } + + pub fn get_rs_type(&self) -> TokenStream { + let ident = match self.col_type { + ColumnType::Char(_) + | ColumnType::String(_) + | ColumnType::Text + | ColumnType::DateTime(_) + | ColumnType::Timestamp(_) + | ColumnType::Time(_) + | ColumnType::Date + | ColumnType::Json + | ColumnType::JsonBinary + | ColumnType::Custom(_) => format_ident!("String"), + ColumnType::TinyInteger(_) => format_ident!("i8"), + ColumnType::SmallInteger(_) => format_ident!("i16"), + ColumnType::Integer(_) => format_ident!("i32"), + ColumnType::BigInteger(_) => format_ident!("i64"), + ColumnType::Float(_) + | ColumnType::Decimal(_) + | ColumnType::Money(_) => format_ident!("f32"), + ColumnType::Double(_) => format_ident!("f64"), + ColumnType::Binary(_) => format_ident!("Vec"), + ColumnType::Boolean => format_ident!("bool"), + }; + match self.not_null { + true => quote! { #ident }, + false => quote! { Option<#ident> }, + } + } + + pub fn get_type(&self) -> TokenStream { + match &self.col_type { + ColumnType::Char(s) => match s { + Some(s) => quote! { ColumnType::Char(Some(#s)) }, + None => quote! { ColumnType::Char(None) }, + }, + ColumnType::String(s) => match s { + Some(s) => quote! { ColumnType::String(Some(#s)) }, + None => quote! { ColumnType::String(None) }, + }, + ColumnType::Text => quote! { ColumnType::Text }, + ColumnType::TinyInteger(s) => match s { + Some(s) => quote! { ColumnType::TinyInteger(Some(#s)) }, + None => quote! { ColumnType::TinyInteger(None) }, + }, + ColumnType::SmallInteger(s) => match s { + Some(s) => quote! { ColumnType::SmallInteger(Some(#s)) }, + None => quote! { ColumnType::SmallInteger(None) }, + }, + ColumnType::Integer(s) => match s { + Some(s) => quote! { ColumnType::Integer(Some(#s)) }, + None => quote! { ColumnType::Integer(None) }, + }, + ColumnType::BigInteger(s) => match s { + Some(s) => quote! { ColumnType::BigInteger(Some(#s)) }, + None => quote! { ColumnType::BigInteger(None) }, + }, + ColumnType::Float(s) => match s { + Some(s) => quote! { ColumnType::Float(Some(#s)) }, + None => quote! { ColumnType::Float(None) }, + }, + ColumnType::Double(s) => match s { + Some(s) => quote! { ColumnType::Double(Some(#s)) }, + None => quote! { ColumnType::Double(None) }, + }, + ColumnType::Decimal(s) => match s { + Some((s1, s2)) => quote! { ColumnType::Decimal(Some((#s1, #s2))) }, + None => quote! { ColumnType::Decimal(None) }, + }, + ColumnType::DateTime(s) => match s { + Some(s) => quote! { ColumnType::DateTime(Some(#s)) }, + None => quote! { ColumnType::DateTime(None) }, + }, + ColumnType::Timestamp(s) => match s { + Some(s) => quote! { ColumnType::Timestamp(Some(#s)) }, + None => quote! { ColumnType::Timestamp(None) }, + }, + ColumnType::Time(s) => match s { + Some(s) => quote! { ColumnType::Time(Some(#s)) }, + None => quote! { ColumnType::Time(None) }, + }, + ColumnType::Date => quote! { ColumnType::Date }, + ColumnType::Binary(s) => match s { + Some(s) => quote! { ColumnType::Binary(Some(#s)) }, + None => quote! { ColumnType::Binary(None) }, + }, + ColumnType::Boolean => quote! { ColumnType::Boolean }, + ColumnType::Money(s) => match s { + Some((s1, s2)) => quote! { ColumnType::Money(Some((#s1, #s2))) }, + None => quote! { ColumnType::Money(None) }, + }, + ColumnType::Json => quote! { ColumnType::Json }, + ColumnType::JsonBinary => quote! { ColumnType::JsonBinary }, + ColumnType::Custom(s) => { + let s = s.to_string(); + quote! { ColumnType::Custom(sea_query::SeaRc::new(sea_query::Alias::new(#s))) } + } + } + } +} + +impl From<&ColumnDef> for Column { + fn from(col_def: &ColumnDef) -> Self { + let name = col_def.get_column_name(); + let col_type = match col_def.get_column_type() { + Some(ty) => ty.clone(), + None => panic!("ColumnType should not be empty"), + }; + let auto_increments: Vec = col_def + .get_column_spec() + .iter() + .filter_map(|spec| match spec { + ColumnSpec::AutoIncrement => Some(true), + _ => None, + }) + .collect(); + let auto_increment = !auto_increments.is_empty(); + let not_nulls: Vec = col_def + .get_column_spec() + .iter() + .filter_map(|spec| match spec { + ColumnSpec::NotNull => Some(true), + _ => None, + }) + .collect(); + let not_null = !not_nulls.is_empty(); + Self { + name, + col_type, + auto_increment, + not_null, + } + } +} diff --git a/sea-orm-codegen/src/entity/entity.rs b/sea-orm-codegen/src/entity/entity.rs new file mode 100644 index 00000000..98d7047e --- /dev/null +++ b/sea-orm-codegen/src/entity/entity.rs @@ -0,0 +1,121 @@ +use crate::{Column, PrimaryKey, Relation}; +use heck::{CamelCase, SnakeCase}; +use proc_macro2::{Ident, TokenStream}; +use quote::format_ident; + +#[derive(Clone, Debug)] +pub struct Entity { + pub(crate) table_name: String, + pub(crate) columns: Vec, + pub(crate) relations: Vec, + pub(crate) primary_keys: Vec, +} + +impl Entity { + pub fn get_table_name_snake_case(&self) -> String { + self.table_name.to_snake_case() + } + + pub fn get_table_name_camel_case(&self) -> String { + self.table_name.to_camel_case() + } + + pub fn get_table_name_snake_case_ident(&self) -> Ident { + format_ident!("{}", self.get_table_name_snake_case()) + } + + pub fn get_table_name_camel_case_ident(&self) -> Ident { + format_ident!("{}", self.get_table_name_camel_case()) + } + + pub fn get_column_names_snake_case(&self) -> Vec { + self.columns + .iter() + .map(|col| col.get_name_snake_case()) + .collect() + } + + pub fn get_column_names_camel_case(&self) -> Vec { + self.columns + .iter() + .map(|col| col.get_name_camel_case()) + .collect() + } + + pub fn get_column_rs_types(&self) -> Vec { + self.columns + .clone() + .into_iter() + .map(|col| col.get_rs_type()) + .collect() + } + + pub fn get_column_types(&self) -> Vec { + self.columns + .clone() + .into_iter() + .map(|col| col.get_type()) + .collect() + } + + pub fn get_primary_key_names_snake_case(&self) -> Vec { + self.primary_keys + .iter() + .map(|pk| pk.get_name_snake_case()) + .collect() + } + + pub fn get_primary_key_names_camel_case(&self) -> Vec { + self.primary_keys + .iter() + .map(|pk| pk.get_name_camel_case()) + .collect() + } + + pub fn get_relation_ref_tables_snake_case(&self) -> Vec { + self.relations + .iter() + .map(|rel| rel.get_ref_table_snake_case()) + .collect() + } + + pub fn get_relation_ref_tables_camel_case(&self) -> Vec { + self.relations + .iter() + .map(|rel| rel.get_ref_table_camel_case()) + .collect() + } + + pub fn get_relation_rel_types(&self) -> Vec { + self.relations + .iter() + .map(|rel| rel.get_rel_type()) + .collect() + } + + pub fn get_relation_columns_camel_case(&self) -> Vec { + self.relations + .iter() + .map(|rel| rel.get_column_camel_case()) + .collect() + } + + pub fn get_relation_ref_columns_camel_case(&self) -> Vec { + self.relations + .iter() + .map(|rel| rel.get_ref_column_camel_case()) + .collect() + } + + pub fn get_relation_rel_find_helpers(&self) -> Vec { + self.relations + .iter() + .map(|rel| rel.get_rel_find_helper()) + .collect() + } + + pub fn get_primary_key_auto_increment(&self) -> Ident { + let auto_increment = self.columns.iter().any(|col| col.auto_increment); + format_ident!("{}", auto_increment) + } +} diff --git a/sea-orm-codegen/src/entity/generator.rs b/sea-orm-codegen/src/entity/generator.rs new file mode 100644 index 00000000..92857d09 --- /dev/null +++ b/sea-orm-codegen/src/entity/generator.rs @@ -0,0 +1,15 @@ +use crate::{EntityTransformer, Error}; +use sea_schema::mysql::discovery::SchemaDiscovery; +use sqlx::MySqlPool; + +#[derive(Clone, Debug)] +pub struct EntityGenerator {} + +impl EntityGenerator { + pub async fn discover(uri: &str, schema: &str) -> Result { + let connection = MySqlPool::connect(uri).await?; + let schema_discovery = SchemaDiscovery::new(connection, schema); + let schema = schema_discovery.discover().await; + Ok(EntityTransformer { schema }) + } +} diff --git a/sea-orm-codegen/src/entity/mod.rs b/sea-orm-codegen/src/entity/mod.rs new file mode 100644 index 00000000..5badda4f --- /dev/null +++ b/sea-orm-codegen/src/entity/mod.rs @@ -0,0 +1,15 @@ +mod column; +mod entity; +mod generator; +mod primary_key; +mod relation; +mod transformer; +mod writer; + +pub use column::*; +pub use entity::*; +pub use generator::*; +pub use primary_key::*; +pub use relation::*; +pub use transformer::*; +pub use writer::*; diff --git a/sea-orm-codegen/src/entity/primary_key.rs b/sea-orm-codegen/src/entity/primary_key.rs new file mode 100644 index 00000000..5efbc038 --- /dev/null +++ b/sea-orm-codegen/src/entity/primary_key.rs @@ -0,0 +1,18 @@ +use heck::{CamelCase, SnakeCase}; +use proc_macro2::Ident; +use quote::format_ident; + +#[derive(Clone, Debug)] +pub struct PrimaryKey { + pub(crate) name: String, +} + +impl PrimaryKey { + pub fn get_name_snake_case(&self) -> Ident { + format_ident!("{}", self.name.to_snake_case()) + } + + pub fn get_name_camel_case(&self) -> Ident { + format_ident!("{}", self.name.to_camel_case()) + } +} diff --git a/sea-orm-codegen/src/entity/relation.rs b/sea-orm-codegen/src/entity/relation.rs new file mode 100644 index 00000000..bbcfd155 --- /dev/null +++ b/sea-orm-codegen/src/entity/relation.rs @@ -0,0 +1,60 @@ +use heck::{CamelCase, SnakeCase}; +use proc_macro2::Ident; +use quote::format_ident; +use sea_orm::RelationType; +use sea_query::TableForeignKey; + +#[derive(Clone, Debug)] +pub struct Relation { + pub(crate) ref_table: String, + pub(crate) columns: Vec, + pub(crate) ref_columns: Vec, + pub(crate) rel_type: RelationType, +} + +impl Relation { + pub fn get_ref_table_snake_case(&self) -> Ident { + format_ident!("{}", self.ref_table.to_snake_case()) + } + + pub fn get_ref_table_camel_case(&self) -> Ident { + format_ident!("{}", self.ref_table.to_camel_case()) + } + + pub fn get_rel_type(&self) -> Ident { + match self.rel_type { + RelationType::HasOne => format_ident!("has_one"), + RelationType::HasMany => format_ident!("has_many"), + } + } + + pub fn get_column_camel_case(&self) -> Ident { + format_ident!("{}", self.columns[0].to_camel_case()) + } + + pub fn get_ref_column_camel_case(&self) -> Ident { + format_ident!("{}", self.ref_columns[0].to_camel_case()) + } + + pub fn get_rel_find_helper(&self) -> Ident { + format_ident!("find_{}", self.ref_table.to_snake_case()) + } +} + +impl From<&TableForeignKey> for Relation { + fn from(tbl_fk: &TableForeignKey) -> Self { + let ref_table = match tbl_fk.get_ref_table() { + Some(s) => s, + None => panic!("RefTable should not be empty"), + }; + let columns = tbl_fk.get_columns(); + let ref_columns = tbl_fk.get_ref_columns(); + let rel_type = RelationType::HasOne; + Self { + ref_table, + columns, + ref_columns, + rel_type, + } + } +} diff --git a/sea-orm-codegen/src/entity/transformer.rs b/sea-orm-codegen/src/entity/transformer.rs new file mode 100644 index 00000000..d437d0f3 --- /dev/null +++ b/sea-orm-codegen/src/entity/transformer.rs @@ -0,0 +1,86 @@ +use crate::{Entity, EntityWriter, Error, PrimaryKey, Relation}; +use sea_orm::RelationType; +use sea_query::TableStatement; +use sea_schema::mysql::def::Schema; +use std::{collections::HashMap, mem::swap}; + +#[derive(Clone, Debug)] +pub struct EntityTransformer { + pub(crate) schema: Schema, +} + +impl EntityTransformer { + pub fn transform(self) -> Result { + let mut inverse_relations: HashMap> = HashMap::new(); + let mut entities = Vec::new(); + for table_ref in self.schema.tables.iter() { + let table_stmt = table_ref.write(); + let table_create = match table_stmt { + TableStatement::Create(stmt) => stmt, + _ => { + return Err(Error::TransformError( + "TableStatement should be create".into(), + )) + } + }; + let table_name = match table_create.get_table_name() { + Some(s) => s, + None => { + return Err(Error::TransformError( + "Table name should not be empty".into(), + )) + } + }; + let columns = table_create + .get_columns() + .iter() + .map(|col_def| col_def.into()) + .collect(); + let relations = table_create + .get_foreign_key_create_stmts() + .iter() + .map(|fk_create_stmt| fk_create_stmt.get_foreign_key()) + .map(|tbl_fk| tbl_fk.into()); + let primary_keys = table_create + .get_indexes() + .iter() + .filter(|index| index.is_primary_key()) + .map(|index| { + index + .get_index_spec() + .get_column_names() + .into_iter() + .map(|name| PrimaryKey { name }) + .collect::>() + }) + .flatten() + .collect(); + let entity = Entity { + table_name: table_name.clone(), + columns, + relations: relations.clone().collect(), + primary_keys, + }; + entities.push(entity); + for mut rel in relations.into_iter() { + let ref_table = rel.ref_table; + swap(&mut rel.columns, &mut rel.ref_columns); + rel.rel_type = RelationType::HasMany; + rel.ref_table = table_name.clone(); + if let Some(vec) = inverse_relations.get_mut(&ref_table) { + vec.push(rel); + } else { + inverse_relations.insert(ref_table, vec![rel]); + } + } + } + for (tbl_name, relations) in inverse_relations.iter() { + for ent in entities.iter_mut() { + if ent.table_name.eq(tbl_name) { + ent.relations.append(relations.clone().as_mut()); + } + } + } + Ok(EntityWriter { entities }) + } +} diff --git a/sea-orm-codegen/src/entity/writer.rs b/sea-orm-codegen/src/entity/writer.rs new file mode 100644 index 00000000..5b200026 --- /dev/null +++ b/sea-orm-codegen/src/entity/writer.rs @@ -0,0 +1,297 @@ +use crate::{Entity, Error}; +use proc_macro2::TokenStream; +use quote::quote; +use std::{ + fs::{self, File}, + io::{self, Write}, + path::Path, + process::Command, +}; + +#[derive(Clone, Debug)] +pub struct EntityWriter { + pub(crate) entities: Vec, +} + +impl EntityWriter { + pub fn generate(self, output_dir: &str) -> Result<(), Error> { + for entity in self.entities.iter() { + let code_blocks = Self::gen_code_blocks(entity); + Self::write(output_dir, entity, code_blocks)?; + } + for entity in self.entities.iter() { + Self::format_entity(output_dir, entity)?; + } + self.write_mod(output_dir)?; + self.write_prelude(output_dir)?; + Ok(()) + } + + pub fn write_mod(&self, output_dir: &str) -> io::Result<()> { + let file_name = "mod.rs"; + let dir = Self::create_dir(output_dir)?; + let file_path = dir.join(file_name); + let mut file = fs::File::create(file_path)?; + Self::write_doc_comment(&mut file)?; + for entity in self.entities.iter() { + let code_block = Self::gen_mod(entity); + file.write_all(code_block.to_string().as_bytes())?; + } + Self::format_file(output_dir, file_name)?; + Ok(()) + } + + pub fn write_prelude(&self, output_dir: &str) -> io::Result<()> { + let file_name = "prelude.rs"; + let dir = Self::create_dir(output_dir)?; + let file_path = dir.join(file_name); + let mut file = fs::File::create(file_path)?; + Self::write_doc_comment(&mut file)?; + for entity in self.entities.iter() { + let code_block = Self::gen_prelude_use(entity); + file.write_all(code_block.to_string().as_bytes())?; + } + Self::format_file(output_dir, file_name)?; + Ok(()) + } + + pub fn write( + output_dir: &str, + entity: &Entity, + code_blocks: Vec, + ) -> io::Result<()> { + let dir = Self::create_dir(output_dir)?; + let file_path = dir.join(format!("{}.rs", entity.table_name)); + let mut file = fs::File::create(file_path)?; + Self::write_doc_comment(&mut file)?; + for code_block in code_blocks { + file.write_all(code_block.to_string().as_bytes())?; + file.write_all(b"\n\n")?; + } + Ok(()) + } + + pub fn write_doc_comment(file: &mut File) -> io::Result<()> { + let ver = env!("CARGO_PKG_VERSION"); + let comments = vec![format!( + "//! SeaORM Entity. Generated by sea-orm-codegen {}", + ver + )]; + for comment in comments { + file.write_all(comment.as_bytes())?; + file.write_all(b"\n\n")?; + } + Ok(()) + } + + pub fn create_dir(output_dir: &str) -> io::Result<&Path> { + let dir = Path::new(output_dir); + fs::create_dir_all(dir)?; + Ok(dir) + } + + pub fn format_entity(output_dir: &str, entity: &Entity) -> io::Result<()> { + Self::format_file(output_dir, &format!("{}.rs", entity.table_name)) + } + + pub fn format_file(output_dir: &str, file_name: &str) -> io::Result<()> { + Command::new("rustfmt") + .arg(Path::new(output_dir).join(file_name)) + .spawn()? + .wait()?; + Ok(()) + } + + pub fn gen_code_blocks(entity: &Entity) -> Vec { + let mut code_blocks = vec![ + Self::gen_import(), + Self::gen_entity_struct(), + Self::gen_impl_entity_name(entity), + Self::gen_model_struct(entity), + Self::gen_column_enum(entity), + Self::gen_primary_key_enum(entity), + Self::gen_impl_primary_key(entity), + Self::gen_relation_enum(entity), + Self::gen_impl_column_trait(entity), + Self::gen_impl_relation_trait(entity), + ]; + code_blocks.extend(Self::gen_impl_related(entity)); + code_blocks.extend(vec![ + Self::gen_impl_model(entity), + Self::gen_impl_active_model_behavior(), + ]); + code_blocks + } + + pub fn gen_import() -> TokenStream { + quote! { + use sea_orm::entity::prelude::*; + } + } + + pub fn gen_entity_struct() -> TokenStream { + quote! { + #[derive(Copy, Clone, Default, Debug, DeriveEntity)] + pub struct Entity; + } + } + + pub fn gen_impl_entity_name(entity: &Entity) -> TokenStream { + let table_name_snake_case = entity.get_table_name_snake_case(); + quote! { + impl EntityName for Entity { + fn table_name(&self) -> &str { + #table_name_snake_case + } + } + } + } + + pub fn gen_model_struct(entity: &Entity) -> TokenStream { + let column_names_snake_case = entity.get_column_names_snake_case(); + let column_rs_types = entity.get_column_rs_types(); + quote! { + #[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)] + pub struct Model { + #(pub #column_names_snake_case: #column_rs_types),* + } + } + } + + pub fn gen_column_enum(entity: &Entity) -> TokenStream { + let column_names_camel_case = entity.get_column_names_camel_case(); + quote! { + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + pub enum Column { + #(#column_names_camel_case),* + } + } + } + + pub fn gen_primary_key_enum(entity: &Entity) -> TokenStream { + let primary_key_names_camel_case = entity.get_primary_key_names_camel_case(); + quote! { + #[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] + pub enum PrimaryKey { + #(#primary_key_names_camel_case),* + } + } + } + + pub fn gen_impl_primary_key(entity: &Entity) -> TokenStream { + let primary_key_auto_increment = entity.get_primary_key_auto_increment(); + quote! { + impl PrimaryKeyTrait for PrimaryKey { + fn auto_increment() -> bool { + #primary_key_auto_increment + } + } + } + } + + pub fn gen_relation_enum(entity: &Entity) -> TokenStream { + let relation_ref_tables_camel_case = entity.get_relation_ref_tables_camel_case(); + quote! { + #[derive(Copy, Clone, Debug, EnumIter)] + pub enum Relation { + #(#relation_ref_tables_camel_case),* + } + } + } + + pub fn gen_impl_column_trait(entity: &Entity) -> TokenStream { + let column_names_camel_case = entity.get_column_names_camel_case(); + let column_types = entity.get_column_types(); + quote! { + impl ColumnTrait for Column { + type EntityName = Entity; + + fn def(&self) -> ColumnType { + match self { + #(Self::#column_names_camel_case => #column_types),* + } + } + } + } + } + + pub fn gen_impl_relation_trait(entity: &Entity) -> TokenStream { + let relation_ref_tables_camel_case = entity.get_relation_ref_tables_camel_case(); + let relation_rel_types = entity.get_relation_rel_types(); + let relation_ref_tables_snake_case = entity.get_relation_ref_tables_snake_case(); + let relation_columns_camel_case = entity.get_relation_columns_camel_case(); + let relation_ref_columns_camel_case = entity.get_relation_ref_columns_camel_case(); + let quoted = if relation_ref_tables_camel_case.is_empty() { + quote! { + _ => panic!("No RelationDef"), + } + } else { + quote! { + #(Self::#relation_ref_tables_camel_case => Entity::#relation_rel_types(super::#relation_ref_tables_snake_case::Entity) + .from(Column::#relation_columns_camel_case) + .to(super::#relation_ref_tables_snake_case::Column::#relation_ref_columns_camel_case) + .into()),* + } + }; + quote! { + impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + #quoted + } + } + } + } + } + + pub fn gen_impl_related(entity: &Entity) -> Vec { + let camel = entity.get_relation_ref_tables_camel_case(); + let snake = entity.get_relation_ref_tables_snake_case(); + camel + .iter() + .zip(snake) + .map(|(c, s)| { + quote! { + impl Related for Entity { + fn to() -> RelationDef { + Relation::#c.def() + } + } + } + }) + .collect() + } + + pub fn gen_impl_model(entity: &Entity) -> TokenStream { + let relation_ref_tables_snake_case = entity.get_relation_ref_tables_snake_case(); + let relation_rel_find_helpers = entity.get_relation_rel_find_helpers(); + quote! { + impl Model { + #(pub fn #relation_rel_find_helpers(&self) -> Select { + Entity::find_related().belongs_to::(self) + })* + } + } + } + + pub fn gen_impl_active_model_behavior() -> TokenStream { + quote! { + impl ActiveModelBehavior for ActiveModel {} + } + } + + pub fn gen_mod(entity: &Entity) -> TokenStream { + let table_name_snake_case_ident = entity.get_table_name_snake_case_ident(); + quote! { + pub mod #table_name_snake_case_ident; + } + } + + pub fn gen_prelude_use(entity: &Entity) -> TokenStream { + let table_name_snake_case_ident = entity.get_table_name_snake_case_ident(); + let table_name_camel_case_ident = entity.get_table_name_camel_case_ident(); + quote! { + pub use super::#table_name_snake_case_ident::Entity as #table_name_camel_case_ident; + } + } +} diff --git a/sea-orm-codegen/src/error.rs b/sea-orm-codegen/src/error.rs new file mode 100644 index 00000000..5454800c --- /dev/null +++ b/sea-orm-codegen/src/error.rs @@ -0,0 +1,40 @@ +use std::{error, fmt, io}; + +#[derive(Debug)] +pub enum Error { + StdIoError(io::Error), + SqlxError(sqlx::Error), + TransformError(String), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::StdIoError(e) => write!(f, "{:?}", e), + Self::SqlxError(e) => write!(f, "{:?}", e), + Self::TransformError(e) => write!(f, "{:?}", e), + } + } +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match self { + Self::StdIoError(e) => Some(e), + Self::SqlxError(e) => Some(e), + Self::TransformError(_) => None, + } + } +} + +impl From for Error { + fn from(io_err: io::Error) -> Self { + Self::StdIoError(io_err) + } +} + +impl From for Error { + fn from(sqlx_err: sqlx::Error) -> Self { + Self::SqlxError(sqlx_err) + } +} diff --git a/sea-orm-codegen/src/lib.rs b/sea-orm-codegen/src/lib.rs new file mode 100644 index 00000000..07e167bc --- /dev/null +++ b/sea-orm-codegen/src/lib.rs @@ -0,0 +1,5 @@ +mod entity; +mod error; + +pub use entity::*; +pub use error::*; diff --git a/src/entity/relation.rs b/src/entity/relation.rs index 4cb2a3d5..d618ce52 100644 --- a/src/entity/relation.rs +++ b/src/entity/relation.rs @@ -3,7 +3,7 @@ use core::marker::PhantomData; use sea_query::{DynIden, IntoIden, JoinType}; use std::fmt::Debug; -#[derive(Debug)] +#[derive(Clone, Debug)] pub enum RelationType { HasOne, HasMany,