diff --git a/Cargo.toml b/Cargo.toml index 24ffe12ea..1c38fd08a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -231,4 +231,7 @@ with-uuid = ["uuid", "sea-query/with-uuid", "sea-query-sqlx?/with-uuid"] # This allows us to develop using a local version of sea-query [patch.crates-io] # sea-query = { path = "../sea-query" } +sea-query = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } +sea-query-sqlx = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } +sea-query-rusqlite = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } # sea-query = { git = "https://github.com/SeaQL/sea-query", branch = "master" } diff --git a/examples/actix_example/Cargo.toml b/examples/actix_example/Cargo.toml index 3fb8a62b6..b400ee64e 100644 --- a/examples/actix_example/Cargo.toml +++ b/examples/actix_example/Cargo.toml @@ -11,3 +11,7 @@ members = [".", "api", "entity", "migration"] [dependencies] actix-example-api = { path = "api" } + +[patch.crates-io] +sea-query = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } +sea-query-sqlx = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } diff --git a/examples/axum_example/Cargo.toml b/examples/axum_example/Cargo.toml index e7781d9ab..1c1b27eb1 100644 --- a/examples/axum_example/Cargo.toml +++ b/examples/axum_example/Cargo.toml @@ -11,3 +11,7 @@ members = [".", "api", "entity", "migration"] [dependencies] axum-example-api = { path = "api" } + +[patch.crates-io] +sea-query = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } +sea-query-sqlx = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } diff --git a/examples/basic/Cargo.toml b/examples/basic/Cargo.toml index 8bd8f1a77..d74b0bb2c 100644 --- a/examples/basic/Cargo.toml +++ b/examples/basic/Cargo.toml @@ -16,3 +16,7 @@ sea-orm = { path = "../../", features = [ ] } serde_json = { version = "1" } tokio = { version = "1", features = ["full"] } + +[patch.crates-io] +sea-query = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } +sea-query-sqlx = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } diff --git a/examples/graphql_example/Cargo.toml b/examples/graphql_example/Cargo.toml index b2a1bd2fe..cb423350f 100644 --- a/examples/graphql_example/Cargo.toml +++ b/examples/graphql_example/Cargo.toml @@ -12,3 +12,7 @@ members = [".", "api", "entity", "migration"] [dependencies] graphql-example-api = { path = "api" } + +[patch.crates-io] +sea-query = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } +sea-query-sqlx = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } diff --git a/examples/jsonrpsee_example/Cargo.toml b/examples/jsonrpsee_example/Cargo.toml index 4c7e717ad..b0e6040a1 100644 --- a/examples/jsonrpsee_example/Cargo.toml +++ b/examples/jsonrpsee_example/Cargo.toml @@ -11,3 +11,7 @@ members = [".", "api", "entity", "migration"] [dependencies] jsonrpsee-example-api = { path = "api" } + +[patch.crates-io] +sea-query = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } +sea-query-sqlx = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } diff --git a/examples/parquet_example/Cargo.toml b/examples/parquet_example/Cargo.toml index 3c128c179..9800e5915 100644 --- a/examples/parquet_example/Cargo.toml +++ b/examples/parquet_example/Cargo.toml @@ -24,3 +24,7 @@ features = [ "with-rust_decimal", ] path = "../../" + +[patch.crates-io] +sea-query = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } +sea-query-sqlx = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } diff --git a/examples/poem_example/Cargo.toml b/examples/poem_example/Cargo.toml index eb124fb2b..a7662dafb 100644 --- a/examples/poem_example/Cargo.toml +++ b/examples/poem_example/Cargo.toml @@ -10,3 +10,7 @@ members = [".", "api", "entity", "migration"] [dependencies] poem-example-api = { path = "api" } + +[patch.crates-io] +sea-query = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } +sea-query-sqlx = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } diff --git a/examples/proxy_cloudflare_worker_example/Cargo.toml b/examples/proxy_cloudflare_worker_example/Cargo.toml index 2f6ebb328..95d655756 100644 --- a/examples/proxy_cloudflare_worker_example/Cargo.toml +++ b/examples/proxy_cloudflare_worker_example/Cargo.toml @@ -51,3 +51,5 @@ sea-orm = { path = "../../", default-features = false, features = [ [patch.crates-io] # https://github.com/cloudflare/workers-rs/pull/591 worker = { git = "https://github.com/cloudflare/workers-rs.git", rev = "ff2e6a0fd58b7e7b4b7651aba46e04067597eb03" } +sea-query = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } +sea-query-sqlx = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } diff --git a/examples/proxy_gluesql_example/Cargo.toml b/examples/proxy_gluesql_example/Cargo.toml index fbd68b9cd..8d145a947 100644 --- a/examples/proxy_gluesql_example/Cargo.toml +++ b/examples/proxy_gluesql_example/Cargo.toml @@ -31,3 +31,7 @@ sqlparser = "0.40" [dev-dependencies] smol = { version = "1.2" } smol-potat = { version = "1.1" } + +[patch.crates-io] +sea-query = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } +sea-query-sqlx = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } diff --git a/examples/quickstart/Cargo.toml b/examples/quickstart/Cargo.toml index ea3dfba25..a75651a52 100644 --- a/examples/quickstart/Cargo.toml +++ b/examples/quickstart/Cargo.toml @@ -22,3 +22,7 @@ features = [ "schema-sync", ] path = "../../" + +[patch.crates-io] +sea-query = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } +sea-query-sqlx = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } diff --git a/examples/rocket_example/Cargo.toml b/examples/rocket_example/Cargo.toml index 119c09a62..89be9a55f 100644 --- a/examples/rocket_example/Cargo.toml +++ b/examples/rocket_example/Cargo.toml @@ -11,3 +11,7 @@ members = [".", "api", "entity", "migration"] [dependencies] rocket-example-api = { path = "api" } + +[patch.crates-io] +sea-query = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } +sea-query-sqlx = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } diff --git a/examples/rocket_okapi_example/Cargo.toml b/examples/rocket_okapi_example/Cargo.toml index c8aa9f321..d01f912ff 100644 --- a/examples/rocket_okapi_example/Cargo.toml +++ b/examples/rocket_okapi_example/Cargo.toml @@ -14,3 +14,7 @@ members = [".", "api", "service", "entity", "migration", "dto"] [dependencies] rocket-example-api = { path = "api" } + +[patch.crates-io] +sea-query = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } +sea-query-sqlx = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } diff --git a/examples/salvo_example/Cargo.toml b/examples/salvo_example/Cargo.toml index befaf17bb..3df7b189b 100644 --- a/examples/salvo_example/Cargo.toml +++ b/examples/salvo_example/Cargo.toml @@ -10,3 +10,7 @@ members = [".", "api", "entity", "migration"] [dependencies] salvo-example-api = { path = "api" } + +[patch.crates-io] +sea-query = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } +sea-query-sqlx = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } diff --git a/examples/seaography_example/graphql/Cargo.toml b/examples/seaography_example/graphql/Cargo.toml index 6ce606efc..5fce2dbed 100644 --- a/examples/seaography_example/graphql/Cargo.toml +++ b/examples/seaography_example/graphql/Cargo.toml @@ -24,3 +24,9 @@ serde_json = { version = "1.0.103" } [workspace] members = [] + +[patch.crates-io] +# sea-query = { path = "../sea-query" } +sea-query = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } +sea-query-sqlx = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } +sea-query-rusqlite = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } diff --git a/examples/seaography_example/migration/Cargo.toml b/examples/seaography_example/migration/Cargo.toml index 1dffa9d8b..d088b04a0 100644 --- a/examples/seaography_example/migration/Cargo.toml +++ b/examples/seaography_example/migration/Cargo.toml @@ -25,3 +25,8 @@ features = [ ] path = "../../../sea-orm-migration" # remove this line in your own project version = "~2.0.0-rc.34" # sea-orm-migration version + +[patch.crates-io] +sea-query = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } +sea-query-sqlx = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } +sea-query-rusqlite = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } diff --git a/examples/tonic_example/Cargo.toml b/examples/tonic_example/Cargo.toml index f2233dac1..f2a743d34 100644 --- a/examples/tonic_example/Cargo.toml +++ b/examples/tonic_example/Cargo.toml @@ -22,3 +22,7 @@ path = "./src/server.rs" [[bin]] name = "client" path = "./src/client.rs" + +[patch.crates-io] +sea-query = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } +sea-query-sqlx = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } diff --git a/sea-orm-cli/Cargo.toml b/sea-orm-cli/Cargo.toml index daefee670..db8adbb0b 100644 --- a/sea-orm-cli/Cargo.toml +++ b/sea-orm-cli/Cargo.toml @@ -129,4 +129,6 @@ runtime-tokio-rustls = [ # This allows us to develop using an overridden version of sea-query [patch.crates-io] # sea-query = { path = "../sea-query" } +sea-query = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } +sea-query-sqlx = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } # sea-query = { git = "https://github.com/SeaQL/sea-query", branch = "master" } diff --git a/sea-orm-macros/src/derives/active_enum.rs b/sea-orm-macros/src/derives/active_enum.rs index 1049d2c51..f746c2062 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,329 @@ 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) + } - let val = if *is_string { - quote! { v.as_ref() } + 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 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 try_get_by_impl = { + let enum_name = &self.enum_name; + if self.generate_enum_impls() { + quote! { + 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! { + 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 try_getable_array_impl(&self) -> TokenStream { + let ident = &self.ident; + + 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!() + } + } + + 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 +658,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 +668,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 +677,12 @@ 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(); quote!( #[doc = " Generated by sea-orm-macros"] @@ -343,90 +698,17 @@ 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() - } - } - - #[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) - } - } - - #[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) - } - - fn type_name() -> String { - <::Value as sea_orm::sea_query::ValueType>::type_name() - } + #convert_impls - fn array_type() -> sea_orm::sea_query::ArrayType { - <::Value as sea_orm::sea_query::ValueType>::array_type() - } + #try_getable_impl - fn column_type() -> sea_orm::sea_query::ColumnType { - ::db_type() - .get_column_type() - .to_owned() - .into() - } + #value_type_impl - fn enum_type_name() -> Option<&'static str> { - Some(stringify!(#ident)) - } - } - - #[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 +717,7 @@ impl ActiveEnum { } } - #impl_not_u8 + #not_u8_impl ) } } @@ -444,7 +726,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 d085c067e..58e1080e2 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 @@ -549,15 +557,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`: @@ -1181,14 +1192,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 f685191cc..fd1ee2c7e 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-migration/Cargo.toml b/sea-orm-migration/Cargo.toml index 652275da3..f07bc2169 100644 --- a/sea-orm-migration/Cargo.toml +++ b/sea-orm-migration/Cargo.toml @@ -95,3 +95,5 @@ with-uuid = ["sea-orm/with-uuid"] [patch.crates-io] # sea-query = { path = "../sea-query" } +sea-query = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } +sea-query-sqlx = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } diff --git a/sea-orm-sync/Cargo.toml b/sea-orm-sync/Cargo.toml index 9b8465d4e..177503ad4 100644 --- a/sea-orm-sync/Cargo.toml +++ b/sea-orm-sync/Cargo.toml @@ -188,3 +188,6 @@ with-uuid = ["uuid", "sea-query/with-uuid", "sea-query-rusqlite?/with-uuid"] [patch.crates-io] # sea-query = { path = "../sea-query" } +sea-query = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } +sea-query-sqlx = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } +sea-query-rusqlite = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } diff --git a/sea-orm-sync/examples/parquet_example/Cargo.toml b/sea-orm-sync/examples/parquet_example/Cargo.toml index 08a200e52..4a0a940d6 100644 --- a/sea-orm-sync/examples/parquet_example/Cargo.toml +++ b/sea-orm-sync/examples/parquet_example/Cargo.toml @@ -22,3 +22,7 @@ features = [ "with-rust_decimal", ] path = "../../" + +[patch.crates-io] +sea-query = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } +sea-query-rusqlite = { git = "https://github.com/Huliiiiii/sea-query", branch = "enum" } diff --git a/sea-orm-sync/src/dynamic/model.rs b/sea-orm-sync/src/dynamic/model.rs index 576e3a920..bd506cc51 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/ARROW.md b/sea-orm-sync/src/entity/ARROW.md new file mode 100644 index 000000000..a06a46b34 --- /dev/null +++ b/sea-orm-sync/src/entity/ARROW.md @@ -0,0 +1,270 @@ +# Apache Arrow Type Support in SeaORM + +This document explains Apache Arrow's decimal and timestamp formats and their integration with SeaORM. + +## Arrow Decimal Types Overview + +Apache Arrow provides fixed-point decimal types for representing precise numeric values with a specific precision and scale: + +### 1. **Decimal128** +- **Storage**: 128-bit (16 bytes) fixed-point decimal +- **Precision**: Up to 38 decimal digits +- **Format**: `DataType::Decimal128(precision, scale)` + - `precision` (u8): Total number of decimal digits (1-38) + - `scale` (i8): Number of digits after decimal point (can be negative) +- **Use case**: Standard precision decimals (e.g., financial calculations, prices) +- **Example**: `Decimal128(10, 2)` represents numbers like `12345678.90` + +### 2. **Decimal256** +- **Storage**: 256-bit (32 bytes) fixed-point decimal +- **Precision**: Up to 76 decimal digits +- **Format**: `DataType::Decimal256(precision, scale)` + - `precision` (u8): Total number of decimal digits (1-76) + - `scale` (i8): Number of digits after decimal point (can be negative) +- **Use case**: High-precision scientific calculations, very large numbers +- **Example**: `Decimal256(76, 20)` for extreme precision requirements + +### Precision vs Scale + +- **Precision**: Total number of significant digits (before + after decimal point) +- **Scale**: Number of digits after the decimal point + - Positive scale: digits after decimal (e.g., scale=2 → `123.45`) + - Zero scale: integer (e.g., scale=0 → `12345`) + - Negative scale: multiplier (e.g., scale=-2 → `12300` stored as `123`) + +**Examples**: +``` +Decimal128(10, 2) → Can store: -99999999.99 to 99999999.99 +Decimal128(5, 0) → Can store: -99999 to 99999 (integers) +Decimal128(10, 4) → Can store: -999999.9999 to 999999.9999 +Decimal256(38, 10) → High precision: up to 28 digits before, 10 after decimal +``` + +## SeaORM Decimal Support + +SeaORM supports two Rust decimal libraries: + +### 1. **rust_decimal** (feature: `with-rust_decimal`) +- Type: `rust_decimal::Decimal` +- Precision: 28-29 significant digits +- Scale: 0-28 +- Storage: 128-bit +- Value type: `Value::Decimal` +- Best for: Most business applications, financial calculations + +### 2. **bigdecimal** (feature: `with-bigdecimal`) +- Type: `bigdecimal::BigDecimal` +- Precision: Arbitrary (limited by memory) +- Scale: Arbitrary +- Storage: Variable (uses BigInt internally) +- Value type: `Value::BigDecimal` +- Best for: Arbitrary precision requirements, scientific computing + +## Arrow → SeaORM Mapping + +### Decimal128Array → rust_decimal::Decimal +- **When**: Feature `with-rust_decimal` is enabled +- **Limitation**: Precision ≤ 28, Scale ≤ 28 +- **Conversion**: Direct mapping using `i128` to `Decimal::from_i128_with_scale()` +- **Column Type**: `ColumnType::Decimal(Some((precision, scale)))` + +### Decimal128Array → bigdecimal::BigDecimal +- **When**: Feature `with-bigdecimal` is enabled (fallback if rust_decimal fails or not available) +- **Limitation**: None (arbitrary precision) +- **Conversion**: Convert via string representation or BigInt +- **Column Type**: `ColumnType::Decimal(Some((precision, scale)))` or `ColumnType::Money(_)` + +### Decimal256Array → bigdecimal::BigDecimal +- **When**: Feature `with-bigdecimal` is enabled +- **Required**: BigDecimal for precision > 38 +- **Conversion**: Convert via byte array to BigInt, then apply scale +- **Column Type**: `ColumnType::Decimal(Some((precision, scale)))` + +## Implementation Strategy + +1. **Decimal128Array**: + - Try `with-rust_decimal` first (if precision/scale fit) + - Fallback to `with-bigdecimal` if needed + - Return type error if neither feature is enabled + +2. **Decimal256Array**: + - Requires `with-bigdecimal` (rust_decimal can't handle precision > 28) + - Convert byte representation to BigInt + - Apply scale to create BigDecimal + +3. **Null Handling**: + - Return `Value::Decimal(None)` or `Value::BigDecimal(None)` for null values + +--- + +# Apache Arrow Timestamp Types Support + +Arrow provides several temporal types for representing dates, times, and timestamps with varying precision. + +## Arrow Temporal Types Overview + +### Date Types + +#### 1. **Date32** +- **Storage**: 32-bit signed integer +- **Unit**: Days since Unix epoch (1970-01-01) +- **Format**: `DataType::Date32` +- **Range**: Approximately ±5.8 million years from epoch +- **Use case**: Calendar dates without time component + +#### 2. **Date64** +- **Storage**: 64-bit signed integer +- **Unit**: Milliseconds since Unix epoch +- **Format**: `DataType::Date64` +- **Range**: Much larger than Date32 +- **Use case**: Dates with millisecond precision (though time is typically zeroed) + +### Time Types + +#### 1. **Time32** +- **Storage**: 32-bit signed integer +- **Units**: Second or Millisecond +- **Variants**: + - `Time32(TimeUnit::Second)` - seconds since midnight + - `Time32(TimeUnit::Millisecond)` - milliseconds since midnight +- **Range**: 0 to 86,399 seconds (00:00:00 to 23:59:59) +- **Use case**: Time of day without date + +#### 2. **Time64** +- **Storage**: 64-bit signed integer +- **Units**: Microsecond or Nanosecond +- **Variants**: + - `Time64(TimeUnit::Microsecond)` - microseconds since midnight + - `Time64(TimeUnit::Nanosecond)` - nanoseconds since midnight +- **Range**: 0 to 86,399,999,999,999 nanoseconds +- **Use case**: High-precision time of day + +### Timestamp Types + +**Timestamp** types represent absolute points in time with optional timezone. + +- **Storage**: 64-bit signed integer +- **Units**: Second, Millisecond, Microsecond, or Nanosecond +- **Timezone**: Optional timezone string (e.g., "UTC", "America/New_York") +- **Format**: `Timestamp(TimeUnit, Option)` + +**Variants**: +```rust +DataType::Timestamp(TimeUnit::Second, None) // No timezone +DataType::Timestamp(TimeUnit::Millisecond, None) // No timezone +DataType::Timestamp(TimeUnit::Microsecond, None) // No timezone +DataType::Timestamp(TimeUnit::Nanosecond, None) // No timezone + +DataType::Timestamp(TimeUnit::Second, Some("UTC".into())) // With timezone +DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())) // With timezone +DataType::Timestamp(TimeUnit::Nanosecond, Some("UTC".into())) // With timezone +``` + +**TimeUnit Precision**: +- **Second**: 1 second precision (1,000,000,000 ns) +- **Millisecond**: 1 millisecond precision (1,000,000 ns) +- **Microsecond**: 1 microsecond precision (1,000 ns) +- **Nanosecond**: 1 nanosecond precision (highest) + +## SeaORM Temporal Type Support + +SeaORM supports two Rust datetime libraries for temporal types: + +### 1. **chrono** (feature: `with-chrono`) - Preferred +- Type Mappings: + - `chrono::NaiveDate` - Date without timezone + - `chrono::NaiveTime` - Time without date/timezone + - `chrono::NaiveDateTime` - DateTime without timezone + - `chrono::DateTime` - DateTime with UTC timezone +- Value types: + - `Value::ChronoDate(Option)` + - `Value::ChronoTime(Option)` + - `Value::ChronoDateTime(Option)` + - `Value::ChronoDateTimeUtc(Option>)` +- Best for: Most applications needing date/time support + +### 2. **time** (feature: `with-time`) - Alternative +- Type Mappings: + - `time::Date` - Date without timezone + - `time::Time` - Time without date/timezone + - `time::PrimitiveDateTime` - DateTime without timezone + - `time::OffsetDateTime` - DateTime with timezone offset +- Value types: + - `Value::TimeDate(Option)` + - `Value::TimeTime(Option