diff --git a/Cargo.toml b/Cargo.toml index 72d8e3f4f2..494d845398 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,7 +70,7 @@ sea-query = { version = "=1.0.0-rc.33", default-features = false, features = [ "backend-sqlite", "sea-orm", ] } -sea-query-sqlx = { version = "=0.8.0-rc.14", default-features = false, optional = true } +sea-query-sqlx = { version = "=0.8.0-rc.15", default-features = false, optional = true } sea-schema = { version = "0.17.0-rc.15", default-features = false, features = [ "discovery", "writer", @@ -164,6 +164,7 @@ sqlx-mysql = [ ] sqlx-postgres = [ "sqlx-dep", + "sea-orm-macros/sqlx-postgres", "sea-query-sqlx/sqlx-postgres", "postgres-array", "sea-schema?/sqlx-postgres", diff --git a/examples/actix_example/Cargo.toml b/examples/actix_example/Cargo.toml index 3fb8a62b61..3257d5c38d 100644 --- a/examples/actix_example/Cargo.toml +++ b/examples/actix_example/Cargo.toml @@ -11,3 +11,5 @@ members = [".", "api", "entity", "migration"] [dependencies] actix-example-api = { path = "api" } + +[patch.crates-io] diff --git a/examples/axum_example/Cargo.toml b/examples/axum_example/Cargo.toml index e7781d9abb..c72f623fe6 100644 --- a/examples/axum_example/Cargo.toml +++ b/examples/axum_example/Cargo.toml @@ -11,3 +11,5 @@ members = [".", "api", "entity", "migration"] [dependencies] axum-example-api = { path = "api" } + +[patch.crates-io] diff --git a/examples/basic/Cargo.toml b/examples/basic/Cargo.toml index 8bd8f1a779..03bcaf2945 100644 --- a/examples/basic/Cargo.toml +++ b/examples/basic/Cargo.toml @@ -16,3 +16,5 @@ sea-orm = { path = "../../", features = [ ] } serde_json = { version = "1" } tokio = { version = "1", features = ["full"] } + +[patch.crates-io] diff --git a/examples/graphql_example/Cargo.toml b/examples/graphql_example/Cargo.toml index b2a1bd2fed..a056b5539c 100644 --- a/examples/graphql_example/Cargo.toml +++ b/examples/graphql_example/Cargo.toml @@ -12,3 +12,5 @@ members = [".", "api", "entity", "migration"] [dependencies] graphql-example-api = { path = "api" } + +[patch.crates-io] diff --git a/examples/jsonrpsee_example/Cargo.toml b/examples/jsonrpsee_example/Cargo.toml index 4c7e717ad4..1ecf4913c2 100644 --- a/examples/jsonrpsee_example/Cargo.toml +++ b/examples/jsonrpsee_example/Cargo.toml @@ -11,3 +11,5 @@ members = [".", "api", "entity", "migration"] [dependencies] jsonrpsee-example-api = { path = "api" } + +[patch.crates-io] diff --git a/examples/parquet_example/Cargo.toml b/examples/parquet_example/Cargo.toml index a17e0d93f2..969da8fb19 100644 --- a/examples/parquet_example/Cargo.toml +++ b/examples/parquet_example/Cargo.toml @@ -24,3 +24,5 @@ features = [ "with-rust_decimal", ] path = "../../" + +[patch.crates-io] diff --git a/examples/poem_example/Cargo.toml b/examples/poem_example/Cargo.toml index eb124fb2b1..b8620db36d 100644 --- a/examples/poem_example/Cargo.toml +++ b/examples/poem_example/Cargo.toml @@ -10,3 +10,5 @@ members = [".", "api", "entity", "migration"] [dependencies] poem-example-api = { path = "api" } + +[patch.crates-io] diff --git a/examples/proxy_gluesql_example/Cargo.toml b/examples/proxy_gluesql_example/Cargo.toml index fbd68b9cd1..6acbbda557 100644 --- a/examples/proxy_gluesql_example/Cargo.toml +++ b/examples/proxy_gluesql_example/Cargo.toml @@ -31,3 +31,5 @@ sqlparser = "0.40" [dev-dependencies] smol = { version = "1.2" } smol-potat = { version = "1.1" } + +[patch.crates-io] diff --git a/examples/quickstart/Cargo.toml b/examples/quickstart/Cargo.toml index ea3dfba255..ed856e6dc5 100644 --- a/examples/quickstart/Cargo.toml +++ b/examples/quickstart/Cargo.toml @@ -22,3 +22,5 @@ features = [ "schema-sync", ] path = "../../" + +[patch.crates-io] diff --git a/examples/rocket_example/Cargo.toml b/examples/rocket_example/Cargo.toml index 119c09a629..1d192cb532 100644 --- a/examples/rocket_example/Cargo.toml +++ b/examples/rocket_example/Cargo.toml @@ -11,3 +11,5 @@ members = [".", "api", "entity", "migration"] [dependencies] rocket-example-api = { path = "api" } + +[patch.crates-io] diff --git a/examples/rocket_okapi_example/Cargo.toml b/examples/rocket_okapi_example/Cargo.toml index c8aa9f321d..6cabcfc863 100644 --- a/examples/rocket_okapi_example/Cargo.toml +++ b/examples/rocket_okapi_example/Cargo.toml @@ -14,3 +14,5 @@ members = [".", "api", "service", "entity", "migration", "dto"] [dependencies] rocket-example-api = { path = "api" } + +[patch.crates-io] diff --git a/examples/salvo_example/Cargo.toml b/examples/salvo_example/Cargo.toml index befaf17bb2..9f91bfea25 100644 --- a/examples/salvo_example/Cargo.toml +++ b/examples/salvo_example/Cargo.toml @@ -10,3 +10,5 @@ members = [".", "api", "entity", "migration"] [dependencies] salvo-example-api = { path = "api" } + +[patch.crates-io] diff --git a/examples/seaography_example/graphql/Cargo.toml b/examples/seaography_example/graphql/Cargo.toml index 6ce606efce..6b24ada16f 100644 --- a/examples/seaography_example/graphql/Cargo.toml +++ b/examples/seaography_example/graphql/Cargo.toml @@ -24,3 +24,6 @@ serde_json = { version = "1.0.103" } [workspace] members = [] + +[patch.crates-io] +# sea-query = { path = "../sea-query" } diff --git a/examples/seaography_example/migration/Cargo.toml b/examples/seaography_example/migration/Cargo.toml index 6b1574c923..955c593fce 100644 --- a/examples/seaography_example/migration/Cargo.toml +++ b/examples/seaography_example/migration/Cargo.toml @@ -25,3 +25,5 @@ features = [ ] path = "../../../sea-orm-migration" # remove this line in your own project version = "~2.0.0-rc.37" # sea-orm-migration version + +[patch.crates-io] diff --git a/examples/tonic_example/Cargo.toml b/examples/tonic_example/Cargo.toml index f2233dac1d..8724486271 100644 --- a/examples/tonic_example/Cargo.toml +++ b/examples/tonic_example/Cargo.toml @@ -22,3 +22,5 @@ path = "./src/server.rs" [[bin]] name = "client" path = "./src/client.rs" + +[patch.crates-io] diff --git a/sea-orm-codegen/src/entity/active_enum.rs b/sea-orm-codegen/src/entity/active_enum.rs index 86c0efe26d..e049bf161c 100644 --- a/sea-orm-codegen/src/entity/active_enum.rs +++ b/sea-orm-codegen/src/entity/active_enum.rs @@ -85,7 +85,7 @@ impl ActiveEnum { } else { quote! { #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum #copy_derive #serde_derive #extra_derives)] - #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = #enum_name)] + #[sea_orm(rs_type = "Enum", db_type = "Enum", enum_name = #enum_name)] #extra_attributes pub enum #enum_iden { #( @@ -125,7 +125,7 @@ mod tests { .to_string(), quote!( #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Copy)] - #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "media_type")] + #[sea_orm(rs_type = "Enum", db_type = "Enum", enum_name = "media_type")] pub enum MediaType { #[sea_orm(string_value = "")] __EmptyString, @@ -168,7 +168,7 @@ mod tests { .to_string(), quote!( #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Copy)] - #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "media_type")] + #[sea_orm(rs_type = "Enum", db_type = "Enum", enum_name = "media_type")] pub enum MediaType { #[sea_orm(string_value = "UNKNOWN")] Unknown, @@ -225,7 +225,7 @@ mod tests { fn build_generated_enum() -> String { quote!( #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Copy, specta :: Type, ts_rs :: TS)] - #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "media_type")] + #[sea_orm(rs_type = "Enum", db_type = "Enum", enum_name = "media_type")] pub enum MediaType { #[sea_orm(string_value = "UNKNOWN")] Unknown, @@ -257,11 +257,7 @@ mod tests { .to_string(), quote!( #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Copy)] - #[sea_orm( - rs_type = "String", - db_type = "Enum", - enum_name = "coinflip_result_type" - )] + #[sea_orm(rs_type = "Enum", db_type = "Enum", enum_name = "coinflip_result_type")] #[serde(rename_all = "camelCase")] pub enum CoinflipResultType { #[sea_orm(string_value = "HEADS")] @@ -290,11 +286,7 @@ mod tests { .to_string(), quote!( #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Copy)] - #[sea_orm( - rs_type = "String", - db_type = "Enum", - enum_name = "coinflip_result_type" - )] + #[sea_orm(rs_type = "Enum", db_type = "Enum", enum_name = "coinflip_result_type")] #[serde(rename_all = "camelCase")] #[ts(export)] pub enum CoinflipResultType { @@ -341,7 +333,7 @@ mod tests { .to_string(), quote!( #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Copy)] - #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "ty")] + #[sea_orm(rs_type = "Enum", db_type = "Enum", enum_name = "ty")] pub enum Ty { #[sea_orm(string_value = "Question")] Question, diff --git a/sea-orm-macros/Cargo.toml b/sea-orm-macros/Cargo.toml index 2f4dbe744a..74a2b6b871 100644 --- a/sea-orm-macros/Cargo.toml +++ b/sea-orm-macros/Cargo.toml @@ -47,6 +47,7 @@ derive = ["bae"] entity-registry = [] postgres-array = [] seaography = ["proc-macro-crate"] +sqlx-postgres = [] strum = [] with-arrow = [] with-json = [] diff --git a/sea-orm-macros/src/derives/active_enum.rs b/sea-orm-macros/src/derives/active_enum.rs index 1049d2c519..5f5cefe385 100644 --- a/sea-orm-macros/src/derives/active_enum.rs +++ b/sea-orm-macros/src/derives/active_enum.rs @@ -8,13 +8,110 @@ use syn::{Expr, Lit, LitInt, LitStr, UnOp, parse}; struct ActiveEnum { ident: syn::Ident, enum_name: String, - rs_type: TokenStream, - db_type: TokenStream, + rs_type: RsType, + db_type: DbType, is_string: bool, variants: Vec, + variant_idents: Vec, + variant_values: Vec, rename_all: Option, } +enum RsType { + String, + Enum, + Other(TokenStream), +} + +impl RsType { + fn from_attr( + ident_span: proc_macro2::Span, + rs_type: Option, + db_type: &DbType, + ) -> Result { + if db_type.is_enum() { + match rs_type.as_deref() { + None => Ok(RsType::Enum), + Some(value) => RsType::from_database_enum_attr_value(value).ok_or_else(|| { + Error::TT(quote_spanned! { + ident_span => compile_error!("`db_type = \"Enum\"` only supports `rs_type = \"String\"` or `rs_type = \"Enum\"` (or omit `rs_type`)"); + }) + }), + } + } else { + let rs_type = match rs_type { + Some(rs_type) => rs_type, + None => { + return Err(Error::TT(quote_spanned! { + ident_span => compile_error!("Missing macro attribute `rs_type`"); + })); + } + }; + + if rs_type == "Enum" { + return Err(Error::TT(quote_spanned! { + ident_span => compile_error!("`rs_type = \"Enum\"` requires `db_type = \"Enum\"`"); + })); + } + + RsType::from_str(&rs_type).map_err(Error::Syn) + } + } + + fn from_str(value: &str) -> syn::Result { + Ok(Self::Other(syn::parse_str::(value)?)) + } + + fn from_database_enum_attr_value(value: &str) -> Option { + match value { + "Enum" => Some(Self::Enum), + "String" => Some(Self::String), + _ => None, + } + } +} + +impl quote::ToTokens for RsType { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + RsType::String => tokens.extend(quote! { String }), + RsType::Enum => tokens.extend(quote! { sea_orm::sea_query::Enum }), + RsType::Other(rs_type) => tokens.extend(rs_type.clone()), + } + } +} + +enum DbType { + Enum, + Other(TokenStream), +} + +impl DbType { + fn from_attr(ident_span: proc_macro2::Span, db_type: Option) -> Result { + let db_type = match db_type { + Some(db_type) => db_type, + None => { + return Err(Error::TT(quote_spanned! { + ident_span => compile_error!("Missing macro attribute `db_type`"); + })); + } + }; + + DbType::from_str(&db_type).map_err(Error::Syn) + } + + fn from_str(value: &str) -> syn::Result { + match value { + "Enum" => Ok(Self::Enum), + _ => Ok(Self::Other(syn::parse_str::(value)?)), + } + } + + fn is_enum(&self) -> bool { + matches!(self, DbType::Enum) + } +} + struct ActiveEnumVariant { ident: syn::Ident, string_value: Option, @@ -34,12 +131,8 @@ impl ActiveEnum { let ident = input.ident; let mut enum_name = ident.to_string().to_upper_camel_case(); - let mut rs_type = Err(Error::TT(quote_spanned! { - ident_span => compile_error!("Missing macro attribute `rs_type`"); - })); - let mut db_type = Err(Error::TT(quote_spanned! { - ident_span => compile_error!("Missing macro attribute `db_type`"); - })); + let mut rs_type = None; + let mut db_type = None; let mut rename_all = None; input @@ -50,24 +143,10 @@ impl ActiveEnum { attr.parse_nested_meta(|meta| { if meta.path.is_ident("rs_type") { let litstr: LitStr = meta.value()?.parse()?; - rs_type = - syn::parse_str::(&litstr.value()).map_err(Error::Syn); + rs_type = Some(litstr.value()); } else if meta.path.is_ident("db_type") { let litstr: LitStr = meta.value()?.parse()?; - let s = litstr.value(); - match s.as_ref() { - "Enum" => { - db_type = Ok(quote! { - Enum { - name: ::name(), - variants: Self::iden_values(), - } - }) - } - _ => { - db_type = syn::parse_str::(&s).map_err(Error::Syn); - } - } + db_type = Some(litstr.value()); } else if meta.path.is_ident("enum_name") { let litstr: LitStr = meta.value()?.parse()?; enum_name = litstr.value(); @@ -84,6 +163,9 @@ impl ActiveEnum { .map_err(Error::Syn) })?; + let db_type = DbType::from_attr(ident_span, db_type)?; + let rs_type = RsType::from_attr(ident_span, rs_type, &db_type)?; + let variant_vec = match input.data { syn::Data::Enum(syn::DataEnum { variants, .. }) => variants, _ => return Err(Error::InputNotEnum), @@ -182,33 +264,11 @@ impl ActiveEnum { }); } - Ok(ActiveEnum { - ident, - enum_name, - rs_type: rs_type?, - db_type: db_type?, - is_string, - variants, - rename_all, - }) - } - - fn expand(&self) -> syn::Result { - let expanded_impl_active_enum = self.impl_active_enum(); - - Ok(expanded_impl_active_enum) - } - - fn impl_active_enum(&self) -> TokenStream { - let Self { - ident, - enum_name, - rs_type, - db_type, - is_string, - variants, - rename_all, - } = self; + if db_type.is_enum() && is_int { + return Err(Error::TT(quote_spanned! { + ident_span => compile_error!("`db_type = \"Enum\"` does not support `num_value` or numeric discriminants"); + })); + } let variant_idents: Vec = variants .iter() @@ -222,26 +282,401 @@ impl ActiveEnum { if let Some(string_value) = &variant.string_value { let string = string_value.value(); - quote! { #string } + Ok(quote! { #string }) } else if let Some(num_value) = &variant.num_value { - quote! { #num_value } - } else if let Some(rename_rule) = variant.rename.or(*rename_all) { + Ok(quote! { #num_value }) + } else if let Some(rename_rule) = variant.rename.or(rename_all) { let variant_ident = variant.ident.convert_case(Some(rename_rule)); - quote! { #variant_ident } + Ok(quote! { #variant_ident }) } else { - quote_spanned! { + Err(Error::TT(quote_spanned! { variant_span => compile_error!("Missing macro attribute, either `string_value`, `num_value` or `rename_all` should be specified"); - } + })) } }) - .collect(); + .collect::>()?; + + Ok(Self { + ident, + enum_name, + rs_type, + db_type, + is_string, + variants, + variant_idents, + variant_values, + rename_all, + }) + } + + fn generate_enum_impls(&self) -> bool { + self.db_type.is_enum() && matches!(self.rs_type, RsType::Enum) + } + + fn to_value_impl(&self) -> TokenStream { + let enum_name = &self.enum_name; + let variant_idents = &self.variant_idents; + let variant_values = &self.variant_values; + + if self.generate_enum_impls() { + quote! { + let value = match self { + #( Self::#variant_idents => #variant_values, )* + }; + sea_orm::sea_query::Enum { + type_name: #enum_name.into(), + value: value.into(), + } + } + } else { + quote! { + match self { + #( Self::#variant_idents => #variant_values, )* + } + .to_owned() + } + } + } + + fn value_type_try_from_impl(&self) -> TokenStream { + if self.generate_enum_impls() { + quote! { + use sea_orm::sea_query::{OptionEnum, Value, ValueTypeErr}; + + match v { + Value::Enum(value) => match value { + OptionEnum::Some(value) => ::try_from_value(value.as_ref()) + .map_err(|_| ValueTypeErr), + OptionEnum::None(_) => Err(ValueTypeErr), + }, + _ => Err(ValueTypeErr), + } + } + } else { + quote! { + use sea_orm::sea_query::{ValueType, ValueTypeErr}; + + let value = <::Value as ValueType>::try_from(v)?; + ::try_from_value(&value).map_err(|_| ValueTypeErr) + } + } + } + + fn nullable_impl(&self) -> TokenStream { + let ident = &self.ident; + let enum_name = &self.enum_name; + let nullable_value_impl = if self.generate_enum_impls() { + quote! { + use sea_orm::sea_query::{OptionEnum, Value}; + Value::Enum(OptionEnum::None(#enum_name.into())) + } + } else { + quote! { + use sea_orm::sea_query; + <::Value as sea_query::Nullable>::null() + } + }; + + quote! { + #[automatically_derived] + #[allow(unexpected_cfgs)] + impl sea_orm::sea_query::Nullable for #ident { + fn null() -> sea_orm::sea_query::Value { + #nullable_value_impl + } + } + } + } + + 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 val = if *is_string { - quote! { v.as_ref() } + let type_name_impl = quote! { stringify!(#ident).to_owned() }; + + let value_type_array_type = if self.generate_enum_impls() { + quote! { + sea_orm::sea_query::ArrayType::Enum(Box::new(#enum_name.into())) + } + } else { + quote! { + <::Value as sea_orm::sea_query::ValueType>::array_type() + } + }; + + let enum_type_name = if self.db_type.is_enum() { + quote! { Some(#enum_name) } + } else { + quote! { None } + }; + + quote! { + #[automatically_derived] + #[allow(unexpected_cfgs)] + impl sea_orm::sea_query::ValueType for #ident { + fn try_from(v: sea_orm::sea_query::Value) -> std::result::Result { + #value_type_try_from_impl + } + + fn type_name() -> String { + #type_name_impl + } + + fn array_type() -> sea_orm::sea_query::ArrayType { + #value_type_array_type + } + + fn column_type() -> sea_orm::sea_query::ColumnType { + ::db_type() + .get_column_type() + .to_owned() + .into() + } + + fn enum_type_name() -> Option<&'static str> { + #enum_type_name + } + } + } + } + + fn try_getable_impl(&self) -> TokenStream { + let ident = &self.ident; + let sqlx_postgres_try_get = if cfg!(feature = "sqlx-postgres") && self.db_type.is_enum() { + quote! { + if let Some(result) = res.try_get_from_sqlx_postgres::(idx) { + return result; + } + } + } else { + quote!() + }; + let try_get_by_impl = { + let enum_name = &self.enum_name; + if self.generate_enum_impls() { + quote! { + #sqlx_postgres_try_get + let value: String = ::try_get_by(res, idx)?; + let value = sea_orm::sea_query::Enum { + type_name: #enum_name.into(), + value: value.into(), + }; + ::try_from_value(&value) + .map_err(sea_orm::TryGetError::DbErr) + } + } else { + quote! { + #sqlx_postgres_try_get + let value = <::Value as sea_orm::TryGetable>::try_get_by(res, idx)?; + ::try_from_value(&value) + .map_err(sea_orm::TryGetError::DbErr) + } + } + }; + + quote! { + #[automatically_derived] + impl sea_orm::TryGetable for #ident { + fn try_get_by(res: &sea_orm::QueryResult, idx: I) -> std::result::Result { + #try_get_by_impl + } + } + } + } + + fn active_enum_impl(&self) -> TokenStream { + let ident = &self.ident; + let enum_name_iden = format_ident!("{}Enum", ident); + let rs_type = &self.rs_type; + let variant_idents = &self.variant_idents; + let variant_values = &self.variant_values; + let to_value_body = self.to_value_impl(); + let column_type = { + match &self.db_type { + DbType::Enum => quote! { + Enum { + name: ::name(), + variants: Self::iden_values(), + } + }, + DbType::Other(db_type) => db_type.clone(), + } + }; + + let val = if self.generate_enum_impls() { + quote! { v.value.as_ref() } + } else if self.is_string { + quote! { <::Value as std::convert::AsRef>::as_ref(v) } } else { quote! { v } }; + quote! { + #[automatically_derived] + impl sea_orm::ActiveEnum for #ident { + type Value = #rs_type; + + type ValueVec = Vec<#rs_type>; + + fn name() -> sea_orm::sea_query::DynIden { + #enum_name_iden.into() + } + + fn to_value(&self) -> ::Value { + #to_value_body + } + + fn try_from_value(v: &::Value) -> std::result::Result { + match #val { + #( #variant_values => Ok(Self::#variant_idents), )* + _ => Err(sea_orm::DbErr::Type(format!( + "unexpected value for {} enum: {}", + stringify!(#ident), + #val + ))), + } + } + + fn db_type() -> sea_orm::ColumnDef { + sea_orm::prelude::ColumnTypeTrait::def(sea_orm::ColumnType::#column_type) + } + } + } + } + + fn convert_impls(&self) -> TokenStream { + let ident = &self.ident; + + if self.generate_enum_impls() { + let enum_name = &self.enum_name; + let variant_idents = &self.variant_idents; + let variant_values = &self.variant_values; + + quote! { + #[automatically_derived] + impl std::convert::From<#ident> for sea_orm::sea_query::Enum { + fn from(source: #ident) -> Self { + let value = match source { + #( #ident::#variant_idents => #variant_values, )* + }; + Self { + type_name: #enum_name.into(), + value: value.into(), + } + } + } + + #[automatically_derived] + impl std::convert::From<#ident> for sea_orm::sea_query::Value { + fn from(source: #ident) -> Self { + let enum_value = sea_orm::sea_query::Enum::from(source); + sea_orm::sea_query::Value::from(enum_value) + } + } + } + } else { + quote! { + #[automatically_derived] + impl std::convert::From<#ident> for sea_orm::sea_query::Value { + fn from(source: #ident) -> Self { + <#ident as sea_orm::ActiveEnum>::to_value(&source).into() + } + } + } + } + } + + fn sqlx_postgres_impl(&self) -> TokenStream { + if !cfg!(feature = "sqlx-postgres") || !self.db_type.is_enum() { + return quote!(); + } + + 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; + + 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) + } + } + + #[automatically_derived] + impl<'r> sea_orm::sqlx::decode::Decode<'r, sea_orm::sqlx::Postgres> for #ident { + fn decode( + value: sea_orm::sqlx::postgres::PgValueRef<'r>, + ) -> std::result::Result< + Self, + std::boxed::Box< + dyn std::error::Error + 'static + std::marker::Send + std::marker::Sync, + >, + > { + let value = <&'r str as sea_orm::sqlx::decode::Decode< + 'r, + sea_orm::sqlx::Postgres, + >>::decode(value)?; + + match value { + #( #variant_values => Ok(Self::#variant_idents), )* + _ => Err(format!("invalid value {:?} for enum {}", value, #ident_s).into()), + } + } + } + + #[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) + } + } + } + } + + fn try_getable_array_impl(&self) -> TokenStream { + let ident = &self.ident; + + if cfg!(feature = "postgres-array") { + let sqlx_postgres_try_get = if cfg!(feature = "sqlx-postgres") && self.db_type.is_enum() + { + quote! { + if let Some(result) = res.try_get_from_sqlx_postgres::, I>(index) { + return result; + } + } + } else { + quote!() + }; + quote!( + #[automatically_derived] + impl sea_orm::TryGetableArray for #ident { + fn try_get_by(res: &sea_orm::QueryResult, index: I) -> std::result::Result, sea_orm::TryGetError> { + #sqlx_postgres_try_get + <::Value as sea_orm::ActiveEnumValue>::try_get_vec_by(res, index)? + .into_iter() + .map(|value| ::try_from_value(&value).map_err(Into::into)) + .collect() + } + } + ) + } else { + quote!() + } + } + + fn expand(&self) -> TokenStream { + let Self { + ident, + enum_name, + variants, + rename_all, + .. + } = self; + let enum_name_iden = format_ident!("{}Enum", ident); let str_variants: Vec = variants @@ -295,6 +730,7 @@ impl ActiveEnum { #[doc = " Generated by sea-orm-macros"] pub fn iden_values() -> Vec { <#enum_variant_iden as sea_orm::strum::IntoEnumIterator>::iter() + // TODO: Use DynIden constructor .map(|v| sea_orm::sea_query::SeaRc::new(v) as sea_orm::sea_query::DynIden) .collect() } @@ -304,7 +740,7 @@ impl ActiveEnum { quote!() }; - let impl_not_u8 = if cfg!(feature = "postgres-array") { + let not_u8_impl = if cfg!(feature = "postgres-array") { quote!( #[automatically_derived] impl sea_orm::sea_query::postgres_array::NotU8 for #ident {} @@ -313,21 +749,13 @@ impl ActiveEnum { quote!() }; - let impl_try_getable_array = if cfg!(feature = "postgres-array") { - quote!( - #[automatically_derived] - impl sea_orm::TryGetableArray for #ident { - fn try_get_by(res: &sea_orm::QueryResult, index: I) -> std::result::Result, sea_orm::TryGetError> { - <::Value as sea_orm::ActiveEnumValue>::try_get_vec_by(res, index)? - .into_iter() - .map(|value| ::try_from_value(&value).map_err(Into::into)) - .collect() - } - } - ) - } else { - quote!() - }; + let value_type_impl = self.value_type_impl(); + let convert_impls = self.convert_impls(); + let nullable_impl = self.nullable_impl(); + let impl_try_getable_array = self.try_getable_array_impl(); + let active_enum_impl = self.active_enum_impl(); + let try_getable_impl = self.try_getable_impl(); + let sqlx_postgres_impl = self.sqlx_postgres_impl(); quote!( #[doc = " Generated by sea-orm-macros"] @@ -343,90 +771,19 @@ impl ActiveEnum { #impl_enum_variant_iden - #[automatically_derived] - impl sea_orm::ActiveEnum for #ident { - type Value = #rs_type; - - type ValueVec = Vec<#rs_type>; - - fn name() -> sea_orm::sea_query::DynIden { - sea_orm::sea_query::SeaRc::new(#enum_name_iden) as sea_orm::sea_query::DynIden - } - - fn to_value(&self) -> ::Value { - match self { - #( Self::#variant_idents => #variant_values, )* - } - .to_owned() - } - - fn try_from_value(v: &::Value) -> std::result::Result { - match #val { - #( #variant_values => Ok(Self::#variant_idents), )* - _ => Err(sea_orm::DbErr::Type(format!( - "unexpected value for {} enum: {}", - stringify!(#ident), - v - ))), - } - } - - fn db_type() -> sea_orm::ColumnDef { - sea_orm::prelude::ColumnTypeTrait::def(sea_orm::ColumnType::#db_type) - } - } + #active_enum_impl #impl_try_getable_array - #[automatically_derived] - #[allow(clippy::from_over_into)] - impl Into for #ident { - fn into(self) -> sea_orm::sea_query::Value { - ::to_value(&self).into() - } - } + #convert_impls - #[automatically_derived] - impl sea_orm::TryGetable for #ident { - fn try_get_by(res: &sea_orm::QueryResult, idx: I) -> std::result::Result { - let value = <::Value as sea_orm::TryGetable>::try_get_by(res, idx)?; - ::try_from_value(&value).map_err(sea_orm::TryGetError::DbErr) - } - } + #try_getable_impl - #[automatically_derived] - impl sea_orm::sea_query::ValueType for #ident { - fn try_from(v: sea_orm::sea_query::Value) -> std::result::Result { - let value = <::Value as sea_orm::sea_query::ValueType>::try_from(v)?; - ::try_from_value(&value).map_err(|_| sea_orm::sea_query::ValueTypeErr) - } + #sqlx_postgres_impl - fn type_name() -> String { - <::Value as sea_orm::sea_query::ValueType>::type_name() - } - - fn array_type() -> sea_orm::sea_query::ArrayType { - <::Value as sea_orm::sea_query::ValueType>::array_type() - } - - fn column_type() -> sea_orm::sea_query::ColumnType { - ::db_type() - .get_column_type() - .to_owned() - .into() - } - - fn enum_type_name() -> Option<&'static str> { - Some(stringify!(#ident)) - } - } + #value_type_impl - #[automatically_derived] - impl sea_orm::sea_query::Nullable for #ident { - fn null() -> sea_orm::sea_query::Value { - <::Value as sea_orm::sea_query::Nullable>::null() - } - } + #nullable_impl #[automatically_derived] impl sea_orm::IntoActiveValue<#ident> for #ident { @@ -435,7 +792,7 @@ impl ActiveEnum { } } - #impl_not_u8 + #not_u8_impl ) } } @@ -444,7 +801,7 @@ pub fn expand_derive_active_enum(input: syn::DeriveInput) -> syn::Result model.expand(), + Ok(model) => Ok(model.expand()), Err(Error::InputNotEnum) => Ok(quote_spanned! { ident_span => compile_error!("you can only derive ActiveEnum on enums"); }), diff --git a/sea-orm-macros/src/lib.rs b/sea-orm-macros/src/lib.rs index 14b2879ae0..81996306b1 100644 --- a/sea-orm-macros/src/lib.rs +++ b/sea-orm-macros/src/lib.rs @@ -12,6 +12,14 @@ mod strum; mod raw_sql; +#[proc_macro] +pub fn raw_sql(input: TokenStream) -> TokenStream { + match raw_sql::expand(input) { + Ok(token_stream) => token_stream.into(), + Err(e) => e.to_compile_error().into(), + } +} + /// Create an Entity /// /// ### Usage @@ -675,15 +683,18 @@ pub fn derive_active_model_behavior(input: TokenStream) -> TokenStream { /// All macro attributes listed below have to be annotated in the form of `#[sea_orm(attr = value)]`. /// /// - For enum -/// - `rs_type`: Define `ActiveEnum::Value` +/// - `rs_type`: Define `ActiveEnum::Value` for non-native enums (`db_type != "Enum"`) /// - Possible values: `String`, `i8`, `i16`, `i32`, `i64`, `u8`, `u16`, `u32`, `u64` /// - Note that value has to be passed as string, i.e. `rs_type = "i8"` /// - `db_type`: Define `ColumnType` returned by `ActiveEnum::db_type()` -/// - Possible values: all available enum variants of `ColumnType`, e.g. `String(StringLen::None)`, `String(StringLen::N(1))`, `Integer` +/// - Possible values: all available enum variants of `ColumnType`, e.g. `Enum`, `String(StringLen::None)`, `String(StringLen::N(1))`, `Integer` /// - Note that value has to be passed as string, i.e. `db_type = "Integer"` /// - `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"` +/// - 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. /// /// - For enum variant /// - `string_value` or `num_value`: @@ -1307,14 +1318,6 @@ pub fn derive_arrow_schema(input: TokenStream) -> TokenStream { } } -#[proc_macro] -pub fn raw_sql(input: TokenStream) -> TokenStream { - match raw_sql::expand(input) { - Ok(token_stream) => token_stream.into(), - Err(e) => e.to_compile_error().into(), - } -} - #[cfg(feature = "derive")] #[proc_macro_attribute] pub fn sea_orm_model(_attr: TokenStream, input: TokenStream) -> TokenStream { diff --git a/sea-orm-macros/tests/derive_active_enum_test.rs b/sea-orm-macros/tests/derive_active_enum_test.rs index f685191cc7..fd1ee2c7eb 100644 --- a/sea-orm-macros/tests/derive_active_enum_test.rs +++ b/sea-orm-macros/tests/derive_active_enum_test.rs @@ -1,3 +1,4 @@ +use sea_orm::sea_query::{ArrayType, Value, ValueType}; use sea_orm::{ActiveEnum, entity::prelude::StringLen}; use sea_orm_macros::{DeriveActiveEnum, EnumIter}; @@ -34,15 +35,21 @@ enum TestEnum { CustomStringValue, } +#[derive(Debug, EnumIter, DeriveActiveEnum, Eq, PartialEq)] +#[sea_orm(db_type = "Enum", enum_name = "test_enum", rename_all = "camelCase")] +enum TestRenameAllWithoutCasesEnum { + HelloWorld, +} + #[derive(Debug, EnumIter, DeriveActiveEnum, Eq, PartialEq)] #[sea_orm( - rs_type = "String", + rs_type = "Enum", db_type = "Enum", enum_name = "test_enum", rename_all = "camelCase" )] -enum TestRenameAllWithoutCasesEnum { - HelloWorld, +enum TestEnumWithEnumValue { + DefaultVariant, } #[derive(Debug, EnumIter, DeriveActiveEnum, Eq, PartialEq)] @@ -148,7 +155,67 @@ fn derive_active_enum_value_2() { assert_eq!(TestEnum3::HelloWorld.to_value(), "hello_world"); assert_eq!( - TestRenameAllWithoutCasesEnum::HelloWorld.to_value(), + TestRenameAllWithoutCasesEnum::HelloWorld + .to_value() + .value + .as_ref(), "helloWorld" ); } + +#[test] +fn derive_database_enum_value_type() { + assert_eq!(TestEnum::enum_type_name(), Some("test_enum")); + assert_eq!(TestEnum::array_type(), ArrayType::String); + assert_eq!( + Value::from(TestEnum::DefaultVariant), + Value::String(Some(String::from("defaultVariant"))) + ); + assert_eq!( + ::try_from(Value::String(Some(String::from("defaultVariant")))) + .unwrap(), + TestEnum::DefaultVariant + ); +} + +#[test] +fn derive_database_enum_rs_type_enum() { + let value = TestEnumWithEnumValue::DefaultVariant.to_value(); + assert_eq!(value.value.as_ref(), "defaultVariant"); + assert_eq!( + ::try_from_value(&value), + Ok(TestEnumWithEnumValue::DefaultVariant) + ); + let value: Value = value.into(); + assert_eq!( + value, + Value::Enum(sea_orm::sea_query::OptionEnum::Some(Box::new( + sea_orm::sea_query::Enum { + type_name: String::from("test_enum").into(), + value: "defaultVariant".into(), + }, + ))) + ); +} + +#[test] +fn derive_database_enum_default_rs_type_enum() { + let value = TestRenameAllWithoutCasesEnum::HelloWorld.to_value(); + assert_eq!(value.value.as_ref(), "helloWorld"); + let value: Value = value.into(); + assert_eq!( + value, + Value::Enum(sea_orm::sea_query::OptionEnum::Some(Box::new( + sea_orm::sea_query::Enum { + type_name: String::from("test_enum").into(), + value: "helloWorld".into(), + }, + ))) + ); +} + +#[test] +fn derive_non_database_enum_value_type() { + assert_eq!(TestEnum2::enum_type_name(), None); + assert_eq!(TestEnum2::array_type(), ArrayType::String); +} diff --git a/sea-orm-sync/examples/parquet_example/Cargo.toml b/sea-orm-sync/examples/parquet_example/Cargo.toml index 8d7b772310..a336cd99f5 100644 --- a/sea-orm-sync/examples/parquet_example/Cargo.toml +++ b/sea-orm-sync/examples/parquet_example/Cargo.toml @@ -22,3 +22,5 @@ features = [ "with-rust_decimal", ] path = "../../" + +[patch.crates-io] diff --git a/sea-orm-sync/src/dynamic/model.rs b/sea-orm-sync/src/dynamic/model.rs index fe3fe30518..874caaa375 100644 --- a/sea-orm-sync/src/dynamic/model.rs +++ b/sea-orm-sync/src/dynamic/model.rs @@ -82,6 +82,18 @@ fn try_get(res: &QueryResult, pre: &str, col: &str, ty: &ArrayType) -> Result Value::Float(res.try_get(pre, col)?), ArrayType::Double => Value::Double(res.try_get(pre, col)?), ArrayType::String => Value::String(res.try_get(pre, col)?), + ArrayType::Enum(type_name) => { + let type_name = type_name.as_ref().clone(); + match res.try_get::>(pre, col)? { + Some(value) => { + Value::Enum(sea_query::OptionEnum::Some(Box::new(sea_query::Enum { + type_name, + value: value.into(), + }))) + } + None => Value::Enum(sea_query::OptionEnum::None(type_name)), + } + } ArrayType::Char => return Err(DbErr::Type("Unsupported type: char".into())), ArrayType::Bytes => Value::Bytes(res.try_get(pre, col)?), diff --git a/sea-orm-sync/src/entity/active_enum.rs b/sea-orm-sync/src/entity/active_enum.rs index d35fa79ec5..f91c2fb1ba 100644 --- a/sea-orm-sync/src/entity/active_enum.rs +++ b/sea-orm-sync/src/entity/active_enum.rs @@ -135,7 +135,11 @@ pub trait ActiveEnum: Sized + Iterable { /// Construct a enum expression with casting fn as_enum(&self) -> SimpleExpr { - Expr::val(Self::to_value(self)).as_enum(Self::name()) + let value: Value = Self::to_value(self).into(); + match value { + Value::Enum(_) => Expr::val(value), + _ => Expr::val(value).as_enum(Self::name()), + } } /// Get the name of all enum variants @@ -197,6 +201,39 @@ impl_active_enum_value_with_pg_array!(i16); impl_active_enum_value_with_pg_array!(i32); impl_active_enum_value_with_pg_array!(i64); +impl TryGetable for sea_query::Enum { + fn try_get_by(res: &QueryResult, idx: I) -> Result { + let value: String = ::try_get_by(res, idx)?; + Ok(Self { + type_name: "".into(), + value: value.into(), + }) + } +} + +impl ActiveEnumValue for sea_query::Enum { + fn try_get_vec_by(_res: &QueryResult, _index: I) -> Result, TryGetError> { + #[cfg(feature = "postgres-array")] + { + let values: Vec = as TryGetable>::try_get_by(_res, _index)?; + Ok(values + .into_iter() + .map(|value| Self { + type_name: "".into(), + value: value.into(), + }) + .collect()) + } + #[cfg(not(feature = "postgres-array"))] + { + Err(TryGetError::DbErr(DbErr::BackendNotSupported { + db: "Postgres", + ctx: "ActiveEnumValue::try_get_vec_by (`postgres-array` not enabled)", + })) + } + } +} + impl TryFromU64 for T where T: ActiveEnum, diff --git a/sea-orm-sync/src/entity/column.rs b/sea-orm-sync/src/entity/column.rs index fdc38a88f5..37777011d9 100644 --- a/sea-orm-sync/src/entity/column.rs +++ b/sea-orm-sync/src/entity/column.rs @@ -617,6 +617,17 @@ pub(crate) fn select_enum_as(col: Expr, _: DynIden, col_type: &ColumnType) -> Ex } pub(crate) fn save_enum_as(col: Expr, enum_name: DynIden, col_type: &ColumnType) -> Expr { + if matches!(col, Expr::Value(Value::Enum(_))) { + return col; + } + #[cfg(feature = "postgres-array")] + if matches!( + col, + Expr::Value(Value::Array(sea_query::ArrayType::Enum(_), _)) + ) { + return col; + } + let type_name = match col_type { ColumnType::Array(_) => format!("{enum_name}[]").into_iden(), _ => enum_name, diff --git a/src/dynamic/model.rs b/src/dynamic/model.rs index fe3fe30518..874caaa375 100644 --- a/src/dynamic/model.rs +++ b/src/dynamic/model.rs @@ -82,6 +82,18 @@ fn try_get(res: &QueryResult, pre: &str, col: &str, ty: &ArrayType) -> Result Value::Float(res.try_get(pre, col)?), ArrayType::Double => Value::Double(res.try_get(pre, col)?), ArrayType::String => Value::String(res.try_get(pre, col)?), + ArrayType::Enum(type_name) => { + let type_name = type_name.as_ref().clone(); + match res.try_get::>(pre, col)? { + Some(value) => { + Value::Enum(sea_query::OptionEnum::Some(Box::new(sea_query::Enum { + type_name, + value: value.into(), + }))) + } + None => Value::Enum(sea_query::OptionEnum::None(type_name)), + } + } ArrayType::Char => return Err(DbErr::Type("Unsupported type: char".into())), ArrayType::Bytes => Value::Bytes(res.try_get(pre, col)?), diff --git a/src/entity/active_enum.rs b/src/entity/active_enum.rs index d35fa79ec5..f6aabf4fc5 100644 --- a/src/entity/active_enum.rs +++ b/src/entity/active_enum.rs @@ -135,7 +135,11 @@ pub trait ActiveEnum: Sized + Iterable { /// Construct a enum expression with casting fn as_enum(&self) -> SimpleExpr { - Expr::val(Self::to_value(self)).as_enum(Self::name()) + let value: Value = Self::to_value(self).into(); + match value { + Value::Enum(_) => Expr::val(value), + _ => Expr::val(value).as_enum(Self::name()), + } } /// Get the name of all enum variants @@ -197,6 +201,43 @@ impl_active_enum_value_with_pg_array!(i16); impl_active_enum_value_with_pg_array!(i32); impl_active_enum_value_with_pg_array!(i64); +impl TryGetable for sea_query::Enum { + fn try_get_by(res: &QueryResult, idx: I) -> Result { + let value: String = ::try_get_by(res, idx)?; + Ok(Self { + // `DeriveActiveEnum` overwrites `type_name` when constructing values for queries, but we + // can't reliably recover the enum type name from `QueryResult`. Keeping it empty may + // still cause issues if this value is later reused to build SQL (e.g. missing casts). + type_name: "".into(), + value: value.into(), + }) + } +} + +impl ActiveEnumValue for sea_query::Enum { + fn try_get_vec_by(_res: &QueryResult, _index: I) -> Result, TryGetError> { + #[cfg(feature = "postgres-array")] + { + let values: Vec = as TryGetable>::try_get_by(_res, _index)?; + Ok(values + .into_iter() + .map(|value| Self { + // See comment in `TryGetable for sea_query::Enum` about empty `type_name`. + type_name: "".into(), + value: value.into(), + }) + .collect()) + } + #[cfg(not(feature = "postgres-array"))] + { + Err(TryGetError::DbErr(DbErr::BackendNotSupported { + db: "Postgres", + ctx: "ActiveEnumValue::try_get_vec_by (`postgres-array` not enabled)", + })) + } + } +} + impl TryFromU64 for T where T: ActiveEnum, diff --git a/src/entity/column.rs b/src/entity/column.rs index fdc38a88f5..37777011d9 100644 --- a/src/entity/column.rs +++ b/src/entity/column.rs @@ -617,6 +617,17 @@ pub(crate) fn select_enum_as(col: Expr, _: DynIden, col_type: &ColumnType) -> Ex } pub(crate) fn save_enum_as(col: Expr, enum_name: DynIden, col_type: &ColumnType) -> Expr { + if matches!(col, Expr::Value(Value::Enum(_))) { + return col; + } + #[cfg(feature = "postgres-array")] + if matches!( + col, + Expr::Value(Value::Array(sea_query::ArrayType::Enum(_), _)) + ) { + return col; + } + let type_name = match col_type { ColumnType::Array(_) => format!("{enum_name}[]").into_iden(), _ => enum_name, diff --git a/src/executor/query.rs b/src/executor/query.rs index 7e5989d07b..eaf4ae39ab 100644 --- a/src/executor/query.rs +++ b/src/executor/query.rs @@ -17,7 +17,7 @@ use crate::debug_print; #[cfg(feature = "sqlx-dep")] use crate::driver::*; #[cfg(feature = "sqlx-dep")] -use sqlx::Row; +use sqlx::{Row, TypeInfo, ValueRef}; /// Defines the result of a query operation on a Model #[derive(Debug)] @@ -81,6 +81,37 @@ impl From for TryGetError { // QueryResult // impl QueryResult { + #[doc(hidden)] + #[cfg(feature = "sqlx-postgres")] + pub fn try_get_from_sqlx_postgres(&self, idx: I) -> Option> + where + T: sqlx::Type + for<'r> sqlx::Decode<'r, sqlx::Postgres>, + I: ColIdx, + { + match &self.row { + QueryResultRow::SqlxPostgres(row) => { + let value = match row.try_get_raw(idx.as_sqlx_postgres_index()) { + Ok(value) => value, + Err(err) => return Some(Err(sqlx_error_to_query_err(err).into())), + }; + + if !value.is_null() { + let ty = value.type_info(); + if !ty.is_null() && !T::compatible(&ty) { + return None; + } + } + + Some( + row.try_get::, _>(idx.as_sqlx_postgres_index()) + .map_err(|e| sqlx_error_to_query_err(e).into()) + .and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx))), + ) + } + _ => None, + } + } + /// Get a value from the query result with an ColIdx pub fn try_get_by(&self, index: I) -> Result where diff --git a/tests/active_enum_from_query_result_tests.rs b/tests/active_enum_from_query_result_tests.rs new file mode 100644 index 0000000000..5f0a40bfe5 --- /dev/null +++ b/tests/active_enum_from_query_result_tests.rs @@ -0,0 +1,222 @@ +#![allow(unused_imports, dead_code)] + +pub mod common; + +pub use common::{TestContext, features::*, setup::*}; +use sea_orm::{ + ConnectionTrait, DatabaseConnection, DbErr, FromQueryResult, + entity::*, + sea_query::{Expr, ExprTrait, Query}, +}; +use sea_orm::{DeriveActiveEnum, EnumIter}; + +#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)] +#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "tea")] +enum TeaString { + #[sea_orm(string_value = "EverydayTea")] + EverydayTea, + #[sea_orm(string_value = "BreakfastTea")] + BreakfastTea, + #[sea_orm(string_value = "AfternoonTea")] + AfternoonTea, +} + +#[sea_orm_macros::test] +#[cfg(feature = "sqlx-postgres")] +async fn from_query_result_with_native_pg_enum() -> Result<(), DbErr> { + let ctx = TestContext::new("from_query_result_native_pg_enum").await; + let db = &ctx.db; + + create_tea_enum(db).await?; + create_active_enum_table(db).await?; + + active_enum::ActiveModel { + id: Set(1), + category: Set(None), + color: Set(None), + tea: Set(Some(Tea::EverydayTea)), + } + .insert(db) + .await?; + + let query = Query::select() + .column(active_enum::Column::Id) + .column(active_enum::Column::Tea) + .from(active_enum::Entity) + .to_owned(); + + #[derive(Debug, PartialEq, FromQueryResult)] + struct ActiveEnumResult { + pub id: i32, + pub tea: Option, + } + + let rows = db.query_all(&query).await?; + let results: Vec = rows + .iter() + .map(|r| ActiveEnumResult::from_query_result(r, "")) + .collect::, _>>()?; + + assert_eq!( + results, + vec![ActiveEnumResult { + id: 1, + tea: Some(Tea::EverydayTea), + }] + ); + + ctx.delete().await; + Ok(()) +} + +#[sea_orm_macros::test] +#[cfg(feature = "sqlx-postgres")] +async fn from_query_result_with_native_pg_enum_rs_type_string() -> Result<(), DbErr> { + let ctx = TestContext::new("from_query_result_native_pg_enum_rs_type_string").await; + let db = &ctx.db; + + create_tea_enum(db).await?; + create_active_enum_table(db).await?; + + active_enum::ActiveModel { + id: Set(1), + category: Set(None), + color: Set(None), + tea: Set(Some(Tea::BreakfastTea)), + } + .insert(db) + .await?; + + let query = Query::select() + .column(active_enum::Column::Id) + .column(active_enum::Column::Tea) + .from(active_enum::Entity) + .to_owned(); + + #[derive(Debug, PartialEq, FromQueryResult)] + struct ActiveEnumStringResult { + pub id: i32, + pub tea: Option, + } + + let rows = db.query_all(&query).await?; + let results: Vec = rows + .iter() + .map(|r| ActiveEnumStringResult::from_query_result(r, "")) + .collect::, _>>()?; + + assert_eq!( + results, + vec![ActiveEnumStringResult { + id: 1, + tea: Some(TeaString::BreakfastTea), + }] + ); + + ctx.delete().await; + Ok(()) +} + +#[sea_orm_macros::test] +#[cfg(feature = "sqlx-postgres")] +async fn from_raw_sql_into_model_with_native_pg_enum() -> Result<(), DbErr> { + let ctx = TestContext::new("from_raw_sql_native_pg_enum").await; + let db = &ctx.db; + + create_tea_enum(db).await?; + create_active_enum_table(db).await?; + + active_enum::ActiveModel { + id: Set(1), + category: Set(None), + color: Set(None), + tea: Set(Some(Tea::EverydayTea)), + } + .insert(db) + .await?; + + use sea_orm::{DbBackend, Statement}; + use sea_query::PostgresQueryBuilder; + + let query = Query::select() + .column(active_enum::Column::Id) + .column(active_enum::Column::Tea) + .from(active_enum::Entity) + .to_owned(); + + #[derive(Debug, PartialEq, FromQueryResult)] + struct ActiveEnumResult { + pub id: i32, + pub tea: Option, + } + + let stmt = Statement::from_string(DbBackend::Postgres, query.to_string(PostgresQueryBuilder)); + + assert_eq!( + ActiveEnumResult { + id: 1, + tea: Some(Tea::EverydayTea), + }, + active_enum::Entity::find() + .from_raw_sql(stmt) + .into_model::() + .one(db) + .await? + .unwrap() + ); + + ctx.delete().await; + Ok(()) +} + +#[sea_orm_macros::test] +#[cfg(feature = "sqlx-postgres")] +async fn from_query_result_with_cast_works() -> Result<(), DbErr> { + let ctx = TestContext::new("from_query_result_cast_workaround").await; + let db = &ctx.db; + + create_tea_enum(db).await?; + create_active_enum_table(db).await?; + + active_enum::ActiveModel { + id: Set(1), + category: Set(None), + color: Set(None), + tea: Set(Some(Tea::BreakfastTea)), + } + .insert(db) + .await?; + + use sea_orm::sea_query::Alias; + let query = Query::select() + .column(active_enum::Column::Id) + .expr_as( + Expr::col(active_enum::Column::Tea).cast_as(Alias::new("TEXT")), + Alias::new("tea"), + ) + .from(active_enum::Entity) + .to_owned(); + + #[derive(Debug, PartialEq, FromQueryResult)] + struct ActiveEnumResult { + pub id: i32, + pub tea: Option, + } + + let rows = db.query_all(&query).await?; + let results: Vec = rows + .iter() + .map(|r| ActiveEnumResult::from_query_result(r, "")) + .collect::, _>>()?; + + assert_eq!( + results, + vec![ActiveEnumResult { + id: 1, + tea: Some(Tea::BreakfastTea), + }] + ); + + ctx.delete().await; + Ok(()) +} diff --git a/tests/active_enum_tests.rs b/tests/active_enum_tests.rs index df7b4b1b71..cdf684860d 100644 --- a/tests/active_enum_tests.rs +++ b/tests/active_enum_tests.rs @@ -199,7 +199,7 @@ pub async fn insert_active_enum(db: &DatabaseConnection) -> Result<(), DbErr> { r#""active_enum"."color","#, r#"CAST("active_enum"."tea" AS "text")"#, r#"FROM "public"."active_enum""#, - r#"WHERE "active_enum"."tea" IN (CAST('EverydayTea' AS "tea"), CAST('BreakfastTea' AS "tea"))"#, + r#"WHERE "active_enum"."tea" IN ('EverydayTea'::"tea", 'BreakfastTea'::"tea")"#, ] .join(" ") ); @@ -234,7 +234,7 @@ pub async fn insert_active_enum(db: &DatabaseConnection) -> Result<(), DbErr> { r#"CAST("active_enum"."tea" AS "text")"#, r#"FROM "public"."active_enum""#, r#"WHERE "active_enum"."tea" IS NOT NULL"#, - r#"AND "active_enum"."tea" NOT IN (CAST('BreakfastTea' AS "tea"))"#, + r#"AND "active_enum"."tea" NOT IN ('BreakfastTea'::"tea")"#, ] .join(" ") ); diff --git a/tests/common/features/sea_orm_active_enums.rs b/tests/common/features/sea_orm_active_enums.rs index 7e967a1eed..87d510dbda 100644 --- a/tests/common/features/sea_orm_active_enums.rs +++ b/tests/common/features/sea_orm_active_enums.rs @@ -18,8 +18,10 @@ pub enum Color { White, } +// Changed to rs_type to "Enum" for test showcase +// Works the same with rs_type "String" #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, DeriveDisplay)] -#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "tea")] +#[sea_orm(rs_type = "Enum", db_type = "Enum", enum_name = "tea")] pub enum Tea { #[sea_orm(string_value = "EverydayTea")] EverydayTea,