From af5721a3f1b14639a4cb311607360cc547af9502 Mon Sep 17 00:00:00 2001 From: Billy Chan Date: Thu, 16 Jun 2022 18:25:20 +0800 Subject: [PATCH 1/2] de(serialize) custom JSON types --- sea-orm-macros/src/derives/mod.rs | 2 + .../src/derives/try_getable_from_json.rs | 44 ++++++++ sea-orm-macros/src/lib.rs | 10 ++ src/entity/prelude.rs | 2 +- src/executor/query.rs | 60 +++++++++++ src/lib.rs | 2 +- tests/common/features/json_struct.rs | 25 +++++ tests/common/features/mod.rs | 2 + tests/common/features/schema.rs | 23 ++++ tests/json_struct_tests.rs | 100 ++++++++++++++++++ 10 files changed, 268 insertions(+), 2 deletions(-) create mode 100644 sea-orm-macros/src/derives/try_getable_from_json.rs create mode 100644 tests/common/features/json_struct.rs create mode 100644 tests/json_struct_tests.rs diff --git a/sea-orm-macros/src/derives/mod.rs b/sea-orm-macros/src/derives/mod.rs index 36b9f6698..9c91daad8 100644 --- a/sea-orm-macros/src/derives/mod.rs +++ b/sea-orm-macros/src/derives/mod.rs @@ -9,6 +9,7 @@ mod into_active_model; mod model; mod primary_key; mod relation; +mod try_getable_from_json; pub use active_enum::*; pub use active_model::*; @@ -21,3 +22,4 @@ pub use into_active_model::*; pub use model::*; pub use primary_key::*; pub use relation::*; +pub use try_getable_from_json::*; diff --git a/sea-orm-macros/src/derives/try_getable_from_json.rs b/sea-orm-macros/src/derives/try_getable_from_json.rs new file mode 100644 index 000000000..3f8342021 --- /dev/null +++ b/sea-orm-macros/src/derives/try_getable_from_json.rs @@ -0,0 +1,44 @@ +use proc_macro2::{Ident, TokenStream}; +use quote::{format_ident, quote, quote_spanned}; +use syn::{ext::IdentExt, Data, DataStruct, Field, Fields}; + +pub fn expand_derive_try_getable_from_json(ident: Ident, data: Data) -> syn::Result { + Ok(quote!( + #[automatically_derived] + impl sea_orm::TryGetableFromJson for #ident {} + + #[automatically_derived] + impl std::convert::From<#ident> for sea_orm::Value { + fn from(source: #ident) -> Self { + sea_orm::Value::Json(serde_json::to_value(&source).ok().map(|s| std::boxed::Box::new(s))) + } + } + + #[automatically_derived] + impl sea_query::ValueType for #ident { + fn try_from(v: sea_orm::Value) -> Result { + match v { + sea_orm::Value::Json(Some(json)) => Ok( + serde_json::from_value(*json).map_err(|_| sea_orm::sea_query::ValueTypeErr)?, + ), + _ => Err(sea_orm::sea_query::ValueTypeErr), + } + } + + fn type_name() -> String { + stringify!(#ident).to_owned() + } + + fn column_type() -> sea_orm::sea_query::ColumnType { + sea_orm::sea_query::ColumnType::Json + } + } + + #[automatically_derived] + impl sea_orm::sea_query::Nullable for #ident { + fn null() -> sea_orm::Value { + sea_orm::Value::Json(None) + } + } + )) +} diff --git a/sea-orm-macros/src/lib.rs b/sea-orm-macros/src/lib.rs index 3201907ea..9872d9769 100644 --- a/sea-orm-macros/src/lib.rs +++ b/sea-orm-macros/src/lib.rs @@ -582,6 +582,16 @@ pub fn derive_relation(input: TokenStream) -> TokenStream { .into() } +#[proc_macro_derive(DeriveTryGetableFromJson)] +pub fn derive_try_getable_from_json(input: TokenStream) -> TokenStream { + let DeriveInput { ident, data, .. } = parse_macro_input!(input); + + match derives::expand_derive_try_getable_from_json(ident, data) { + Ok(ts) => ts.into(), + Err(e) => e.to_compile_error().into(), + } +} + #[doc(hidden)] #[proc_macro_attribute] pub fn test(_: TokenStream, input: TokenStream) -> TokenStream { diff --git a/src/entity/prelude.rs b/src/entity/prelude.rs index 10b1bb503..ebc7a182c 100644 --- a/src/entity/prelude.rs +++ b/src/entity/prelude.rs @@ -9,7 +9,7 @@ pub use crate::{ pub use crate::{ DeriveActiveEnum, DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn, DeriveCustomColumn, DeriveEntity, DeriveEntityModel, DeriveIntoActiveModel, DeriveModel, - DerivePrimaryKey, DeriveRelation, + DerivePrimaryKey, DeriveRelation, DeriveTryGetableFromJson, }; #[cfg(feature = "with-json")] diff --git a/src/executor/query.rs b/src/executor/query.rs index 5d19aa1f1..d7c2bb0d3 100644 --- a/src/executor/query.rs +++ b/src/executor/query.rs @@ -584,6 +584,66 @@ fn try_get_many_with_slice_len_of(len: usize, cols: &[String]) -> Result<(), Try } } +// TryGetableFromJson // + +/// Perform a query on multiple columns +#[cfg(feature = "with-json")] +pub trait TryGetableFromJson: Sized +where + for<'de> Self: serde::Deserialize<'de>, +{ + /// Ensure the type implements this method + fn try_get_from_json(res: &QueryResult, pre: &str, col: &str) -> Result { + let column = format!("{}{}", pre, col); + let res = match &res.row { + #[cfg(feature = "sqlx-mysql")] + QueryResultRow::SqlxMySql(row) => { + use sqlx::Row; + row.try_get::, _>(column.as_str()) + .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) + .and_then(|opt| opt.ok_or(TryGetError::Null)) + } + #[cfg(feature = "sqlx-postgres")] + QueryResultRow::SqlxPostgres(row) => { + use sqlx::Row; + row.try_get::, _>(column.as_str()) + .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) + .and_then(|opt| opt.ok_or(TryGetError::Null)) + } + #[cfg(feature = "sqlx-sqlite")] + QueryResultRow::SqlxSqlite(row) => { + use sqlx::Row; + row.try_get::, _>(column.as_str()) + .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e))) + .and_then(|opt| opt.ok_or(TryGetError::Null)) + } + #[cfg(feature = "mock")] + QueryResultRow::Mock(row) => { + row.try_get::(column.as_str()) + .map_err(|e| { + debug_print!("{:#?}", e.to_string()); + TryGetError::Null + }) + } + #[allow(unreachable_patterns)] + _ => unreachable!(), + }; + res.and_then(|json| { + serde_json::from_value(json).map_err(|e| TryGetError::DbErr(DbErr::Json(e.to_string()))) + }) + } +} + +#[cfg(feature = "with-json")] +impl TryGetable for T +where + T: TryGetableFromJson, +{ + fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result { + T::try_get_from_json(res, pre, col) + } +} + // TryFromU64 // /// Try to convert a type to a u64 pub trait TryFromU64: Sized { diff --git a/src/lib.rs b/src/lib.rs index b0604a2cb..90ab96fdd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -330,7 +330,7 @@ pub use schema::*; pub use sea_orm_macros::{ DeriveActiveEnum, DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn, DeriveCustomColumn, DeriveEntity, DeriveEntityModel, DeriveIntoActiveModel, DeriveModel, - DerivePrimaryKey, DeriveRelation, FromQueryResult, + DerivePrimaryKey, DeriveRelation, DeriveTryGetableFromJson, FromQueryResult, }; pub use sea_query; diff --git a/tests/common/features/json_struct.rs b/tests/common/features/json_struct.rs new file mode 100644 index 000000000..2ad7f70da --- /dev/null +++ b/tests/common/features/json_struct.rs @@ -0,0 +1,25 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "json_struct")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub json: Json, + pub json_value: KeyValue, + pub json_value_opt: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, DeriveTryGetableFromJson)] +pub struct KeyValue { + pub id: i32, + pub name: String, + pub price: f32, + pub notes: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/tests/common/features/mod.rs b/tests/common/features/mod.rs index fc9ac047e..0b26b2615 100644 --- a/tests/common/features/mod.rs +++ b/tests/common/features/mod.rs @@ -3,6 +3,7 @@ pub mod active_enum_child; pub mod applog; pub mod byte_primary_key; pub mod insert_default; +pub mod json_struct; pub mod json_vec; pub mod metadata; pub mod repository; @@ -17,6 +18,7 @@ pub use active_enum_child::Entity as ActiveEnumChild; pub use applog::Entity as Applog; pub use byte_primary_key::Entity as BytePrimaryKey; pub use insert_default::Entity as InsertDefault; +pub use json_struct::Entity as JsonStruct; pub use json_vec::Entity as JsonVec; pub use metadata::Entity as Metadata; pub use repository::Entity as Repository; diff --git a/tests/common/features/schema.rs b/tests/common/features/schema.rs index 0a6549008..b6bf9ae47 100644 --- a/tests/common/features/schema.rs +++ b/tests/common/features/schema.rs @@ -19,6 +19,7 @@ pub async fn create_tables(db: &DatabaseConnection) -> Result<(), DbErr> { create_satellites_table(db).await?; create_transaction_log_table(db).await?; create_json_vec_table(db).await?; + create_json_struct_table(db).await?; let create_enum_stmts = match db_backend { DbBackend::MySql | DbBackend::Sqlite => Vec::new(), @@ -303,3 +304,25 @@ pub async fn create_json_vec_table(db: &DbConn) -> Result { create_table(db, &create_table_stmt, JsonVec).await } + +pub async fn create_json_struct_table(db: &DbConn) -> Result { + let stmt = sea_query::Table::create() + .table(json_struct::Entity) + .col( + ColumnDef::new(json_struct::Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(json_struct::Column::Json).json().not_null()) + .col( + ColumnDef::new(json_struct::Column::JsonValue) + .json() + .not_null(), + ) + .col(ColumnDef::new(json_struct::Column::JsonValueOpt).json()) + .to_owned(); + + create_table(db, &stmt, JsonStruct).await +} diff --git a/tests/json_struct_tests.rs b/tests/json_struct_tests.rs new file mode 100644 index 000000000..5c5876902 --- /dev/null +++ b/tests/json_struct_tests.rs @@ -0,0 +1,100 @@ +pub mod common; + +pub use common::{features::*, setup::*, TestContext}; +use pretty_assertions::assert_eq; +use sea_orm::{entity::prelude::*, entity::*, DatabaseConnection}; +use serde_json::json; + +#[sea_orm_macros::test] +#[cfg(any( + feature = "sqlx-mysql", + feature = "sqlx-sqlite", + feature = "sqlx-postgres" +))] +async fn main() -> Result<(), DbErr> { + let ctx = TestContext::new("json_struct_tests").await; + create_tables(&ctx.db).await?; + insert_json_struct_1(&ctx.db).await?; + insert_json_struct_2(&ctx.db).await?; + ctx.delete().await; + + Ok(()) +} + +pub async fn insert_json_struct_1(db: &DatabaseConnection) -> Result<(), DbErr> { + use json_struct::*; + + let model = Model { + id: 1, + json: json!({ + "id": 1, + "name": "apple", + "price": 12.01, + "notes": "hand picked, organic", + }), + json_value: KeyValue { + id: 1, + name: "apple".into(), + price: 12.01, + notes: Some("hand picked, organic".into()), + } + .into(), + json_value_opt: Some(KeyValue { + id: 1, + name: "apple".into(), + price: 12.01, + notes: Some("hand picked, organic".into()), + }) + .into(), + }; + + let result = model.clone().into_active_model().insert(db).await?; + + assert_eq!(result, model); + + assert_eq!( + Entity::find() + .filter(Column::Id.eq(model.id)) + .one(db) + .await?, + Some(model) + ); + + Ok(()) +} + +pub async fn insert_json_struct_2(db: &DatabaseConnection) -> Result<(), DbErr> { + use json_struct::*; + + let model = Model { + id: 2, + json: json!({ + "id": 2, + "name": "orange", + "price": 10.93, + "notes": "sweet & juicy", + }), + json_value: KeyValue { + id: 1, + name: "orange".into(), + price: 10.93, + notes: None, + } + .into(), + json_value_opt: None.into(), + }; + + let result = model.clone().into_active_model().insert(db).await?; + + assert_eq!(result, model); + + assert_eq!( + Entity::find() + .filter(Column::Id.eq(model.id)) + .one(db) + .await?, + Some(model) + ); + + Ok(()) +} From cc6e256d19f914b523058c54a50237d0071f3805 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Fri, 1 Jul 2022 00:54:51 +0800 Subject: [PATCH 2/2] Rename DeriveTryGetableFromJson -> FromJsonQueryResult --- sea-orm-macros/src/derives/try_getable_from_json.rs | 2 +- sea-orm-macros/src/lib.rs | 6 +++--- src/entity/prelude.rs | 2 +- src/lib.rs | 2 +- tests/common/features/json_struct.rs | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/sea-orm-macros/src/derives/try_getable_from_json.rs b/sea-orm-macros/src/derives/try_getable_from_json.rs index 3f8342021..efcb76b55 100644 --- a/sea-orm-macros/src/derives/try_getable_from_json.rs +++ b/sea-orm-macros/src/derives/try_getable_from_json.rs @@ -2,7 +2,7 @@ use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote, quote_spanned}; use syn::{ext::IdentExt, Data, DataStruct, Field, Fields}; -pub fn expand_derive_try_getable_from_json(ident: Ident, data: Data) -> syn::Result { +pub fn expand_derive_from_json_query_result(ident: Ident, data: Data) -> syn::Result { Ok(quote!( #[automatically_derived] impl sea_orm::TryGetableFromJson for #ident {} diff --git a/sea-orm-macros/src/lib.rs b/sea-orm-macros/src/lib.rs index 9872d9769..ec4a64ed7 100644 --- a/sea-orm-macros/src/lib.rs +++ b/sea-orm-macros/src/lib.rs @@ -582,11 +582,11 @@ pub fn derive_relation(input: TokenStream) -> TokenStream { .into() } -#[proc_macro_derive(DeriveTryGetableFromJson)] -pub fn derive_try_getable_from_json(input: TokenStream) -> TokenStream { +#[proc_macro_derive(FromJsonQueryResult)] +pub fn derive_from_json_query_result(input: TokenStream) -> TokenStream { let DeriveInput { ident, data, .. } = parse_macro_input!(input); - match derives::expand_derive_try_getable_from_json(ident, data) { + match derives::expand_derive_from_json_query_result(ident, data) { Ok(ts) => ts.into(), Err(e) => e.to_compile_error().into(), } diff --git a/src/entity/prelude.rs b/src/entity/prelude.rs index ebc7a182c..ee181f380 100644 --- a/src/entity/prelude.rs +++ b/src/entity/prelude.rs @@ -9,7 +9,7 @@ pub use crate::{ pub use crate::{ DeriveActiveEnum, DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn, DeriveCustomColumn, DeriveEntity, DeriveEntityModel, DeriveIntoActiveModel, DeriveModel, - DerivePrimaryKey, DeriveRelation, DeriveTryGetableFromJson, + DerivePrimaryKey, DeriveRelation, FromJsonQueryResult, }; #[cfg(feature = "with-json")] diff --git a/src/lib.rs b/src/lib.rs index 90ab96fdd..0ca2323f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -330,7 +330,7 @@ pub use schema::*; pub use sea_orm_macros::{ DeriveActiveEnum, DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn, DeriveCustomColumn, DeriveEntity, DeriveEntityModel, DeriveIntoActiveModel, DeriveModel, - DerivePrimaryKey, DeriveRelation, DeriveTryGetableFromJson, FromQueryResult, + DerivePrimaryKey, DeriveRelation, FromJsonQueryResult, FromQueryResult, }; pub use sea_query; diff --git a/tests/common/features/json_struct.rs b/tests/common/features/json_struct.rs index 2ad7f70da..44235d1b7 100644 --- a/tests/common/features/json_struct.rs +++ b/tests/common/features/json_struct.rs @@ -11,7 +11,7 @@ pub struct Model { pub json_value_opt: Option, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, DeriveTryGetableFromJson)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, FromJsonQueryResult)] pub struct KeyValue { pub id: i32, pub name: String,