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

Fix IntoActiveValue for Option #323

Closed
wants to merge 8 commits into from

Conversation

YoshieraHuang
Copy link
Contributor

Related to #322

@tyt2y3
Copy link
Member

tyt2y3 commented Nov 17, 2021

@billy1624 thoughts?

@billy1624
Copy link
Member

billy1624 commented Nov 17, 2021

Given the original model

#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "fruit")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32,
    pub name: String,
    pub cake_id: Option<i32>,
}

Some of the possible mappings are...

  1. Mapping T to T

    • Handled by
      impl IntoActiveValue<T> for T {
          fn into_active_value(self) -> ActiveValue<T> {
              Set(self)
          }
      }
    • Well handled!
  2. Mapping Option<T> to T

    • Handled by
      impl IntoActiveValue<T> for Option<T> {
          fn into_active_value(self) -> ActiveValue<T> {
              match self {
                  Some(value) => Set(value),
                  None => Unset(None),
              }
          }
      }
    • Well handled!
  3. Mapping Option<Option<T>> to Option<T>

    • Handled by
      impl IntoActiveValue<Option<T>> for Option<Option<T>> {
          fn into_active_value(self) -> ActiveValue<Option<T>> {
              match self {
                  Some(value) => Set(value),
                  None => Unset(None),
              }
          }
      }
    • Well handled!
  4. Mapping Option<T> to Option<T>

    • Handled by
      impl IntoActiveValue<T> for Option<T> {
          fn into_active_value(self) -> ActiveValue<T> {
              match self {
                  Some(value) => Set(value),
                  None => Unset(None),
              }
          }
      }
    • Wait! Seems wrong...
      See 7cfd010 and CI log
      #[derive(DeriveIntoActiveModel)]
      pub struct NewFruit {
          pub cake_id: Option<i32>,
      }
      
      // This is fine
      assert_eq!(
          my_fruit::NewFruit {
              cake_id: Some(1),
          }
          .into_active_model(),
          fruit::ActiveModel {
              id: Unset(None),
              name: Unset(None),
              cake_id: Set(Some(1)),
          }
      );
      
      assert_eq!(
          my_fruit::NewFruit {
              cake_id: None,
          }
          .into_active_model(),
          fruit::ActiveModel {
              id: Unset(None),
              name: Unset(None),
              // Isn't this `Set(None)`?
              cake_id: Unset(None),
          }
      );
    • This could be resolved by adding... but it confused the compiler
      See 962b2fa and CI log
      impl IntoActiveValue<Option<T>> for Option<T> {
          fn into_active_value(self) -> ActiveValue<Option<T>> {
              match self {
                  Some(value) => Set(Some(value)),
                  None => Set(None),
              }
          }
      }

@billy1624
Copy link
Member

May I ask @acidic9 for help? Since he is the original author of it

@tyt2y3
Copy link
Member

tyt2y3 commented Nov 17, 2021

Thanks @billy1624 for the investigation. So there are different possibilities. In a nutshell,

  1. ActiveValue<Option<T>> -> Option<Option<T>>, can be Set(T), Set(NULL) or Unset
  2. ActiveValue<T> -> T: always map to Set(T)
  3. ActiveValue<T> -> Option<T>: can be Set(T) or Unset, but not Set(NULL)
  4. Option<T> -> Option<T>: Set(T) or Set(NULL) (this does not currently exist, and might be what the issue is asking for)

So I think there is a semantic ambiguity, but probably not a bug.

@tqwewe
Copy link
Contributor

tqwewe commented Nov 17, 2021

#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "fruit")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32,
    pub foo: String,
    pub bar: Option<i32>,
}

#[derive(DeriveIntoActiveModel)]
pub struct UpdateFruit {
    pub name: String,
    pub price: f64, // Sets bar to `Set(Some(price))`
}

#[derive(DeriveIntoActiveModel)]
pub struct UpdateFruit {
    pub name: String,
    pub price: Option<f64>, // Sets bar to `Set(price)`
}

#[derive(DeriveIntoActiveModel)]
pub struct UpdateFruit2 {
    pub name: String,
    pub price: Option<Option<f64>>, // Sets bar to `Set(Some(price))` or `Set(None`
}

I think this is the ended behaviour of the API I had in mind when writing this.. they are not all working correctly?
I haven't had time to clone this PR and test it, but I thought I'd share what I was expecting my implementation to do.

@YoshieraHuang
Copy link
Contributor Author

YoshieraHuang commented Nov 17, 2021

Hi, thanks for your concerns. After reading comments, I agree that it is not a bug, it is just an ambiguity. I think the ambiguity originates from the nullability of fields in Model. For example:

#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "fruit")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32,
    pub non_nullable: String,
    pub nullable: Option<String>
}

The current code is reasonable for nullable field:

  1. Some(Some(String)) -> Set(Some(String))
  2. Some(None) -> Set(None)
  3. Some(String) -> Set(Some(String))
  4. None -> Unset(None)

It works fine now .1 and 2 are achieved by impl IntoActiveValue<Option<T>> for Option<Option<T>>, whiile 3 and 4 are achieved by impl IntoActiveValue<Option<T>> for Option<T>.

There are some possible desired mapping from UpdateXXX to ActiveModel for non_nullable field:

  1. String -> Set(String), the string is required and should be set to field. Ok, works fine for now.
  2. None -> Unset(None). the string is not required. Let the field untouched. Ok, also fine.
  3. Some(String) -> Set(String). the string is not required but it is given, the field has to be set. Uhh, seems wrong. The type mismatches.

The 3rd of the mapping of non_nullable field conflicts with the 3rd of nullable field. However, now there is no tools to tell from each other.

@tyt2y3
Copy link
Member

tyt2y3 commented Nov 17, 2021

Yes, it appears our type system cannot properly express the idea of required but nullable.

Say, we have a ActiveValue and ActiveValueNullable instead of relying on ActiveValue<Option<T>>, then there would not be such ambiguity. But it seems to require a major overhaul. And it might be a bad idea. There could be smarter ways.

@tyt2y3
Copy link
Member

tyt2y3 commented Nov 17, 2021

Oh, there is the Deserialize macro.

So, from a serde_json standpoint, can we actually enforce the schema of required but nullable?

Seems not! Because it maps both undefined and null to None.

This thread might be helpful serde-rs/serde#984

There may not be a good derive macro solution to handle this automatically for now.

@tyt2y3 tyt2y3 force-pushed the master branch 4 times, most recently from f381a89 to d952a3e Compare November 21, 2021 09:57
@billy1624
Copy link
Member

I will revisit this after

@tyt2y3 tyt2y3 force-pushed the master branch 7 times, most recently from bc75b4b to e7d7962 Compare December 25, 2021 17:00
@tyt2y3
Copy link
Member

tyt2y3 commented Jan 27, 2022

Close it for now, we need to look for a better solution

@tyt2y3 tyt2y3 closed this Jan 27, 2022
@kun1988
Copy link

kun1988 commented Oct 31, 2023

Close it for now, we need to look for a better solution

Have a better solution now? Thanks. @tyt2y3

@franklx
Copy link

franklx commented Jan 3, 2024

I managed to implement partial updates using a custom simple Option-like type:
franklx@2585894
After testing and refinements I'll open a PR.

@LockedThread
Copy link

I wish there was a better solution for this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants