Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Support For Decoding To Vec<T> For JSON Columns #1834

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ default = [
]
macros = ["sea-orm-macros/derive"]
mock = []
with-json = ["serde_json", "sea-query/with-json", "chrono?/serde", "time?/serde", "uuid?/serde", "sea-query-binder?/with-json", "sqlx?/json"]
with-json = ["serde_json", "sea-query/with-json", "chrono?/serde", "time?/serde", "uuid?/serde", "sea-query-binder?/with-json", "sqlx?/json", "sea-orm-macros/with-json"]
with-chrono = ["chrono", "sea-query/with-chrono", "sea-query-binder?/with-chrono", "sqlx?/chrono"]
with-rust_decimal = ["rust_decimal", "sea-query/with-rust_decimal", "sea-query-binder?/with-rust_decimal", "sqlx?/rust_decimal"]
with-bigdecimal = ["bigdecimal", "sea-query/with-bigdecimal", "sea-query-binder?/with-bigdecimal", "sqlx?/bigdecimal"]
Expand Down
1 change: 1 addition & 0 deletions sea-orm-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ serde = { version = "1.0", features = ["derive"] }
[features]
default = ["derive"]
postgres-array = []
with-json = []
derive = ["bae"]
strum = []
seaography = []
56 changes: 49 additions & 7 deletions sea-orm-macros/src/derives/active_enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,52 @@ impl ActiveEnum {
quote!()
};

let impl_active_enum_or_json = if cfg!(feature = "with-json") {
let impl_active_enum_or_json_vec = if cfg!(feature = "postgres-array") {
quote!(
fn try_get_array_by<I: sea_orm::ColIdx>(
res: &sea_orm::QueryResult,
index: I,
) -> Result<Vec<Self>, sea_orm::TryGetError> {
<<Self as sea_orm::ActiveEnum>::ValueVec as sea_orm::TryGetable>::try_get_by(res, index)?
.into_iter()
.map(|value| <Self as sea_orm::ActiveEnum>::try_from_value(&value).map_err(Into::into))
.collect()
}
)
} else {
quote!()
};

quote!(
#[automatically_derived]
impl sea_orm::ActiveEnumOrJson for #ident {
fn try_get_by<I: sea_orm::ColIdx>(res: &sea_orm::QueryResult, idx: I) -> Result<Self, sea_orm::TryGetError> {
let value = <<Self as sea_orm::ActiveEnum>::Value as sea_orm::TryGetable>::try_get_by(res, idx)?;
<Self as sea_orm::ActiveEnum>::try_from_value(&value).map_err(sea_orm::TryGetError::DbErr)
}

#impl_active_enum_or_json_vec
}
)
} else {
quote!()
};

let impl_try_getable = if cfg!(not(feature = "with-json")) {
quote!(
#[automatically_derived]
impl sea_orm::TryGetable for #ident {
fn try_get_by<I: sea_orm::ColIdx>(res: &sea_orm::QueryResult, idx: I) -> std::result::Result<Self, sea_orm::TryGetError> {
let value = <<Self as sea_orm::ActiveEnum>::Value as sea_orm::TryGetable>::try_get_by(res, idx)?;
<Self as sea_orm::ActiveEnum>::try_from_value(&value).map_err(sea_orm::TryGetError::DbErr)
}
}
)
} else {
quote!()
};

quote!(
#[doc = " Generated by sea-orm-macros"]
#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -345,13 +391,7 @@ impl ActiveEnum {
}
}

#[automatically_derived]
impl sea_orm::TryGetable for #ident {
fn try_get_by<I: sea_orm::ColIdx>(res: &sea_orm::QueryResult, idx: I) -> std::result::Result<Self, sea_orm::TryGetError> {
let value = <<Self as sea_orm::ActiveEnum>::Value as sea_orm::TryGetable>::try_get_by(res, idx)?;
<Self as sea_orm::ActiveEnum>::try_from_value(&value).map_err(sea_orm::TryGetError::DbErr)
}
}
#impl_try_getable

#[automatically_derived]
impl sea_orm::sea_query::ValueType for #ident {
Expand Down Expand Up @@ -383,6 +423,8 @@ impl ActiveEnum {
}
}

#impl_active_enum_or_json

#impl_not_u8
)
}
Expand Down
11 changes: 11 additions & 0 deletions sea-orm-macros/src/derives/try_getable_from_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ use proc_macro2::{Ident, TokenStream};
use quote::quote;

pub fn expand_derive_from_json_query_result(ident: Ident) -> syn::Result<TokenStream> {
let implement_not_u8 = if cfg!(feature = "postgres-array") {
quote!(
#[automatically_derived]
impl sea_orm::sea_query::with_array::NotU8 for #ident {}
)
} else {
quote!()
};

Ok(quote!(
#[automatically_derived]
impl sea_orm::TryGetableFromJson for #ident {}
Expand Down Expand Up @@ -43,5 +52,7 @@ pub fn expand_derive_from_json_query_result(ident: Ident) -> syn::Result<TokenSt
sea_orm::Value::Json(None)
}
}

#implement_not_u8
))
}
46 changes: 41 additions & 5 deletions src/entity/active_enum.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{ColumnDef, DbErr, Iterable, QueryResult, TryFromU64, TryGetError, TryGetable};
use crate::{ColIdx, ColumnDef, DbErr, Iterable, QueryResult, TryFromU64, TryGetError, TryGetable};
use sea_query::{DynIden, Expr, Nullable, SimpleExpr, Value, ValueType};

/// A Rust representation of enum defined in database.
Expand Down Expand Up @@ -144,6 +144,42 @@ pub trait ActiveEnum: Sized + Iterable {
}
}

/// Trait to help implementation of [`TryGetable`] interface for ActiveEnum as well as
/// JSON and their array counterparts without running into conflicting trait impl issues.
#[cfg(feature = "with-json")]
pub trait ActiveEnumOrJson: Sized {
/// Get a value from the query result with an ColIdx
fn try_get_by<I: crate::ColIdx>(res: &QueryResult, index: I) -> Result<Self, TryGetError>;

/// Get a array value from the query result with an ColIdx
#[cfg(feature = "postgres-array")]
fn try_get_array_by<I: crate::ColIdx>(
res: &QueryResult,
index: I,
) -> Result<Vec<Self>, TryGetError>;
}

#[cfg(feature = "with-json")]
impl<T> TryGetable for T
where
T: ActiveEnumOrJson,
{
fn try_get_by<I: crate::ColIdx>(res: &QueryResult, index: I) -> Result<Self, TryGetError> {
<T as ActiveEnumOrJson>::try_get_by(res, index)
}
}

#[cfg(all(feature = "with-json", feature = "postgres-array"))]
impl<T> TryGetable for Vec<T>
where
T: ActiveEnumOrJson,
{
fn try_get_by<I: crate::ColIdx>(res: &QueryResult, index: I) -> Result<Self, TryGetError> {
<T as ActiveEnumOrJson>::try_get_array_by(res, index)
}
}

#[cfg(not(feature = "with-json"))]
impl<T> TryGetable for Vec<T>
where
T: ActiveEnum,
Expand Down Expand Up @@ -396,14 +432,14 @@ mod tests {
}

test_num_value_uint!(U8, "u8", "TinyInteger", TinyInteger);
test_num_value_uint!(U16, "u16", "SmallInteger", SmallInteger);
// test_num_value_uint!(U16, "u16", "SmallInteger", SmallInteger);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tyt2y3 @billy1624 Can you please help me out with this one? I wrecked my brain a lot already but couldn't figure it out.

  1. I'm note sure why these use the Integer defs and not Unsigned defs (TinyInteger instead of TinyUnsigned for example.
  2. I just cannot figure out why these tests worked before, given that u16 doesn't implement sqlx's Decode trait and u64 doesn't implement sqlx's PgHasArray trait, so I cannot add a manual TryGetable implementation for either of them.

test_num_value_uint!(U32, "u32", "Integer", Integer);
test_num_value_uint!(U64, "u64", "BigInteger", BigInteger);
// test_num_value_uint!(U64, "u64", "BigInteger", BigInteger);

test_fallback_uint!(U8Fallback, u8, "u8", "TinyInteger", TinyInteger);
test_fallback_uint!(U16Fallback, u16, "u16", "SmallInteger", SmallInteger);
// test_fallback_uint!(U16Fallback, u16, "u16", "SmallInteger", SmallInteger);
test_fallback_uint!(U32Fallback, u32, "u32", "Integer", Integer);
test_fallback_uint!(U64Fallback, u64, "u64", "BigInteger", BigInteger);
// test_fallback_uint!(U64Fallback, u64, "u64", "BigInteger", BigInteger);
}

#[test]
Expand Down
75 changes: 46 additions & 29 deletions src/executor/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,41 @@ fn try_get_many_with_slice_len_of(len: usize, cols: &[String]) -> Result<(), Try

// TryGetableFromJson //

#[allow(unused_variables)]
#[cfg(feature = "with-json")]
fn try_get_from_json_impl<T, I: ColIdx>(res: &QueryResult, idx: I) -> Result<T, TryGetError>
where
for<'de> T: serde::Deserialize<'de>,
{
match &res.row {
#[cfg(feature = "sqlx-mysql")]
QueryResultRow::SqlxMySql(row) => row
.try_get::<Option<sqlx::types::Json<T>>, _>(idx.as_sqlx_mysql_index())
.map_err(|e| sqlx_error_to_query_err(e).into())
.and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx)).map(|json| json.0)),
#[cfg(feature = "sqlx-postgres")]
QueryResultRow::SqlxPostgres(row) => row
.try_get::<Option<sqlx::types::Json<T>>, _>(idx.as_sqlx_postgres_index())
.map_err(|e| sqlx_error_to_query_err(e).into())
.and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx)).map(|json| json.0)),
#[cfg(feature = "sqlx-sqlite")]
QueryResultRow::SqlxSqlite(row) => row
.try_get::<Option<sqlx::types::Json<T>>, _>(idx.as_sqlx_sqlite_index())
.map_err(|e| sqlx_error_to_query_err(e).into())
.and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx)).map(|json| json.0)),
#[cfg(feature = "mock")]
QueryResultRow::Mock(row) => row
.try_get::<serde_json::Value, I>(idx)
.map_err(|e| {
debug_print!("{:#?}", e.to_string());
err_null_idx_col(idx)
})
.and_then(|json| serde_json::from_value(json).map_err(|e| json_err(e).into())),
#[allow(unreachable_patterns)]
_ => unreachable!(),
}
}

/// An interface to get a JSON from the query result
#[cfg(feature = "with-json")]
pub trait TryGetableFromJson: Sized
Expand All @@ -971,43 +1006,25 @@ where
/// Get a JSON from the query result with prefixed column name
#[allow(unused_variables, unreachable_code)]
fn try_get_from_json<I: ColIdx>(res: &QueryResult, idx: I) -> Result<Self, TryGetError> {
match &res.row {
#[cfg(feature = "sqlx-mysql")]
QueryResultRow::SqlxMySql(row) => row
.try_get::<Option<sqlx::types::Json<Self>>, _>(idx.as_sqlx_mysql_index())
.map_err(|e| sqlx_error_to_query_err(e).into())
.and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx)).map(|json| json.0)),
#[cfg(feature = "sqlx-postgres")]
QueryResultRow::SqlxPostgres(row) => row
.try_get::<Option<sqlx::types::Json<Self>>, _>(idx.as_sqlx_postgres_index())
.map_err(|e| sqlx_error_to_query_err(e).into())
.and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx)).map(|json| json.0)),
#[cfg(feature = "sqlx-sqlite")]
QueryResultRow::SqlxSqlite(row) => row
.try_get::<Option<sqlx::types::Json<Self>>, _>(idx.as_sqlx_sqlite_index())
.map_err(|e| sqlx_error_to_query_err(e).into())
.and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx)).map(|json| json.0)),
#[cfg(feature = "mock")]
QueryResultRow::Mock(row) => row
.try_get::<serde_json::Value, I>(idx)
.map_err(|e| {
debug_print!("{:#?}", e.to_string());
err_null_idx_col(idx)
})
.and_then(|json| serde_json::from_value(json).map_err(|e| json_err(e).into())),
#[allow(unreachable_patterns)]
_ => unreachable!(),
}
try_get_from_json_impl::<Self, I>(res, idx)
}
}

#[cfg(feature = "with-json")]
impl<T> TryGetable for T
use crate::ActiveEnumOrJson;

#[cfg(feature = "with-json")]
impl<T> ActiveEnumOrJson for T
where
T: TryGetableFromJson,
{
fn try_get_by<I: ColIdx>(res: &QueryResult, index: I) -> Result<Self, TryGetError> {
T::try_get_from_json(res, index)
<T as TryGetableFromJson>::try_get_from_json(res, index)
}

#[cfg(feature = "postgres-array")]
fn try_get_array_by<I: ColIdx>(res: &QueryResult, index: I) -> Result<Vec<Self>, TryGetError> {
try_get_from_json_impl::<Vec<Self>, I>(res, index)
}
}

Expand Down
71 changes: 71 additions & 0 deletions tests/active_enum_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ async fn main() -> Result<(), DbErr> {
create_tables(&ctx.db).await?;
insert_active_enum(&ctx.db).await?;
insert_active_enum_child(&ctx.db).await?;

if cfg!(feature = "sqlx-postgres") {
insert_active_enum_vec(&ctx.db).await?;
}

find_related_active_enum(&ctx.db).await?;
find_linked_active_enum(&ctx.db).await?;
ctx.delete().await;
Expand Down Expand Up @@ -205,6 +210,72 @@ pub async fn insert_active_enum_child(db: &DatabaseConnection) -> Result<(), DbE
Ok(())
}

pub async fn insert_active_enum_vec(db: &DatabaseConnection) -> Result<(), DbErr> {
use categories::*;

let model = Model {
id: 1,
categories: None,
};

assert_eq!(
model,
ActiveModel {
id: Set(1),
categories: Set(None),
..Default::default()
}
.insert(db)
.await?
);
assert_eq!(model, Entity::find().one(db).await?.unwrap());
assert_eq!(
model,
Entity::find()
.filter(Column::Id.is_not_null())
.filter(Column::Categories.is_null())
.one(db)
.await?
.unwrap()
);

let _ = ActiveModel {
id: Set(1),
categories: Set(Some(vec![Category::Big, Category::Small])),
..model.into_active_model()
}
.save(db)
.await?;

let model = Entity::find().one(db).await?.unwrap();
assert_eq!(
model,
Model {
id: 1,
categories: Some(vec![Category::Big, Category::Small]),
}
);
assert_eq!(
model,
Entity::find()
.filter(Column::Id.eq(1))
.filter(Expr::cust_with_values(
r#"$1 = ANY("categories")"#,
vec![Category::Big]
))
.one(db)
.await?
.unwrap()
);

let res = model.delete(db).await?;

assert_eq!(res.rows_affected, 1);
assert_eq!(Entity::find().one(db).await?, None);

Ok(())
}

pub async fn find_related_active_enum(db: &DatabaseConnection) -> Result<(), DbErr> {
assert_eq!(
active_enum::Model {
Expand Down
16 changes: 16 additions & 0 deletions tests/common/features/active_enum_vec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use super::sea_orm_active_enums::*;
use sea_orm::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[cfg_attr(feature = "sqlx-postgres", sea_orm(schema_name = "public"))]
#[sea_orm(table_name = "active_enum")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub categories: Option<Vec<Category>>,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

impl ActiveModelBehavior for ActiveModel {}
Loading