Skip to content

Direct conversion from row-data into concrete struct via conversion functions #1089

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

Closed
elkowar opened this issue Mar 9, 2021 · 5 comments
Closed

Comments

@elkowar
Copy link

elkowar commented Mar 9, 2021

Currently, query_as! gives us a way to map the queried fields into a very basic representation of the row-data. In most cases, this is not exactly what we need, thus we either manually .map the anonymous record into our business-datatype, or we change our data representation - or create two structs with a .into.

Optimally, my proposal would be to allow for #[sqlx::convert = some_func] attributes on each fields of a struct, such that query_as! (or something equivalent) could then convert the values into our business-data directly.

Example:

Currently, we'd have to do something like this:

struct Foo(i32);
struct Bar(String);
enum Thing { X, Y }

struct MyData {
  foo: Foo,
  bar: Bar,
  thing: Thing
}

// in some query:
query!("SELECT * from whatever where stuff")
  .fetch_one(&mut conn)
  .await
  .map(|x| MyData {
    foo: Foo(x.foo as i32),
    bar: Bar(x.bar),
    thing: match x.thing {
      "x": Thing::X,
      "y": Thing::Y,
      _ => panic!("This is just for demo, please don't actually panic :/")
  })
  .other_stuff()

Given the same types, my proposal would be to have:

struct Foo(i32);
impl Foo {
  fn from_i64(i: i64) -> Self {
    Foo(i as i32) // obviously this is kind of ugly, but ehhhhh
  }
}
struct Bar(String);
enum Thing { X, Y }
impl FromStr for Thing {
  type Err = anyhow::Error;
  fn from_str(s: &str) -> Result<Self, Self::Err> {
    match s {
      "x": Ok(Thing::X),
      "y": Ok(Thing::Y),
      _ => Err(anyhow!("This is broken :/")
    }
  }
}

#[derive(sqlx::QueryAsMagic)]
struct MyData {
  #[sqlx::convert = Foo::from_i64]
  foo: Foo,
  #[sqlx::convert = Bar] // struct constructor is enough to convert here
  bar: Bar,
  
  // for more complex values, conversion may fail.
  // thus, a try_convert option is important - the new query_as-version should thus return a result value 
  #[sqlx::try_from_str] // directly use FromStr implementation of the type
  // alternatively, less specific:
  #[sqlx::try_convert = Thing::from_str]
  thing: Thing
}

// now, in the same query function:
// (the MyData could be a turbofish on a non-macro version, i'd assume - as the macro magic would just implement some trait.
let result = query_into!(MyData, "SELECT * from whatever where stuff")
  .fetch_one(&mut conn)
  .await;
match result {
  Err(QueryErr::ConversionFailed(err)) => {} // err is the error returned by the conversion
  Err(QueryErr::OtherStuffIdk(other_err)) => {} // other failures that are possible, i.e. connection errors, or whatever  exists already
  Ok(value) => {
    // epic, we have our value of type MyData!
  }
}

Implementation wise i'd say that this could be done by generating a trait implementation that either pretty much does FromRow,
or by basing on query_as - first generating a named struct with fields of the basic types, then generating a conversion from that raw data struct into the business object.

@elkowar
Copy link
Author

elkowar commented Mar 9, 2021

Optimally, things like optionals could be handled magically, too:
a try_convert = Whatever::from_str attribute on a Option<Whatever> field should be treated like value.map(|x| Whatever::from_str(x)).transpose()? (meaning that it applies the conversion inside of the Option automatically, without the function having to be specifically for Option<T>

@mehcode
Copy link
Member

mehcode commented Mar 9, 2021

Unfortunately this is completely impossible until Rust has stronger constant execution facilities. Notably we need const functions in traits.

See #514 to track the status of this.

@mehcode mehcode closed this as completed Mar 9, 2021
@mehcode
Copy link
Member

mehcode commented Mar 9, 2021

We did make a recent large improvement to map in 0.5

Given:

query!("SELECT * from whatever where stuff")
  .fetch_one(&mut conn)
  .await
  .map(|x| MyData {
    foo: Foo(x.foo as i32),
    bar: Bar(x.bar),
    thing: match x.thing {
      "x": Thing::X,
      "y": Thing::Y,
      _ => panic!("This is just for demo, please don't actually panic :/")
  })
  .other_stuff()

You can now do:

query!("SELECT * from whatever where stuff")
  .map(|x| MyData {
    foo: Foo(x.foo as i32),
    bar: Bar(x.bar),
    thing: match x.thing {
      "x": Thing::X,
      "y": Thing::Y,
      _ => panic!("This is just for demo, please don't actually panic :/")
  })
  .fetch_one(&mut conn)
  // other fetch_ variants can be used with the above map as well
  // .fetch_all(&mut conn) 
  .await
  .other_stuff()

@mehcode
Copy link
Member

mehcode commented Mar 9, 2021

I want to note that I don't intend to dismiss your specific ideas for derive(QueryAsMagic). The ideal would be to extend FromRow to support the things you are discussing and eventually use FromRow for query_as! (similarly to query_as).

If you want to push some of those ideas (in a separate issue) into extending FromRow now, it will eventually get to where you want it (in query_as!).

@elkowar
Copy link
Author

elkowar commented Mar 9, 2021

Ahh, well, sad i guess,.... I may do a separate issue to extend FromRow for now, yea! Thanks anyways.

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

No branches or pull requests

2 participants