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

Rework ActiveEnum to support TryGetable for Vec<Json> #1898

Merged
merged 4 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions sea-orm-macros/src/derives/active_enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,16 @@ impl ActiveEnum {
}
}

#[automatically_derived]
impl sea_orm::TryGetableArray for #ident {
fn try_get_by<I: sea_orm::ColIdx>(res: &sea_orm::QueryResult, index: I) -> std::result::Result<Vec<Self>, sea_orm::TryGetError> {
<<Self as sea_orm::ActiveEnum>::Value as sea_orm::ActiveEnumValue>::try_get_vec_by(res, index)?
.into_iter()
.map(|value| <Self as sea_orm::ActiveEnum>::try_from_value(&value).map_err(Into::into))
.collect()
}
}

#[automatically_derived]
#[allow(clippy::from_over_into)]
impl Into<sea_orm::sea_query::Value> for #ident {
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 impl_not_u8 = if cfg!(feature = "postgres-array") {
quote!(
#[automatically_derived]
impl sea_orm::sea_query::value::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)
}
}

#impl_not_u8
))
}
66 changes: 50 additions & 16 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 @@ -110,11 +110,11 @@ use sea_query::{DynIden, Expr, Nullable, SimpleExpr, Value, ValueType};
/// impl ActiveModelBehavior for ActiveModel {}
/// ```
pub trait ActiveEnum: Sized + Iterable {
/// Define the Rust type that each enum variant represents.
type Value: Into<Value> + ValueType + Nullable + TryGetable;
/// Define the Rust type that each enum variant corresponds.
type Value: ActiveEnumValue;

/// Define the enum value in Vector type.
type ValueVec: IntoIterator<Item = Self::Value>;
/// This has no purpose. It will be removed in the next major version.
type ValueVec;

/// Get the name of enum
fn name() -> DynIden;
Expand Down Expand Up @@ -144,19 +144,53 @@ pub trait ActiveEnum: Sized + Iterable {
}
}

impl<T> TryGetable for Vec<T>
where
T: ActiveEnum,
T::ValueVec: TryGetable,
{
fn try_get_by<I: crate::ColIdx>(res: &QueryResult, index: I) -> Result<Self, TryGetError> {
<T::ValueVec as TryGetable>::try_get_by(res, index)?
.into_iter()
.map(|value| T::try_from_value(&value).map_err(Into::into))
.collect()
}
/// The Rust Value backing ActiveEnums
pub trait ActiveEnumValue: Into<Value> + ValueType + Nullable + TryGetable {
/// For getting an array of enum. Postgres only
fn try_get_vec_by<I: ColIdx>(res: &QueryResult, index: I) -> Result<Vec<Self>, TryGetError>;
}

macro_rules! impl_active_enum_value {
($type:ident) => {
impl ActiveEnumValue for $type {
fn try_get_vec_by<I: ColIdx>(
_res: &QueryResult,
_index: I,
) -> Result<Vec<Self>, TryGetError> {
panic!("Not supported by `postgres-array`")
}
}
};
}

macro_rules! impl_active_enum_value_with_pg_array {
($type:ident) => {
impl ActiveEnumValue for $type {
fn try_get_vec_by<I: ColIdx>(
_res: &QueryResult,
_index: I,
) -> Result<Vec<Self>, TryGetError> {
#[cfg(feature = "postgres-array")]
{
<Vec<Self>>::try_get_by(_res, _index)
}
#[cfg(not(feature = "postgres-array"))]
panic!("`postgres-array` is not enabled")
}
}
};
}

impl_active_enum_value!(u8);
impl_active_enum_value!(u16);
impl_active_enum_value!(u32);
impl_active_enum_value!(u64);
impl_active_enum_value_with_pg_array!(String);
impl_active_enum_value_with_pg_array!(i8);
impl_active_enum_value_with_pg_array!(i16);
impl_active_enum_value_with_pg_array!(i32);
impl_active_enum_value_with_pg_array!(i64);

impl<T> TryFromU64 for T
where
T: ActiveEnum,
Expand Down
39 changes: 36 additions & 3 deletions src/entity/column.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ pub trait ColumnTrait: IdenStatic + Iterable + FromStr {
}

/// Cast value of an enum column as enum type; do nothing if `self` is not an enum.
/// Will also transform `Array(Vec<Json>)` into `Json(Vec<Json>)` if the column type is `Json`.
fn save_enum_as(&self, val: Expr) -> SimpleExpr {
cast_enum_as(val, self, |col, enum_name, col_type| {
let type_name = match col_type {
Expand Down Expand Up @@ -412,9 +413,41 @@ where
{
let col_def = col.def();
let col_type = col_def.get_column_type();
match col_type.get_enum_name() {
Some(enum_name) => f(expr, SeaRc::clone(enum_name), col_type),
None => expr.into(),

match col_type {
#[cfg(all(feature = "with-json", feature = "postgres-array"))]
ColumnType::Json | ColumnType::JsonBinary => {
use sea_query::ArrayType;
use serde_json::Value as Json;

#[allow(clippy::boxed_local)]
fn unbox<T>(boxed: Box<T>) -> T {
*boxed
}

let expr = expr.into();
match expr {
SimpleExpr::Value(Value::Array(ArrayType::Json, Some(json_vec))) => {
// flatten Array(Vec<Json>) into Json
let json_vec: Vec<Json> = json_vec
.into_iter()
.filter_map(|val| match val {
Value::Json(Some(json)) => Some(unbox(json)),
_ => None,
})
.collect();
SimpleExpr::Value(Value::Json(Some(Box::new(json_vec.into()))))
}
SimpleExpr::Value(Value::Array(ArrayType::Json, None)) => {
SimpleExpr::Value(Value::Json(None))
}
_ => expr,
}
}
_ => match col_type.get_enum_name() {
Some(enum_name) => f(expr, SeaRc::clone(enum_name), col_type),
None => expr.into(),
},
}
}

Expand Down
45 changes: 45 additions & 0 deletions src/executor/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,25 @@ fn try_get_many_with_slice_len_of(len: usize, cols: &[String]) -> Result<(), Try
}
}

/// An interface to get an array of values from the query result.
/// A type can only implement `ActiveEnum` or `TryGetableFromJson`, but not both.
/// A blanket impl is provided for `TryGetableFromJson`, while the impl for `ActiveEnum`
/// is provided by the `DeriveActiveEnum` macro. So as an end user you won't normally
/// touch this trait.
pub trait TryGetableArray: Sized {
/// Just a delegate
fn try_get_by<I: ColIdx>(res: &QueryResult, index: I) -> Result<Vec<Self>, TryGetError>;
}

impl<T> TryGetable for Vec<T>
where
T: TryGetableArray,
{
fn try_get_by<I: ColIdx>(res: &QueryResult, index: I) -> Result<Self, TryGetError> {
T::try_get_by(res, index)
}
}

// TryGetableFromJson //

/// An interface to get a JSON from the query result
Expand Down Expand Up @@ -999,6 +1018,22 @@ where
_ => unreachable!(),
}
}

/// Get a Vec<Self> from an Array of Json
fn from_json_vec(value: serde_json::Value) -> Result<Vec<Self>, TryGetError> {
match value {
serde_json::Value::Array(values) => {
let mut res = Vec::new();
for item in values {
res.push(serde_json::from_value(item).map_err(json_err)?);
}
Ok(res)
}
_ => Err(TryGetError::DbErr(DbErr::Json(
"Value is not an Array".to_owned(),
))),
}
}
}

#[cfg(feature = "with-json")]
Expand All @@ -1011,6 +1046,16 @@ where
}
}

#[cfg(feature = "with-json")]
impl<T> TryGetableArray for T
where
T: TryGetableFromJson,
{
fn try_get_by<I: ColIdx>(res: &QueryResult, index: I) -> Result<Vec<T>, TryGetError> {
T::from_json_vec(serde_json::Value::try_get_by(res, index)?)
}
}

// TryFromU64 //
/// Try to convert a type to a u64
pub trait TryFromU64: Sized {
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,8 +22,13 @@ async fn main() -> Result<(), DbErr> {
create_tables(&ctx.db).await?;
insert_active_enum(&ctx.db).await?;
insert_active_enum_child(&ctx.db).await?;

#[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;

Ok(())
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 {}
15 changes: 15 additions & 0 deletions tests/common/features/categories.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use super::sea_orm_active_enums::*;
use sea_orm::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "categories")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: i32,
pub categories: Option<Vec<Category>>,
}

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

impl ActiveModelBehavior for ActiveModel {}
Loading