add SelectColumns only modify select columns

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`
This commit is contained in:
ForzenString 2023-04-13 19:15:04 +08:00 committed by Chris Tsang
parent f876927f53
commit 744e063222
10 changed files with 530 additions and 44 deletions

View File

@ -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::*;

View File

@ -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<syn::Ident>,
ident: syn::Ident,
fields: Vec<ColumnAs>,
}
impl DerivePartialModel {
fn new(input: syn::DeriveInput) -> Result<Self, Error> {
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::<Meta, Comma>::parse_terminated) {
for meta in list {
entity_ident = meta
.get_as_kv("entity")
.map(|s| syn::parse_str::<syn::Ident>(&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::<Meta, Comma>::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::<Expr>(&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<TokenStream> {
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<S: sea_orm::SelectColumns>(#select_ident: S) -> S{
#(#select_col_code_gen)*
#select_ident
}
}
}
}
}
pub fn expand_derive_partial_model(input: syn::DeriveInput) -> syn::Result<TokenStream> {
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<String>;
}
impl GetAsKVMeta for Meta {
fn get_as_kv(&self, k: &str) -> Option<String> {
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<T> = Result<T, Box<dyn std::error::Error>>;
#[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::<DeriveInput>(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(())
}
}

View File

@ -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]

View File

@ -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::*;

View File

@ -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<S: SelectColumns>(select: S) -> S;
}

View File

@ -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")]

View File

@ -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<M>(self) -> Selector<SelectModel<M>>
where
M: PartialModelTrait,
{
M::select_cols(crate::QuerySelect::select_only(self)).into_model::<M>()
}
/// Get a selectable Model as a [JsonValue] for SQL JSON operations
#[cfg(feature = "with-json")]
pub fn into_json(self) -> Selector<SelectModel<JsonValue>> {
@ -416,6 +424,18 @@ where
}
}
/// Perform a conversion into a [SelectTwoModel] with [PartialModel](PartialModelTrait)
pub fn into_partial_model<M, N>(self) -> Selector<SelectTwoModel<M, N>>
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::<M, N>()
}
/// Convert the Models into JsonValue
#[cfg(feature = "with-json")]
pub fn into_json(self) -> Selector<SelectTwoModel<JsonValue, JsonValue>> {
@ -469,6 +489,17 @@ where
selector: SelectTwoModel { model: PhantomData },
}
}
/// Performs a conversion to [Selector] with partial model
fn into_partial_model<M, N>(self) -> Selector<SelectTwoModel<M, N>>
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<impl Stream<Item = Result<(M, Option<N>), 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)

View File

@ -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;

View File

@ -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<C: ColumnTrait>(self, col: C) -> Self;
/// Add a select column with alias
///
/// For more detail, please visit [QuerySelect::column_as]
fn select_column_as<C, I>(self, col: C, alias: I) -> Self
where
C: IntoSimpleExpr,
I: IntoIdentity;
}
impl<S> SelectColumns for S
where
S: QuerySelect,
{
fn select_column<C: ColumnTrait>(self, col: C) -> Self {
QuerySelect::column(self, col)
}
fn select_column_as<C, I>(self, col: C, alias: I) -> Self
where
C: IntoSimpleExpr,
I: IntoIdentity,
{
QuerySelect::column_as(self, col, alias)
}
}

View File

@ -1,46 +1,100 @@
use sea_orm::{FromQueryResult, TryGetable};
mod from_query_result {
use sea_orm::{FromQueryResult, TryGetable};
#[derive(FromQueryResult)]
struct SimpleTest {
#[derive(FromQueryResult)]
struct SimpleTest {
_foo: i32,
_bar: String,
}
}
#[derive(FromQueryResult)]
struct GenericTest<T> {
#[derive(FromQueryResult)]
struct GenericTest<T> {
_foo: i32,
_bar: T,
}
}
#[derive(FromQueryResult)]
struct DoubleGenericTest<T, F> {
#[derive(FromQueryResult)]
struct DoubleGenericTest<T, F> {
_foo: T,
_bar: F,
}
}
#[derive(FromQueryResult)]
struct BoundsGenericTest<T: Copy + Clone + 'static> {
#[derive(FromQueryResult)]
struct BoundsGenericTest<T: Copy + Clone + 'static> {
_foo: T,
}
}
#[derive(FromQueryResult)]
struct WhereGenericTest<T>
where
#[derive(FromQueryResult)]
struct WhereGenericTest<T>
where
T: Copy + Clone + 'static,
{
{
_foo: T,
}
}
#[derive(FromQueryResult)]
struct AlreadySpecifiedBoundsGenericTest<T: TryGetable> {
#[derive(FromQueryResult)]
struct AlreadySpecifiedBoundsGenericTest<T: TryGetable> {
_foo: T,
}
}
#[derive(FromQueryResult)]
struct MixedGenericTest<T: Clone, F>
where
#[derive(FromQueryResult)]
struct MixedGenericTest<T: Clone, F>
where
F: Copy + Clone + 'static,
{
{
_foo: T,
_bar: F,
}
}
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(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(Debug, DeriveRelation, EnumIter)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
}
#[derive(FromQueryResult, DerivePartialModel)]
#[sea_orm(entity = "Entity")]
struct SimpleTest {
_foo: i32,
_bar: String,
}
#[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,
}
}