From 6443e8e17aecae3a1945fe976694fba7abcb28bf Mon Sep 17 00:00:00 2001 From: nils-degroot Date: Tue, 28 Apr 2026 11:42:32 +0200 Subject: [PATCH 1/2] feat: support schema_name on DeriveActiveEnum --- sea-orm-macros/src/derives/active_enum.rs | 27 +++- sea-orm-macros/src/lib.rs | 2 + .../tests/derive_active_enum_test.rs | 75 +++++++++++ sea-orm-sync/src/entity/active_enum.rs | 6 + sea-orm-sync/src/query/helper.rs | 2 +- sea-orm-sync/src/schema/entity.rs | 22 ++- sea-orm-sync/tests/active_enum_tests.rs | 125 +++++++++++++++++ sea-orm-sync/tests/common/features/mod.rs | 2 + .../tests/common/features/schema_enum.rs | 17 +++ .../common/features/sea_orm_active_enums.rs | 28 ++++ src/entity/active_enum.rs | 6 + src/schema/entity.rs | 22 ++- tests/active_enum_tests.rs | 126 ++++++++++++++++++ tests/common/features/mod.rs | 2 + tests/common/features/schema_enum.rs | 17 +++ tests/common/features/sea_orm_active_enums.rs | 28 ++++ 16 files changed, 495 insertions(+), 12 deletions(-) create mode 100644 sea-orm-sync/tests/common/features/schema_enum.rs create mode 100644 tests/common/features/schema_enum.rs diff --git a/sea-orm-macros/src/derives/active_enum.rs b/sea-orm-macros/src/derives/active_enum.rs index 5f5cefe385..2794b2c24a 100644 --- a/sea-orm-macros/src/derives/active_enum.rs +++ b/sea-orm-macros/src/derives/active_enum.rs @@ -8,6 +8,7 @@ use syn::{Expr, Lit, LitInt, LitStr, UnOp, parse}; struct ActiveEnum { ident: syn::Ident, enum_name: String, + schema_name: Option, rs_type: RsType, db_type: DbType, is_string: bool, @@ -131,6 +132,7 @@ impl ActiveEnum { let ident = input.ident; let mut enum_name = ident.to_string().to_upper_camel_case(); + let mut schema_name = None; let mut rs_type = None; let mut db_type = None; let mut rename_all = None; @@ -150,6 +152,9 @@ impl ActiveEnum { } else if meta.path.is_ident("enum_name") { let litstr: LitStr = meta.value()?.parse()?; enum_name = litstr.value(); + } else if meta.path.is_ident("schema_name") { + let litstr: LitStr = meta.value()?.parse()?; + schema_name = Some(litstr.value()); } else if meta.path.is_ident("rename_all") { rename_all = Some((&meta).try_into()?); } else { @@ -299,6 +304,7 @@ impl ActiveEnum { Ok(Self { ident, enum_name, + schema_name, rs_type, db_type, is_string, @@ -504,6 +510,15 @@ impl ActiveEnum { } }; + let schema_name_impl = match &self.schema_name { + Some(name) => quote! { + fn schema_name() -> Option<&'static str> { + Some(#name) + } + }, + None => quote! {}, + }; + let val = if self.generate_enum_impls() { quote! { v.value.as_ref() } } else if self.is_string { @@ -523,6 +538,8 @@ impl ActiveEnum { #enum_name_iden.into() } + #schema_name_impl + fn to_value(&self) -> ::Value { #to_value_body } @@ -593,16 +610,20 @@ impl ActiveEnum { } let ident = &self.ident; - let enum_name = &self.enum_name; let ident_s = ident.to_string(); let variant_idents = &self.variant_idents; let variant_values = &self.variant_values; + let pg_type_name = match &self.schema_name { + Some(schema) => format!("{}.{}", schema, self.enum_name), + None => self.enum_name.clone(), + }; + quote! { #[automatically_derived] impl sea_orm::sqlx::Type for #ident { fn type_info() -> sea_orm::sqlx::postgres::PgTypeInfo { - sea_orm::sqlx::postgres::PgTypeInfo::with_name(#enum_name) + sea_orm::sqlx::postgres::PgTypeInfo::with_name(#pg_type_name) } } @@ -631,7 +652,7 @@ impl ActiveEnum { #[automatically_derived] impl sea_orm::sqlx::postgres::PgHasArrayType for #ident { fn array_type_info() -> sea_orm::sqlx::postgres::PgTypeInfo { - sea_orm::sqlx::postgres::PgTypeInfo::array_of(#enum_name) + sea_orm::sqlx::postgres::PgTypeInfo::array_of(#pg_type_name) } } } diff --git a/sea-orm-macros/src/lib.rs b/sea-orm-macros/src/lib.rs index 81996306b1..aa1a51d70b 100644 --- a/sea-orm-macros/src/lib.rs +++ b/sea-orm-macros/src/lib.rs @@ -692,6 +692,8 @@ pub fn derive_active_model_behavior(input: TokenStream) -> TokenStream { /// - `enum_name`: Define `String` returned by `ActiveEnum::name()` /// - This attribute is optional with default value being the name of enum in camel-case /// - Note that value has to be passed as string, i.e. `enum_name = "MyEnum"` +/// - `schema_name`: Define the database schema the enum type belongs to +/// - This attribute is optional; when omitted the enum uses the database search path /// - Constraints for native enums (`db_type = "Enum"`): /// - `rs_type` is optional; it defaults to `Enum`. If specified it must be `String` or `Enum`. /// - `num_value` and numeric discriminants are not allowed. diff --git a/sea-orm-macros/tests/derive_active_enum_test.rs b/sea-orm-macros/tests/derive_active_enum_test.rs index fd1ee2c7eb..1328db6e92 100644 --- a/sea-orm-macros/tests/derive_active_enum_test.rs +++ b/sea-orm-macros/tests/derive_active_enum_test.rs @@ -219,3 +219,78 @@ fn derive_non_database_enum_value_type() { assert_eq!(TestEnum2::enum_type_name(), None); assert_eq!(TestEnum2::array_type(), ArrayType::String); } + +#[derive(Debug, EnumIter, DeriveActiveEnum, Eq, PartialEq)] +#[sea_orm( + rs_type = "Enum", + db_type = "Enum", + enum_name = "tea", + schema_name = "my_schema" +)] +enum SchemaEnum { + #[sea_orm(string_value = "EverydayTea")] + EverydayTea, + #[sea_orm(string_value = "BreakfastTea")] + BreakfastTea, +} + +#[derive(Debug, EnumIter, DeriveActiveEnum, Eq, PartialEq)] +#[sea_orm( + rs_type = "String", + db_type = "Enum", + enum_name = "color", + schema_name = "palette" +)] +enum SchemaStringEnum { + #[sea_orm(string_value = "Red")] + Red, + #[sea_orm(string_value = "Blue")] + Blue, +} + +#[derive(Debug, EnumIter, DeriveActiveEnum, Eq, PartialEq)] +#[sea_orm(rs_type = "Enum", db_type = "Enum", enum_name = "status")] +enum NoSchemaEnum { + #[sea_orm(string_value = "Active")] + Active, + #[sea_orm(string_value = "Inactive")] + Inactive, +} + +#[test] +fn derive_active_enum_schema_name() { + assert_eq!(SchemaEnum::schema_name(), Some("my_schema")); + assert_eq!(SchemaStringEnum::schema_name(), Some("palette")); + assert_eq!(NoSchemaEnum::schema_name(), None); +} + +#[test] +fn derive_active_enum_schema_name_enum_type() { + assert_eq!(SchemaEnum::enum_type_name(), Some("tea")); + assert_eq!(SchemaStringEnum::enum_type_name(), Some("color")); + assert_eq!(NoSchemaEnum::enum_type_name(), Some("status")); +} + +#[test] +fn derive_active_enum_schema_name_values_roundtrip() { + let value = SchemaEnum::EverydayTea.to_value(); + assert_eq!(value.value.as_ref(), "EverydayTea"); + assert_eq!( + ::try_from_value(&value), + Ok(SchemaEnum::EverydayTea) + ); +} + +#[test] +fn derive_active_enum_schema_name_value_conversion() { + let value: Value = SchemaEnum::BreakfastTea.to_value().into(); + assert_eq!( + value, + Value::Enum(sea_orm::sea_query::OptionEnum::Some(Box::new( + sea_orm::sea_query::Enum { + type_name: String::from("tea").into(), + value: "BreakfastTea".into(), + }, + ))) + ); +} diff --git a/sea-orm-sync/src/entity/active_enum.rs b/sea-orm-sync/src/entity/active_enum.rs index f6aabf4fc5..ca72c5d90d 100644 --- a/sea-orm-sync/src/entity/active_enum.rs +++ b/sea-orm-sync/src/entity/active_enum.rs @@ -119,6 +119,12 @@ pub trait ActiveEnum: Sized + Iterable { /// Get the name of enum fn name() -> DynIden; + /// Get the schema name of the enum, if specified. + /// Returns `None` by default, meaning the enum lives in the database search path. + fn schema_name() -> Option<&'static str> { + None + } + /// Convert enum variant into the corresponding value. fn to_value(&self) -> Self::Value; diff --git a/sea-orm-sync/src/query/helper.rs b/sea-orm-sync/src/query/helper.rs index 7aef179692..c3b0971722 100644 --- a/sea-orm-sync/src/query/helper.rs +++ b/sea-orm-sync/src/query/helper.rs @@ -909,7 +909,7 @@ pub(crate) fn join_tbl_on_condition( foreign_keys: Identity, ) -> Condition { let mut cond = Condition::all(); - for (owner_key, foreign_key) in owner_keys.into_iter().zip(foreign_keys.into_iter()) { + for (owner_key, foreign_key) in owner_keys.into_iter().zip(foreign_keys) { cond = cond .add(Expr::col((from_tbl.clone(), owner_key)).equals((to_tbl.clone(), foreign_key))); } diff --git a/sea-orm-sync/src/schema/entity.rs b/sea-orm-sync/src/schema/entity.rs index 1273c8ff57..e6fe081427 100644 --- a/sea-orm-sync/src/schema/entity.rs +++ b/sea-orm-sync/src/schema/entity.rs @@ -3,14 +3,17 @@ use crate::{ PrimaryKeyArity, PrimaryKeyToColumn, PrimaryKeyTrait, RelationTrait, Schema, }; use sea_query::{ - ColumnDef, DynIden, Iden, Index, IndexCreateStatement, SeaRc, TableCreateStatement, + Alias, ColumnDef, DynIden, Iden, Index, IndexCreateStatement, SeaRc, TableCreateStatement, extension::postgres::{Type, TypeCreateStatement}, }; use std::collections::BTreeMap; impl Schema { - /// Creates Postgres enums from an ActiveEnum. See [`TypeCreateStatement`] for more details. - /// Returns None if not Postgres. + /// Creates a Postgres enum type from an [`ActiveEnum`]. See [`TypeCreateStatement`] for more details. + /// Returns `None` if not Postgres. + /// + /// If the [`ActiveEnum`] has a `schema_name` (via `#[sea_orm(schema_name = "...")]`), + /// the resulting statement will be schema-qualified: `CREATE TYPE "schema"."name" AS ENUM (...)`. pub fn create_enum_from_active_enum(&self) -> Option where A: ActiveEnum, @@ -106,7 +109,18 @@ where } let col_def = A::db_type(); let col_type = col_def.get_column_type(); - create_enum_from_column_type(col_type) + let (name, variants) = match col_type { + ColumnType::Enum { name, variants } => (name.clone(), variants.clone()), + _ => return None, + }; + let mut stmt = Type::create(); + if let Some(schema) = A::schema_name() { + let schema_iden: DynIden = SeaRc::new(Alias::new(schema)); + stmt.as_enum((schema_iden, name)); + } else { + stmt.as_enum(name); + } + Some(stmt.values(variants).to_owned()) } pub(crate) fn create_enum_from_column_type(col_type: &ColumnType) -> Option { diff --git a/sea-orm-sync/tests/active_enum_tests.rs b/sea-orm-sync/tests/active_enum_tests.rs index 6323b4c1a0..f2b01f300c 100644 --- a/sea-orm-sync/tests/active_enum_tests.rs +++ b/sea-orm-sync/tests/active_enum_tests.rs @@ -1153,4 +1153,129 @@ mod tests { r#"SELECT CAST("aaa"."tea" AS "text") AS "tea", CAST("foo"."tea" AS "text") AS "nested_tea" FROM "public"."active_enum""#, ); } + + #[test] + fn create_enum_from_active_enum_with_schema_name() { + use sea_orm::{Schema, Statement}; + + let db_postgres = DbBackend::Postgres; + let schema = Schema::new(db_postgres); + + assert_eq!( + db_postgres.build(&schema.create_enum_from_active_enum::().unwrap()), + Statement::from_string( + db_postgres, + r#"CREATE TYPE "my_schema"."mood" AS ENUM ('Happy', 'Sad')"#.to_owned() + ) + ); + + assert_eq!( + db_postgres.build(&schema.create_enum_from_active_enum::().unwrap()), + Statement::from_string( + db_postgres, + r#"CREATE TYPE "my_schema"."priority" AS ENUM ('Low', 'High')"#.to_owned() + ) + ); + } + + #[test] + fn active_enum_schema_name_returns_correct_value() { + use sea_orm::ActiveEnum as ActiveEnumTrait2; + + assert_eq!(Mood::schema_name(), Some("my_schema")); + assert_eq!(Priority::schema_name(), Some("my_schema")); + assert_eq!(Tea::schema_name(), None); + } + + #[test] + fn create_enum_without_schema_name_unchanged() { + use sea_orm::{Schema, Statement}; + + let db_postgres = DbBackend::Postgres; + let schema = Schema::new(db_postgres); + + assert_eq!( + db_postgres.build(&schema.create_enum_from_active_enum::().unwrap()), + Statement::from_string( + db_postgres, + r#"CREATE TYPE "tea" AS ENUM ('EverydayTea', 'BreakfastTea', 'AfternoonTea')"# + .to_owned() + ) + ); + } + + #[cfg(feature = "sqlx-postgres")] + #[test] + fn schema_enum_find_select_sql() { + let select = schema_enum::Entity::find(); + assert_eq!( + select.build(DbBackend::Postgres).to_string(), + [ + r#"SELECT "schema_enum"."id","#, + r#"CAST("schema_enum"."mood" AS "text"),"#, + r#"CAST("schema_enum"."priority" AS "text")"#, + r#"FROM "my_schema"."schema_enum""#, + ] + .join(" ") + ); + } + + #[cfg(feature = "sqlx-postgres")] + #[test] + fn schema_enum_filter_is_in_sql() { + let select = schema_enum::Entity::find() + .filter(schema_enum::Column::Mood.is_in([Mood::Happy, Mood::Sad])); + assert_eq!( + select.build(DbBackend::Postgres).to_string(), + [ + r#"SELECT "schema_enum"."id","#, + r#"CAST("schema_enum"."mood" AS "text"),"#, + r#"CAST("schema_enum"."priority" AS "text")"#, + r#"FROM "my_schema"."schema_enum""#, + r#"WHERE "schema_enum"."mood" IN ('Happy'::"mood", 'Sad'::"mood")"#, + ] + .join(" ") + ); + } + + #[cfg(feature = "sqlx-postgres")] + #[test] + fn schema_enum_filter_eq_sql() { + let select = schema_enum::Entity::find().filter(schema_enum::Column::Mood.eq(Mood::Happy)); + assert_eq!( + select.build(DbBackend::Postgres).to_string(), + [ + r#"SELECT "schema_enum"."id","#, + r#"CAST("schema_enum"."mood" AS "text"),"#, + r#"CAST("schema_enum"."priority" AS "text")"#, + r#"FROM "my_schema"."schema_enum""#, + r#"WHERE "schema_enum"."mood" = 'Happy'::"mood""#, + ] + .join(" ") + ); + } + + #[test] + fn schema_enum_create_type_from_active_enum() { + use sea_orm::{Schema, Statement}; + + let db_postgres = DbBackend::Postgres; + let schema = Schema::new(db_postgres); + + assert_eq!( + db_postgres.build(&schema.create_enum_from_active_enum::().unwrap()), + Statement::from_string( + db_postgres, + r#"CREATE TYPE "my_schema"."mood" AS ENUM ('Happy', 'Sad')"#.to_owned() + ) + ); + + assert_eq!( + db_postgres.build(&schema.create_enum_from_active_enum::().unwrap()), + Statement::from_string( + db_postgres, + r#"CREATE TYPE "my_schema"."priority" AS ENUM ('Low', 'High')"#.to_owned() + ) + ); + } } diff --git a/sea-orm-sync/tests/common/features/mod.rs b/sea-orm-sync/tests/common/features/mod.rs index b9e5e4a7f7..151bc58aa1 100644 --- a/sea-orm-sync/tests/common/features/mod.rs +++ b/sea-orm-sync/tests/common/features/mod.rs @@ -26,6 +26,7 @@ pub mod pi; pub mod repository; pub mod satellite; pub mod schema; +pub mod schema_enum; pub mod sea_orm_active_enums; pub mod self_join; pub mod teas; @@ -57,6 +58,7 @@ pub use metadata::Entity as Metadata; pub use repository::Entity as Repository; pub use satellite::Entity as Satellite; pub use schema::*; +pub use schema_enum::Entity as SchemaEnum; pub use sea_orm_active_enums::*; pub use self_join::Entity as SelfJoin; pub use teas::Entity as Teas; diff --git a/sea-orm-sync/tests/common/features/schema_enum.rs b/sea-orm-sync/tests/common/features/schema_enum.rs new file mode 100644 index 0000000000..9b9484abba --- /dev/null +++ b/sea-orm-sync/tests/common/features/schema_enum.rs @@ -0,0 +1,17 @@ +use super::sea_orm_active_enums::*; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[cfg_attr(feature = "sqlx-postgres", sea_orm(schema_name = "my_schema"))] +#[sea_orm(table_name = "schema_enum")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub mood: Option, + pub priority: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-sync/tests/common/features/sea_orm_active_enums.rs b/sea-orm-sync/tests/common/features/sea_orm_active_enums.rs index 87d510dbda..60a5e19cea 100644 --- a/sea-orm-sync/tests/common/features/sea_orm_active_enums.rs +++ b/sea-orm-sync/tests/common/features/sea_orm_active_enums.rs @@ -66,3 +66,31 @@ pub enum DisplayTea { #[sea_orm(string_value = "BreakfastTea", display_value = "Breakfast")] BreakfastTea, } + +#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)] +#[sea_orm( + rs_type = "Enum", + db_type = "Enum", + enum_name = "mood", + schema_name = "my_schema" +)] +pub enum Mood { + #[sea_orm(string_value = "Happy")] + Happy, + #[sea_orm(string_value = "Sad")] + Sad, +} + +#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)] +#[sea_orm( + rs_type = "String", + db_type = "Enum", + enum_name = "priority", + schema_name = "my_schema" +)] +pub enum Priority { + #[sea_orm(string_value = "Low")] + Low, + #[sea_orm(string_value = "High")] + High, +} diff --git a/src/entity/active_enum.rs b/src/entity/active_enum.rs index f6aabf4fc5..ca72c5d90d 100644 --- a/src/entity/active_enum.rs +++ b/src/entity/active_enum.rs @@ -119,6 +119,12 @@ pub trait ActiveEnum: Sized + Iterable { /// Get the name of enum fn name() -> DynIden; + /// Get the schema name of the enum, if specified. + /// Returns `None` by default, meaning the enum lives in the database search path. + fn schema_name() -> Option<&'static str> { + None + } + /// Convert enum variant into the corresponding value. fn to_value(&self) -> Self::Value; diff --git a/src/schema/entity.rs b/src/schema/entity.rs index 1273c8ff57..e6fe081427 100644 --- a/src/schema/entity.rs +++ b/src/schema/entity.rs @@ -3,14 +3,17 @@ use crate::{ PrimaryKeyArity, PrimaryKeyToColumn, PrimaryKeyTrait, RelationTrait, Schema, }; use sea_query::{ - ColumnDef, DynIden, Iden, Index, IndexCreateStatement, SeaRc, TableCreateStatement, + Alias, ColumnDef, DynIden, Iden, Index, IndexCreateStatement, SeaRc, TableCreateStatement, extension::postgres::{Type, TypeCreateStatement}, }; use std::collections::BTreeMap; impl Schema { - /// Creates Postgres enums from an ActiveEnum. See [`TypeCreateStatement`] for more details. - /// Returns None if not Postgres. + /// Creates a Postgres enum type from an [`ActiveEnum`]. See [`TypeCreateStatement`] for more details. + /// Returns `None` if not Postgres. + /// + /// If the [`ActiveEnum`] has a `schema_name` (via `#[sea_orm(schema_name = "...")]`), + /// the resulting statement will be schema-qualified: `CREATE TYPE "schema"."name" AS ENUM (...)`. pub fn create_enum_from_active_enum(&self) -> Option where A: ActiveEnum, @@ -106,7 +109,18 @@ where } let col_def = A::db_type(); let col_type = col_def.get_column_type(); - create_enum_from_column_type(col_type) + let (name, variants) = match col_type { + ColumnType::Enum { name, variants } => (name.clone(), variants.clone()), + _ => return None, + }; + let mut stmt = Type::create(); + if let Some(schema) = A::schema_name() { + let schema_iden: DynIden = SeaRc::new(Alias::new(schema)); + stmt.as_enum((schema_iden, name)); + } else { + stmt.as_enum(name); + } + Some(stmt.values(variants).to_owned()) } pub(crate) fn create_enum_from_column_type(col_type: &ColumnType) -> Option { diff --git a/tests/active_enum_tests.rs b/tests/active_enum_tests.rs index cdf684860d..99bbe2d7da 100644 --- a/tests/active_enum_tests.rs +++ b/tests/active_enum_tests.rs @@ -1187,4 +1187,130 @@ mod tests { r#"SELECT CAST("aaa"."tea" AS "text") AS "tea", CAST("foo"."tea" AS "text") AS "nested_tea" FROM "public"."active_enum""#, ); } + + #[test] + fn create_enum_from_active_enum_with_schema_name() { + use sea_orm::{Schema, Statement}; + + let db_postgres = DbBackend::Postgres; + let schema = Schema::new(db_postgres); + + assert_eq!( + db_postgres.build(&schema.create_enum_from_active_enum::().unwrap()), + Statement::from_string( + db_postgres, + r#"CREATE TYPE "my_schema"."mood" AS ENUM ('Happy', 'Sad')"#.to_owned() + ) + ); + + assert_eq!( + db_postgres.build(&schema.create_enum_from_active_enum::().unwrap()), + Statement::from_string( + db_postgres, + r#"CREATE TYPE "my_schema"."priority" AS ENUM ('Low', 'High')"#.to_owned() + ) + ); + } + + #[test] + fn active_enum_schema_name_returns_correct_value() { + use sea_orm::ActiveEnum as ActiveEnumTrait2; + + assert_eq!(Mood::schema_name(), Some("my_schema")); + assert_eq!(Priority::schema_name(), Some("my_schema")); + assert_eq!(Tea::schema_name(), None); + } + + #[test] + fn create_enum_without_schema_name_unchanged() { + use sea_orm::{Schema, Statement}; + + let db_postgres = DbBackend::Postgres; + let schema = Schema::new(db_postgres); + + assert_eq!( + db_postgres.build(&schema.create_enum_from_active_enum::().unwrap()), + Statement::from_string( + db_postgres, + r#"CREATE TYPE "tea" AS ENUM ('EverydayTea', 'BreakfastTea', 'AfternoonTea')"# + .to_owned() + ) + ); + } + + #[cfg(feature = "sqlx-postgres")] + #[test] + fn schema_enum_find_select_sql() { + let select = schema_enum::Entity::find(); + assert_eq!( + select.build(DbBackend::Postgres).to_string(), + [ + r#"SELECT "schema_enum"."id","#, + r#"CAST("schema_enum"."mood" AS "text"),"#, + r#"CAST("schema_enum"."priority" AS "text")"#, + r#"FROM "my_schema"."schema_enum""#, + ] + .join(" ") + ); + } + + #[cfg(feature = "sqlx-postgres")] + #[test] + fn schema_enum_filter_is_in_sql() { + let select = schema_enum::Entity::find() + .filter(schema_enum::Column::Mood.is_in([Mood::Happy, Mood::Sad])); + assert_eq!( + select.build(DbBackend::Postgres).to_string(), + [ + r#"SELECT "schema_enum"."id","#, + r#"CAST("schema_enum"."mood" AS "text"),"#, + r#"CAST("schema_enum"."priority" AS "text")"#, + r#"FROM "my_schema"."schema_enum""#, + r#"WHERE "schema_enum"."mood" IN ('Happy'::"mood", 'Sad'::"mood")"#, + ] + .join(" ") + ); + } + + #[cfg(feature = "sqlx-postgres")] + #[test] + fn schema_enum_filter_eq_sql() { + let select = schema_enum::Entity::find() + .filter(schema_enum::Column::Mood.eq(Mood::Happy)); + assert_eq!( + select.build(DbBackend::Postgres).to_string(), + [ + r#"SELECT "schema_enum"."id","#, + r#"CAST("schema_enum"."mood" AS "text"),"#, + r#"CAST("schema_enum"."priority" AS "text")"#, + r#"FROM "my_schema"."schema_enum""#, + r#"WHERE "schema_enum"."mood" = 'Happy'::"mood""#, + ] + .join(" ") + ); + } + + #[test] + fn schema_enum_create_type_from_active_enum() { + use sea_orm::{Schema, Statement}; + + let db_postgres = DbBackend::Postgres; + let schema = Schema::new(db_postgres); + + assert_eq!( + db_postgres.build(&schema.create_enum_from_active_enum::().unwrap()), + Statement::from_string( + db_postgres, + r#"CREATE TYPE "my_schema"."mood" AS ENUM ('Happy', 'Sad')"#.to_owned() + ) + ); + + assert_eq!( + db_postgres.build(&schema.create_enum_from_active_enum::().unwrap()), + Statement::from_string( + db_postgres, + r#"CREATE TYPE "my_schema"."priority" AS ENUM ('Low', 'High')"#.to_owned() + ) + ); + } } diff --git a/tests/common/features/mod.rs b/tests/common/features/mod.rs index b9e5e4a7f7..151bc58aa1 100644 --- a/tests/common/features/mod.rs +++ b/tests/common/features/mod.rs @@ -26,6 +26,7 @@ pub mod pi; pub mod repository; pub mod satellite; pub mod schema; +pub mod schema_enum; pub mod sea_orm_active_enums; pub mod self_join; pub mod teas; @@ -57,6 +58,7 @@ pub use metadata::Entity as Metadata; pub use repository::Entity as Repository; pub use satellite::Entity as Satellite; pub use schema::*; +pub use schema_enum::Entity as SchemaEnum; pub use sea_orm_active_enums::*; pub use self_join::Entity as SelfJoin; pub use teas::Entity as Teas; diff --git a/tests/common/features/schema_enum.rs b/tests/common/features/schema_enum.rs new file mode 100644 index 0000000000..9b9484abba --- /dev/null +++ b/tests/common/features/schema_enum.rs @@ -0,0 +1,17 @@ +use super::sea_orm_active_enums::*; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[cfg_attr(feature = "sqlx-postgres", sea_orm(schema_name = "my_schema"))] +#[sea_orm(table_name = "schema_enum")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub mood: Option, + pub priority: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/tests/common/features/sea_orm_active_enums.rs b/tests/common/features/sea_orm_active_enums.rs index 87d510dbda..60a5e19cea 100644 --- a/tests/common/features/sea_orm_active_enums.rs +++ b/tests/common/features/sea_orm_active_enums.rs @@ -66,3 +66,31 @@ pub enum DisplayTea { #[sea_orm(string_value = "BreakfastTea", display_value = "Breakfast")] BreakfastTea, } + +#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)] +#[sea_orm( + rs_type = "Enum", + db_type = "Enum", + enum_name = "mood", + schema_name = "my_schema" +)] +pub enum Mood { + #[sea_orm(string_value = "Happy")] + Happy, + #[sea_orm(string_value = "Sad")] + Sad, +} + +#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)] +#[sea_orm( + rs_type = "String", + db_type = "Enum", + enum_name = "priority", + schema_name = "my_schema" +)] +pub enum Priority { + #[sea_orm(string_value = "Low")] + Low, + #[sea_orm(string_value = "High")] + High, +} From ac4305a4910a497b0b40358d36824361d4ebc878 Mon Sep 17 00:00:00 2001 From: nils-degroot Date: Wed, 29 Apr 2026 09:41:05 +0200 Subject: [PATCH 2/2] chore: split schema name tests and properly serialize cast --- sea-orm-macros/src/derives/active_enum.rs | 34 ++-- tests/active_enum_schema_name_tests.rs | 175 ++++++++++++++++++ tests/active_enum_tests.rs | 126 ------------- tests/common/features/mod.rs | 2 - tests/common/features/schema_enum.rs | 17 -- tests/common/features/sea_orm_active_enums.rs | 28 --- 6 files changed, 194 insertions(+), 188 deletions(-) create mode 100644 tests/active_enum_schema_name_tests.rs delete mode 100644 tests/common/features/schema_enum.rs diff --git a/sea-orm-macros/src/derives/active_enum.rs b/sea-orm-macros/src/derives/active_enum.rs index 2794b2c24a..3e74ef8825 100644 --- a/sea-orm-macros/src/derives/active_enum.rs +++ b/sea-orm-macros/src/derives/active_enum.rs @@ -7,8 +7,8 @@ use syn::{Expr, Lit, LitInt, LitStr, UnOp, parse}; struct ActiveEnum { ident: syn::Ident, - enum_name: String, - schema_name: Option, + // (schema name, enum name) + enum_name: (Option, String), rs_type: RsType, db_type: DbType, is_string: bool, @@ -303,8 +303,7 @@ impl ActiveEnum { Ok(Self { ident, - enum_name, - schema_name, + enum_name: (schema_name, enum_name), rs_type, db_type, is_string, @@ -320,7 +319,7 @@ impl ActiveEnum { } fn to_value_impl(&self) -> TokenStream { - let enum_name = &self.enum_name; + let enum_name = self.schema_qualified_name(); let variant_idents = &self.variant_idents; let variant_values = &self.variant_values; @@ -370,7 +369,7 @@ impl ActiveEnum { fn nullable_impl(&self) -> TokenStream { let ident = &self.ident; - let enum_name = &self.enum_name; + let enum_name = &self.enum_name.1; let nullable_value_impl = if self.generate_enum_impls() { quote! { use sea_orm::sea_query::{OptionEnum, Value}; @@ -397,7 +396,7 @@ impl ActiveEnum { fn value_type_impl(&self) -> TokenStream { let ident = &self.ident; let value_type_try_from_impl = self.value_type_try_from_impl(); - let enum_name = &self.enum_name; + let enum_name = &self.enum_name.1; let type_name_impl = quote! { stringify!(#ident).to_owned() }; @@ -459,7 +458,8 @@ impl ActiveEnum { quote!() }; let try_get_by_impl = { - let enum_name = &self.enum_name; + let enum_name = self.schema_qualified_name(); + if self.generate_enum_impls() { quote! { #sqlx_postgres_try_get @@ -510,7 +510,7 @@ impl ActiveEnum { } }; - let schema_name_impl = match &self.schema_name { + let schema_name_impl = match &self.enum_name.0 { Some(name) => quote! { fn schema_name() -> Option<&'static str> { Some(#name) @@ -566,7 +566,7 @@ impl ActiveEnum { let ident = &self.ident; if self.generate_enum_impls() { - let enum_name = &self.enum_name; + let enum_name = self.schema_qualified_name(); let variant_idents = &self.variant_idents; let variant_values = &self.variant_values; @@ -613,11 +613,7 @@ impl ActiveEnum { let ident_s = ident.to_string(); let variant_idents = &self.variant_idents; let variant_values = &self.variant_values; - - let pg_type_name = match &self.schema_name { - Some(schema) => format!("{}.{}", schema, self.enum_name), - None => self.enum_name.clone(), - }; + let pg_type_name = self.schema_qualified_name(); quote! { #[automatically_derived] @@ -698,6 +694,7 @@ impl ActiveEnum { .. } = self; + let enum_name = enum_name.1.clone(); let enum_name_iden = format_ident!("{}Enum", ident); let str_variants: Vec = variants @@ -816,6 +813,13 @@ impl ActiveEnum { #not_u8_impl ) } + + fn schema_qualified_name(&self) -> String { + match &self.enum_name { + (Some(schema), enum_name) => format!("{schema}\".\"{enum_name}"), + (None, enum_name) => enum_name.clone(), + } + } } pub fn expand_derive_active_enum(input: syn::DeriveInput) -> syn::Result { diff --git a/tests/active_enum_schema_name_tests.rs b/tests/active_enum_schema_name_tests.rs new file mode 100644 index 0000000000..f3cae702b1 --- /dev/null +++ b/tests/active_enum_schema_name_tests.rs @@ -0,0 +1,175 @@ +use sea_orm::DbBackend; + +use common::features::Tea; +#[cfg(feature = "sqlx-postgres")] +use sea_orm::{QueryFilter, QueryTrait, entity::*}; + +#[path = "./common/mod.rs"] +pub mod common; + +#[derive(Debug, Clone, PartialEq, Eq, sea_orm::EnumIter, sea_orm::DeriveActiveEnum)] +#[sea_orm( + rs_type = "Enum", + db_type = "Enum", + enum_name = "mood", + schema_name = "my_schema" +)] +enum Mood { + #[sea_orm(string_value = "Happy")] + Happy, + #[sea_orm(string_value = "Sad")] + Sad, +} + +#[derive(Debug, Clone, PartialEq, Eq, sea_orm::EnumIter, sea_orm::DeriveActiveEnum)] +#[sea_orm( + rs_type = "String", + db_type = "Enum", + enum_name = "priority", + schema_name = "my_schema" +)] +enum Priority { + #[sea_orm(string_value = "Low")] + Low, + #[sea_orm(string_value = "High")] + High, +} + +mod schema_enum { + use super::{Mood, Priority}; + use sea_orm::entity::prelude::*; + + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[cfg_attr(feature = "sqlx-postgres", sea_orm(schema_name = "my_schema"))] + #[sea_orm(table_name = "schema_enum")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub mood: Option, + pub priority: Option, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + impl ActiveModelBehavior for ActiveModel {} +} + +#[test] +fn create_enum_from_active_enum_with_schema_name() { + use sea_orm::{Schema, Statement}; + + let db_postgres = DbBackend::Postgres; + let schema = Schema::new(db_postgres); + + assert_eq!( + db_postgres.build(&schema.create_enum_from_active_enum::().unwrap()), + Statement::from_string( + db_postgres, + r#"CREATE TYPE "my_schema"."mood" AS ENUM ('Happy', 'Sad')"#.to_owned() + ) + ); + + assert_eq!( + db_postgres.build(&schema.create_enum_from_active_enum::().unwrap()), + Statement::from_string( + db_postgres, + r#"CREATE TYPE "my_schema"."priority" AS ENUM ('Low', 'High')"#.to_owned() + ) + ); +} + +#[test] +fn active_enum_schema_name_returns_correct_value() { + use sea_orm::ActiveEnum as ActiveEnumTrait2; + + assert_eq!(Mood::schema_name(), Some("my_schema")); + assert_eq!(Tea::schema_name(), None); +} + +#[test] +fn create_enum_without_schema_name_unchanged() { + use sea_orm::{Schema, Statement}; + + let db_postgres = DbBackend::Postgres; + let schema = Schema::new(db_postgres); + + assert_eq!( + db_postgres.build(&schema.create_enum_from_active_enum::().unwrap()), + Statement::from_string( + db_postgres, + r#"CREATE TYPE "tea" AS ENUM ('EverydayTea', 'BreakfastTea', 'AfternoonTea')"# + .to_owned() + ) + ); +} + +#[cfg(feature = "sqlx-postgres")] +#[test] +fn schema_enum_find_select_sql() { + let select = schema_enum::Entity::find(); + + assert_eq!( + select.build(DbBackend::Postgres).to_string(), + [ + r#"SELECT "schema_enum"."id","#, + r#"CAST("schema_enum"."mood" AS "text"),"#, + r#"CAST("schema_enum"."priority" AS "text")"#, + r#"FROM "my_schema"."schema_enum""#, + ] + .join(" ") + ); +} + +#[cfg(feature = "sqlx-postgres")] +#[test] +fn schema_enum_filter_is_in_sql() { + let select = schema_enum::Entity::find() + .filter(schema_enum::Column::Mood.is_in([Mood::Happy, Mood::Sad])); + + assert_eq!( + select.build(DbBackend::Postgres).to_string(), + [ + r#"SELECT "schema_enum"."id","#, + r#"CAST("schema_enum"."mood" AS "text"),"#, + r#"CAST("schema_enum"."priority" AS "text")"#, + r#"FROM "my_schema"."schema_enum""#, + r#"WHERE "schema_enum"."mood" IN ('Happy'::"my_schema"."mood", 'Sad'::"my_schema"."mood")"#, + ] + .join(" ") + ); +} + +#[cfg(feature = "sqlx-postgres")] +#[test] +fn schema_enum_filter_eq_sql() { + let select = schema_enum::Entity::find().filter(schema_enum::Column::Mood.eq(Mood::Happy)); + + assert_eq!( + select.build(DbBackend::Postgres).to_string(), + [ + r#"SELECT "schema_enum"."id","#, + r#"CAST("schema_enum"."mood" AS "text"),"#, + r#"CAST("schema_enum"."priority" AS "text")"#, + r#"FROM "my_schema"."schema_enum""#, + r#"WHERE "schema_enum"."mood" = 'Happy'::"my_schema"."mood""#, + ] + .join(" ") + ); +} + +#[test] +fn schema_enum_create_type_from_active_enum() { + use sea_orm::{Schema, Statement}; + + let db_postgres = DbBackend::Postgres; + let schema = Schema::new(db_postgres); + + assert_eq!( + db_postgres.build(&schema.create_enum_from_active_enum::().unwrap()), + Statement::from_string( + db_postgres, + r#"CREATE TYPE "my_schema"."priority" AS ENUM ('Low', 'High')"#.to_owned() + ) + ); +} diff --git a/tests/active_enum_tests.rs b/tests/active_enum_tests.rs index 99bbe2d7da..cdf684860d 100644 --- a/tests/active_enum_tests.rs +++ b/tests/active_enum_tests.rs @@ -1187,130 +1187,4 @@ mod tests { r#"SELECT CAST("aaa"."tea" AS "text") AS "tea", CAST("foo"."tea" AS "text") AS "nested_tea" FROM "public"."active_enum""#, ); } - - #[test] - fn create_enum_from_active_enum_with_schema_name() { - use sea_orm::{Schema, Statement}; - - let db_postgres = DbBackend::Postgres; - let schema = Schema::new(db_postgres); - - assert_eq!( - db_postgres.build(&schema.create_enum_from_active_enum::().unwrap()), - Statement::from_string( - db_postgres, - r#"CREATE TYPE "my_schema"."mood" AS ENUM ('Happy', 'Sad')"#.to_owned() - ) - ); - - assert_eq!( - db_postgres.build(&schema.create_enum_from_active_enum::().unwrap()), - Statement::from_string( - db_postgres, - r#"CREATE TYPE "my_schema"."priority" AS ENUM ('Low', 'High')"#.to_owned() - ) - ); - } - - #[test] - fn active_enum_schema_name_returns_correct_value() { - use sea_orm::ActiveEnum as ActiveEnumTrait2; - - assert_eq!(Mood::schema_name(), Some("my_schema")); - assert_eq!(Priority::schema_name(), Some("my_schema")); - assert_eq!(Tea::schema_name(), None); - } - - #[test] - fn create_enum_without_schema_name_unchanged() { - use sea_orm::{Schema, Statement}; - - let db_postgres = DbBackend::Postgres; - let schema = Schema::new(db_postgres); - - assert_eq!( - db_postgres.build(&schema.create_enum_from_active_enum::().unwrap()), - Statement::from_string( - db_postgres, - r#"CREATE TYPE "tea" AS ENUM ('EverydayTea', 'BreakfastTea', 'AfternoonTea')"# - .to_owned() - ) - ); - } - - #[cfg(feature = "sqlx-postgres")] - #[test] - fn schema_enum_find_select_sql() { - let select = schema_enum::Entity::find(); - assert_eq!( - select.build(DbBackend::Postgres).to_string(), - [ - r#"SELECT "schema_enum"."id","#, - r#"CAST("schema_enum"."mood" AS "text"),"#, - r#"CAST("schema_enum"."priority" AS "text")"#, - r#"FROM "my_schema"."schema_enum""#, - ] - .join(" ") - ); - } - - #[cfg(feature = "sqlx-postgres")] - #[test] - fn schema_enum_filter_is_in_sql() { - let select = schema_enum::Entity::find() - .filter(schema_enum::Column::Mood.is_in([Mood::Happy, Mood::Sad])); - assert_eq!( - select.build(DbBackend::Postgres).to_string(), - [ - r#"SELECT "schema_enum"."id","#, - r#"CAST("schema_enum"."mood" AS "text"),"#, - r#"CAST("schema_enum"."priority" AS "text")"#, - r#"FROM "my_schema"."schema_enum""#, - r#"WHERE "schema_enum"."mood" IN ('Happy'::"mood", 'Sad'::"mood")"#, - ] - .join(" ") - ); - } - - #[cfg(feature = "sqlx-postgres")] - #[test] - fn schema_enum_filter_eq_sql() { - let select = schema_enum::Entity::find() - .filter(schema_enum::Column::Mood.eq(Mood::Happy)); - assert_eq!( - select.build(DbBackend::Postgres).to_string(), - [ - r#"SELECT "schema_enum"."id","#, - r#"CAST("schema_enum"."mood" AS "text"),"#, - r#"CAST("schema_enum"."priority" AS "text")"#, - r#"FROM "my_schema"."schema_enum""#, - r#"WHERE "schema_enum"."mood" = 'Happy'::"mood""#, - ] - .join(" ") - ); - } - - #[test] - fn schema_enum_create_type_from_active_enum() { - use sea_orm::{Schema, Statement}; - - let db_postgres = DbBackend::Postgres; - let schema = Schema::new(db_postgres); - - assert_eq!( - db_postgres.build(&schema.create_enum_from_active_enum::().unwrap()), - Statement::from_string( - db_postgres, - r#"CREATE TYPE "my_schema"."mood" AS ENUM ('Happy', 'Sad')"#.to_owned() - ) - ); - - assert_eq!( - db_postgres.build(&schema.create_enum_from_active_enum::().unwrap()), - Statement::from_string( - db_postgres, - r#"CREATE TYPE "my_schema"."priority" AS ENUM ('Low', 'High')"#.to_owned() - ) - ); - } } diff --git a/tests/common/features/mod.rs b/tests/common/features/mod.rs index 151bc58aa1..b9e5e4a7f7 100644 --- a/tests/common/features/mod.rs +++ b/tests/common/features/mod.rs @@ -26,7 +26,6 @@ pub mod pi; pub mod repository; pub mod satellite; pub mod schema; -pub mod schema_enum; pub mod sea_orm_active_enums; pub mod self_join; pub mod teas; @@ -58,7 +57,6 @@ pub use metadata::Entity as Metadata; pub use repository::Entity as Repository; pub use satellite::Entity as Satellite; pub use schema::*; -pub use schema_enum::Entity as SchemaEnum; pub use sea_orm_active_enums::*; pub use self_join::Entity as SelfJoin; pub use teas::Entity as Teas; diff --git a/tests/common/features/schema_enum.rs b/tests/common/features/schema_enum.rs deleted file mode 100644 index 9b9484abba..0000000000 --- a/tests/common/features/schema_enum.rs +++ /dev/null @@ -1,17 +0,0 @@ -use super::sea_orm_active_enums::*; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[cfg_attr(feature = "sqlx-postgres", sea_orm(schema_name = "my_schema"))] -#[sea_orm(table_name = "schema_enum")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i32, - pub mood: Option, - pub priority: Option, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/tests/common/features/sea_orm_active_enums.rs b/tests/common/features/sea_orm_active_enums.rs index 60a5e19cea..87d510dbda 100644 --- a/tests/common/features/sea_orm_active_enums.rs +++ b/tests/common/features/sea_orm_active_enums.rs @@ -66,31 +66,3 @@ pub enum DisplayTea { #[sea_orm(string_value = "BreakfastTea", display_value = "Breakfast")] BreakfastTea, } - -#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)] -#[sea_orm( - rs_type = "Enum", - db_type = "Enum", - enum_name = "mood", - schema_name = "my_schema" -)] -pub enum Mood { - #[sea_orm(string_value = "Happy")] - Happy, - #[sea_orm(string_value = "Sad")] - Sad, -} - -#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)] -#[sea_orm( - rs_type = "String", - db_type = "Enum", - enum_name = "priority", - schema_name = "my_schema" -)] -pub enum Priority { - #[sea_orm(string_value = "Low")] - Low, - #[sea_orm(string_value = "High")] - High, -}