From 744e06322201e0e6195d3641109980b7a7b66589 Mon Sep 17 00:00:00 2001 From: ForzenString <964413011@qq.com> Date: Thu, 13 Apr 2023 19:15:04 +0800 Subject: [PATCH] add `SelectColumns` only modify select columns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit add `PartialModelTrait` for a part of model re-export `PartialModelTrait` cargo fmt add fn`into_partial_model` on `Select`&`SelectTwo` add `DerivePartialModel` to impl `PartialModel` add macro `DerivePartialModel` to `sea-orm` disambiguate `SelectColumns` function fix macro error cargo fmt && cargo clippy move `SelectColumns` from helper.rs to traits.rs cargo fmt Reduce nest hell of load attribute argument fetch ✅ test `DerivePartialModel` input parse `DerivePartialModel` not derive with generic fix `DerivePartialModel` code generate error remove unused use cargo fmt add `into_partial_model` for `SelectTwoMany` --- sea-orm-macros/src/derives/mod.rs | 2 + sea-orm-macros/src/derives/partial_model.rs | 269 ++++++++++++++++++++ sea-orm-macros/src/lib.rs | 75 ++++++ src/entity/mod.rs | 2 + src/entity/partial_model.rs | 7 + src/entity/prelude.rs | 4 +- src/executor/select.rs | 48 +++- src/lib.rs | 4 +- src/query/traits.rs | 35 ++- tests/derive_tests.rs | 128 +++++++--- 10 files changed, 530 insertions(+), 44 deletions(-) create mode 100644 sea-orm-macros/src/derives/partial_model.rs create mode 100644 src/entity/partial_model.rs diff --git a/sea-orm-macros/src/derives/mod.rs b/sea-orm-macros/src/derives/mod.rs index a3c71fe1..5ec025da 100644 --- a/sea-orm-macros/src/derives/mod.rs +++ b/sea-orm-macros/src/derives/mod.rs @@ -9,6 +9,7 @@ mod from_query_result; mod into_active_model; mod migration; mod model; +mod partial_model; mod primary_key; mod relation; mod try_getable_from_json; @@ -24,6 +25,7 @@ pub use from_query_result::*; pub use into_active_model::*; pub use migration::*; pub use model::*; +pub use partial_model::*; pub use primary_key::*; pub use relation::*; pub use try_getable_from_json::*; diff --git a/sea-orm-macros/src/derives/partial_model.rs b/sea-orm-macros/src/derives/partial_model.rs new file mode 100644 index 00000000..7bd033cd --- /dev/null +++ b/sea-orm-macros/src/derives/partial_model.rs @@ -0,0 +1,269 @@ +use heck::ToUpperCamelCase; +use proc_macro2::Span; +use proc_macro2::TokenStream; +use quote::format_ident; +use quote::quote; +use quote::quote_spanned; +use syn::punctuated::Punctuated; +use syn::spanned::Spanned; +use syn::token::Comma; +use syn::Expr; + +use syn::Meta; + +use self::util::GetAsKVMeta; + +#[derive(Debug)] +enum Error { + InputNotStruct, + EntityNotSpecific, + NotSupportGeneric(Span), + BothFromColAndFromExpr(Span), + Syn(syn::Error), +} +#[derive(Debug, PartialEq, Eq)] +enum ColumnAs { + /// column in the model + Col(syn::Ident), + /// alias from a column in model + ColAlias { col: syn::Ident, field: String }, + /// from a expr + Expr { expr: syn::Expr, field_name: String }, +} + +struct DerivePartialModel { + entity_ident: Option, + ident: syn::Ident, + fields: Vec, +} + +impl DerivePartialModel { + fn new(input: syn::DeriveInput) -> Result { + if !input.generics.params.is_empty() { + return Err(Error::NotSupportGeneric(input.generics.params.span())); + } + + let syn::Data::Struct(syn::DataStruct{fields:syn::Fields::Named(syn::FieldsNamed{named:fields,..}),..},..)= input.data else{ + return Err(Error::InputNotStruct); + }; + + let mut entity_ident = None; + + for attr in input.attrs.iter() { + if let Some(ident) = attr.path.get_ident() { + if ident != "sea_orm" { + continue; + } + } else { + continue; + } + + if let Ok(list) = attr.parse_args_with(Punctuated::::parse_terminated) { + for meta in list { + entity_ident = meta + .get_as_kv("entity") + .map(|s| syn::parse_str::(&s).map_err(Error::Syn)) + .transpose()?; + } + } + } + + let mut column_as_list = Vec::with_capacity(fields.len()); + + for field in fields { + let field_span = field.span(); + + let mut from_col = None; + let mut from_expr = None; + + for attr in field.attrs.iter() { + if !attr.path.is_ident("sea_orm") { + continue; + } + + if let Ok(list) = attr.parse_args_with(Punctuated::::parse_terminated) + { + for meta in list.iter() { + from_col = meta + .get_as_kv("from_col") + .map(|s| format_ident!("{}", s.to_upper_camel_case())); + from_expr = meta + .get_as_kv("from_expr") + .map(|s| syn::parse_str::(&s).map_err(Error::Syn)) + .transpose()?; + } + } + } + + let field_name = field.ident.unwrap(); + + let col_as = match (from_col, from_expr) { + (None, None) => { + if entity_ident.is_none() { + return Err(Error::EntityNotSpecific); + } + ColumnAs::Col(format_ident!( + "{}", + field_name.to_string().to_upper_camel_case() + )) + } + (None, Some(expr)) => ColumnAs::Expr { + expr, + field_name: field_name.to_string(), + }, + (Some(col), None) => { + if entity_ident.is_none() { + return Err(Error::EntityNotSpecific); + } + + let field = field_name.to_string(); + ColumnAs::ColAlias { col, field } + } + (Some(_), Some(_)) => return Err(Error::BothFromColAndFromExpr(field_span)), + }; + column_as_list.push(col_as); + } + + Ok(Self { + entity_ident, + ident: input.ident, + fields: column_as_list, + }) + } + + fn expand(&self) -> syn::Result { + Ok(self.impl_partial_model_trait()) + } + + fn impl_partial_model_trait(&self) -> TokenStream { + let select_ident = format_ident!("select"); + let DerivePartialModel { + entity_ident, + ident, + fields, + } = self; + let select_col_code_gen = fields.iter().map(|col_as| match col_as { + ColumnAs::Col(ident) => { + let entity = entity_ident.as_ref().unwrap(); + let col_value = quote!( <#entity as sea_orm::EntityTrait>::Column:: #ident); + quote!(let #select_ident = sea_orm::SelectColumns::select_column(#select_ident, #col_value);) + }, + ColumnAs::ColAlias { col, field } => { + let entity = entity_ident.as_ref().unwrap(); + let col_value = quote!( <#entity as sea_orm::EntityTrait>::Column:: #col); + quote!(let #select_ident = sea_orm::SelectColumns::select_column_as(#select_ident, #col_value, #field);) + }, + ColumnAs::Expr { expr, field_name } => { + quote!(let #select_ident = sea_orm::SelectColumns::select_column_as(#select_ident, #expr, #field_name);) + }, + }); + + quote! { + #[automatically_derived] + impl sea_orm::PartialModelTrait for #ident{ + fn select_cols(#select_ident: S) -> S{ + #(#select_col_code_gen)* + #select_ident + } + } + } + } +} + +pub fn expand_derive_partial_model(input: syn::DeriveInput) -> syn::Result { + let ident_span = input.ident.span(); + + match DerivePartialModel::new(input) { + Ok(partial_model) => partial_model.expand(), + Err(Error::NotSupportGeneric(span)) => Ok(quote_spanned! { + span => compile_error!("you can only derive `DerivePartialModel` on named struct"); + }), + Err(Error::BothFromColAndFromExpr(span)) => Ok(quote_spanned! { + span => compile_error!("you can only use one of `from_col` or `from_expr`"); + }), + Err(Error::EntityNotSpecific) => Ok(quote_spanned! { + ident_span => compile_error!("you need specific which entity you are using") + }), + Err(Error::InputNotStruct) => Ok(quote_spanned! { + ident_span => compile_error!("you can only derive `DerivePartialModel` on named struct"); + }), + Err(Error::Syn(err)) => Err(err), + } +} + +mod util { + use syn::{Lit, Meta, MetaNameValue}; + + pub(super) trait GetAsKVMeta { + fn get_as_kv(&self, k: &str) -> Option; + } + + impl GetAsKVMeta for Meta { + fn get_as_kv(&self, k: &str) -> Option { + let Meta::NameValue(MetaNameValue{path, lit:Lit::Str(lit), ..}) = self else { + return None; + }; + + if path.is_ident(k) { + Some(lit.value()) + } else { + None + } + } + } +} + +#[cfg(test)] +mod test { + use quote::format_ident; + use syn::DeriveInput; + + use crate::derives::partial_model::ColumnAs; + + use super::DerivePartialModel; + + #[cfg(test)] + type StdResult = Result>; + + #[cfg(test)] + const CODE_SNIPPET: &str = r#" +#[sea_orm(entity = "Entity")] +struct PartialModel{ + default_field: i32, + #[sea_orm(from_col = "bar")] + alias_field: i32, + #[sea_orm(from_expr = "Expr::val(1).add(1)")] + expr_field : i32 +} +"#; + #[test] + fn test_load_macro_input() -> StdResult<()> { + let input = syn::parse_str::(CODE_SNIPPET)?; + + let middle = DerivePartialModel::new(input).unwrap(); + + assert_eq!(middle.entity_ident, Some(format_ident!("Entity"))); + assert_eq!(middle.ident, format_ident!("PartialModel")); + assert_eq!(middle.fields.len(), 3); + assert_eq!( + middle.fields[0], + ColumnAs::Col(format_ident!("DefaultField")) + ); + assert_eq!( + middle.fields[1], + ColumnAs::ColAlias { + col: format_ident!("Bar"), + field: "alias_field".to_string() + }, + ); + assert_eq!( + middle.fields[2], + ColumnAs::Expr { + expr: syn::parse_str("Expr::val(1).add(1)").unwrap(), + field_name: "expr_field".to_string() + } + ); + + Ok(()) + } +} diff --git a/sea-orm-macros/src/lib.rs b/sea-orm-macros/src/lib.rs index cd8d04a2..23fb5aee 100644 --- a/sea-orm-macros/src/lib.rs +++ b/sea-orm-macros/src/lib.rs @@ -1,6 +1,7 @@ extern crate proc_macro; use proc_macro::TokenStream; + use syn::{parse_macro_input, DeriveInput, Error}; #[cfg(feature = "derive")] @@ -677,6 +678,80 @@ pub fn derive_from_json_query_result(input: TokenStream) -> TokenStream { } } +/// The DerivePartialModel derive macro will implement `sea_orm::PartialModelTrait` for simplify partial model queries. +/// +/// ## Usage +/// +/// ```rust +/// use sea_orm::{entity::prelude::*, sea_query::Expr, DerivePartialModel, FromQueryResult}; +/// use serde::{Deserialize, Serialize}; +/// +/// #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)] +/// #[sea_orm(table_name = "posts")] +/// pub struct Model { +/// #[sea_orm(primary_key)] +/// pub id: i32, +/// pub title: String, +/// #[sea_orm(column_type = "Text")] +/// pub text: String, +/// } +/// # #[derive(Copy, Clone, Debug, EnumIter)] +/// # pub enum Relation {} +/// # +/// # impl RelationTrait for Relation { +/// # fn def(&self) -> RelationDef { +/// # panic!("No Relation"); +/// # } +/// # } +/// # +/// # impl ActiveModelBehavior for ActiveModel {} +/// +/// #[derive(Debug, FromQueryResult, DerivePartialModel)] +/// #[sea_orm(entity = "Entity")] +/// struct SelectResult { +/// title: String, +/// #[sea_orm(from_col = "text")] +/// content: String, +/// #[sea_orm(from_expr = "Expr::val(1).add(1)")] +/// sum: i32, +/// } +/// ``` +/// +/// If all fields in the partial model is `from_expr`, the `entity` can be ignore. +/// ``` +/// use sea_orm::{entity::prelude::*, sea_query::Expr, DerivePartialModel, FromQueryResult}; +/// +/// #[derive(Debug, FromQueryResult, DerivePartialModel)] +/// struct SelectResult { +/// #[sea_orm(from_expr = "Expr::val(1).add(1)")] +/// sum: i32, +/// } +/// ``` +/// +/// A field cannot have attributes `from_col` and `from_expr` at the same time. +/// Or, it will result in a compile error. +/// +/// ```compile_fail +/// use sea_orm::{entity::prelude::*, FromQueryResult, DerivePartialModel, sea_query::Expr}; +/// +/// #[derive(Debug, FromQueryResult, DerivePartialModel)] +/// #[sea_orm(entity = "Entity")] +/// struct SelectResult { +/// #[sea_orm(from_expr = "Expr::val(1).add(1)", from_col = "foo")] +/// sum: i32 +/// } +/// ``` +#[cfg(feature = "derive")] +#[proc_macro_derive(DerivePartialModel, attributes(sea_orm))] +pub fn derive_partial_model(input: TokenStream) -> TokenStream { + let derive_input = parse_macro_input!(input); + + match derives::expand_derive_partial_model(derive_input) { + Ok(token_stream) => token_stream.into(), + Err(e) => e.to_compile_error().into(), + } +} + #[doc(hidden)] #[cfg(feature = "derive")] #[proc_macro_attribute] diff --git a/src/entity/mod.rs b/src/entity/mod.rs index 54a289b4..92913c48 100644 --- a/src/entity/mod.rs +++ b/src/entity/mod.rs @@ -103,6 +103,7 @@ mod column; mod identity; mod link; mod model; +mod partial_model; /// Re-export common types from the entity pub mod prelude; mod primary_key; @@ -115,6 +116,7 @@ pub use column::*; pub use identity::*; pub use link::*; pub use model::*; +pub use partial_model::*; // pub use prelude::*; pub use primary_key::*; pub use relation::*; diff --git a/src/entity/partial_model.rs b/src/entity/partial_model.rs new file mode 100644 index 00000000..9da4f1d2 --- /dev/null +++ b/src/entity/partial_model.rs @@ -0,0 +1,7 @@ +use crate::{FromQueryResult, SelectColumns}; + +/// A trait for a part of [Model](super::model::ModelTrait) +pub trait PartialModelTrait: FromQueryResult { + /// Select specific columns this [PartialModel] needs + fn select_cols(select: S) -> S; +} diff --git a/src/entity/prelude.rs b/src/entity/prelude.rs index 85cd3742..993b4c00 100644 --- a/src/entity/prelude.rs +++ b/src/entity/prelude.rs @@ -2,8 +2,8 @@ pub use crate::{ error::*, sea_query::BlobSize, ActiveEnum, ActiveModelBehavior, ActiveModelTrait, ColumnDef, ColumnTrait, ColumnType, ColumnTypeTrait, ConnectionTrait, CursorTrait, DatabaseConnection, DbConn, EntityName, EntityTrait, EnumIter, ForeignKeyAction, Iden, IdenStatic, Linked, - LoaderTrait, ModelTrait, PaginatorTrait, PrimaryKeyToColumn, PrimaryKeyTrait, QueryFilter, - QueryResult, Related, RelationDef, RelationTrait, Select, Value, + LoaderTrait, ModelTrait, PaginatorTrait, PartialModelTrait, PrimaryKeyToColumn, + PrimaryKeyTrait, QueryFilter, QueryResult, Related, RelationDef, RelationTrait, Select, Value, }; #[cfg(feature = "macros")] diff --git a/src/executor/select.rs b/src/executor/select.rs index c3bb1f29..462f1601 100644 --- a/src/executor/select.rs +++ b/src/executor/select.rs @@ -1,7 +1,7 @@ use crate::{ error::*, ConnectionTrait, EntityTrait, FromQueryResult, IdenStatic, Iterable, ModelTrait, - PrimaryKeyToColumn, QueryResult, Select, SelectA, SelectB, SelectTwo, SelectTwoMany, Statement, - StreamTrait, TryGetableMany, + PartialModelTrait, PrimaryKeyToColumn, QueryResult, QuerySelect, Select, SelectA, SelectB, + SelectTwo, SelectTwoMany, Statement, StreamTrait, TryGetableMany, }; use futures::{Stream, TryStreamExt}; use sea_query::SelectStatement; @@ -154,6 +154,14 @@ where } } + /// Return a [Selector] from `Self` that wraps a [SelectModel] with a [PartialModel](PartialModelTrait) + pub fn into_partial_model(self) -> Selector> + where + M: PartialModelTrait, + { + M::select_cols(crate::QuerySelect::select_only(self)).into_model::() + } + /// Get a selectable Model as a [JsonValue] for SQL JSON operations #[cfg(feature = "with-json")] pub fn into_json(self) -> Selector> { @@ -416,6 +424,18 @@ where } } + /// Perform a conversion into a [SelectTwoModel] with [PartialModel](PartialModelTrait) + pub fn into_partial_model(self) -> Selector> + where + M: PartialModelTrait, + N: PartialModelTrait, + { + let select = crate::QuerySelect::select_only(self); + let select = M::select_cols(select); + let select = N::select_cols(select); + select.into_model::() + } + /// Convert the Models into JsonValue #[cfg(feature = "with-json")] pub fn into_json(self) -> Selector> { @@ -469,6 +489,17 @@ where selector: SelectTwoModel { model: PhantomData }, } } + /// Performs a conversion to [Selector] with partial model + fn into_partial_model(self) -> Selector> + where + M: PartialModelTrait, + N: PartialModelTrait, + { + let select = self.select_only(); + let select = M::select_cols(select); + let select = N::select_cols(select); + select.into_model() + } /// Convert the results to JSON #[cfg(feature = "with-json")] @@ -490,6 +521,19 @@ where self.into_model().stream(db).await } + /// Stream the result of the operation with PartialModel + pub async fn stream_partial_model<'a: 'b, 'b, C, M, N>( + self, + db: &'a C, + ) -> Result), DbErr>> + 'b + Send, DbErr> + where + C: ConnectionTrait + StreamTrait + Send, + M: PartialModelTrait + Send + 'b, + N: PartialModelTrait + Send + 'b, + { + self.into_partial_model().stream(db).await + } + /// Get all Models from the select operation /// /// > `SelectTwoMany::one()` method has been dropped (#486) diff --git a/src/lib.rs b/src/lib.rs index 37da8aa5..537e7f5b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -350,8 +350,8 @@ pub use schema::*; pub use sea_orm_macros::{ DeriveActiveEnum, DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn, DeriveCustomColumn, DeriveEntity, DeriveEntityModel, DeriveIntoActiveModel, - DeriveMigrationName, DeriveModel, DerivePrimaryKey, DeriveRelation, FromJsonQueryResult, - FromQueryResult, + DeriveMigrationName, DeriveModel, DerivePartialModel, DerivePrimaryKey, DeriveRelation, + FromJsonQueryResult, FromQueryResult, }; pub use sea_query; diff --git a/src/query/traits.rs b/src/query/traits.rs index 5484a35c..a0e0a646 100644 --- a/src/query/traits.rs +++ b/src/query/traits.rs @@ -1,4 +1,4 @@ -use crate::{DbBackend, Statement}; +use crate::{ColumnTrait, DbBackend, IntoIdentity, IntoSimpleExpr, QuerySelect, Statement}; use sea_query::QueryStatementBuilder; /// A Trait for any type performing queries on a Model or ActiveModel @@ -55,3 +55,36 @@ pub trait QueryTrait { } } } + +/// Select specific column for partial model queries +pub trait SelectColumns { + /// Add a select column + /// + /// For more detail, please visit [QuerySelect::column] + fn select_column(self, col: C) -> Self; + + /// Add a select column with alias + /// + /// For more detail, please visit [QuerySelect::column_as] + fn select_column_as(self, col: C, alias: I) -> Self + where + C: IntoSimpleExpr, + I: IntoIdentity; +} + +impl SelectColumns for S +where + S: QuerySelect, +{ + fn select_column(self, col: C) -> Self { + QuerySelect::column(self, col) + } + + fn select_column_as(self, col: C, alias: I) -> Self + where + C: IntoSimpleExpr, + I: IntoIdentity, + { + QuerySelect::column_as(self, col, alias) + } +} diff --git a/tests/derive_tests.rs b/tests/derive_tests.rs index 5e3738ff..9fca8a4f 100644 --- a/tests/derive_tests.rs +++ b/tests/derive_tests.rs @@ -1,46 +1,100 @@ -use sea_orm::{FromQueryResult, TryGetable}; +mod from_query_result { + use sea_orm::{FromQueryResult, TryGetable}; -#[derive(FromQueryResult)] -struct SimpleTest { - _foo: i32, - _bar: String, + #[derive(FromQueryResult)] + struct SimpleTest { + _foo: i32, + _bar: String, + } + + #[derive(FromQueryResult)] + struct GenericTest { + _foo: i32, + _bar: T, + } + + #[derive(FromQueryResult)] + struct DoubleGenericTest { + _foo: T, + _bar: F, + } + + #[derive(FromQueryResult)] + struct BoundsGenericTest { + _foo: T, + } + + #[derive(FromQueryResult)] + struct WhereGenericTest + where + T: Copy + Clone + 'static, + { + _foo: T, + } + + #[derive(FromQueryResult)] + struct AlreadySpecifiedBoundsGenericTest { + _foo: T, + } + + #[derive(FromQueryResult)] + struct MixedGenericTest + where + F: Copy + Clone + 'static, + { + _foo: T, + _bar: F, + } } -#[derive(FromQueryResult)] -struct GenericTest { - _foo: i32, - _bar: T, -} +mod partial_model { + use entity::{Column, Entity}; + use sea_orm::{ColumnTrait, DerivePartialModel, FromQueryResult}; + use sea_query::Expr; + mod entity { + use sea_orm::{ + ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait, + EnumIter, PrimaryKeyTrait, + }; -#[derive(FromQueryResult)] -struct DoubleGenericTest { - _foo: T, - _bar: F, -} + #[derive(Debug, Clone, DeriveEntityModel)] + #[sea_orm(table_name = "foo_table")] + pub struct Model { + #[sea_orm(primary_key)] + id: i32, + foo: i32, + bar: String, + foo2: bool, + bar2: f64, + } -#[derive(FromQueryResult)] -struct BoundsGenericTest { - _foo: T, -} + #[derive(Debug, DeriveRelation, EnumIter)] + pub enum Relation {} -#[derive(FromQueryResult)] -struct WhereGenericTest -where - T: Copy + Clone + 'static, -{ - _foo: T, -} + impl ActiveModelBehavior for ActiveModel {} + } -#[derive(FromQueryResult)] -struct AlreadySpecifiedBoundsGenericTest { - _foo: T, -} + #[derive(FromQueryResult, DerivePartialModel)] + #[sea_orm(entity = "Entity")] + struct SimpleTest { + _foo: i32, + _bar: String, + } -#[derive(FromQueryResult)] -struct MixedGenericTest -where - F: Copy + Clone + 'static, -{ - _foo: T, - _bar: F, + #[derive(FromQueryResult, DerivePartialModel)] + #[sea_orm(entity = "Entity")] + struct FieldFromDiffNameColumnTest { + #[sea_orm(from_col = "foo2")] + _foo: i32, + #[sea_orm(from_col = "bar2")] + _bar: String, + } + + #[derive(FromQueryResult, DerivePartialModel)] + struct FieldFromExpr { + #[sea_orm(from_expr = "Column::Bar2.sum()")] + _foo: f64, + #[sea_orm(from_expr = "Expr::col(Column::Id).equals(Column::Foo)")] + _bar: bool, + } }