How can I return any subset of a struct?

As an exercise, I'm trying to port a REST API to Rust using Rocket (web framework) and Diesel (ORM), but I can't figure how to return only some fields of the struct that represents some queryable data according to the user input.

In a first time, I'm just trying to return a partial subset of the struct without processing any user selection, but a subset could be any combination of the struct fields.

See below the relevant code. It compiles as long as I don't select some fields in the function item().

#[derive(Debug, Clone, Deserialize, Serialize, Queryable)]
#[serde(crate = "rocket::serde")]
struct Item {
  id: i32,
  name: String,
  description: String,
  link: String
}

table! {
    items (id) {
        id -> Integer,
        name -> Text,
        description -> Text,
        link -> Text,
    }
}

#[get("/item/<input_id>")]
async fn item(db: DbConn, input_id: i32) -> Option<Json<Item>>
    db.run(move |db| {
        items.select((name, description)) // doesn't match the declared return type
             .filter(id.eq(input_id))
             .first(db)
    }).await.map(Json).ok()
}

You would need to define a new struct, or to make the fields be Options so you can set them to None for the missing values.

1 Like

I played with Option but I couldn't make it work. In fact, the problem seems more related to Diesel than to Rust syntax.

The following error states that the selected portion of the schema doesn't fit the the struct:

the trait `Queryable<(diesel::sql_types::Text, diesel::sql_types::Text), Sqlite>` is not implemented for `(i32, std::string::String, std::string::String, std::string::String)`

If I add Options to the struct and Nullables counterparts to the schema, I got quite the same error:

the trait `Queryable<(diesel::sql_types::Nullable<diesel::sql_types::Text>, diesel::sql_types::Nullable<diesel::sql_types::Text>,), Sqlite>` is not implemented for `(std::option::Option<i32>, std::option::Option<std::string::String>, std::option::Option<std::string::String>, std::option::Option<std::string::String>)`

The problem here is that your query returns 2 fields while your struct has 4 fields. There is no way for diesel to tell how to initialize the other two fields. As general rule of thumb each struct implementing Queryable needs to have exactly as much fields as returned by the corresponding query, in exactly that order. Just changing the type to Option<T> is not allowed.

This leaves you 3 options here:

  • Define a custom return type, consisting of two fields
  • Use a 2-element tuple with matching types as return type
  • Reconstruct your query in such a way that it actually returns 4 fields with matching types. If you are not interested in the id and link field you can just select null values at that position. (I do not encourage this variant, I've list it just for completeness)

More generally speaking: Don't thread #[derive(Queryable)] as some kind of classical model, known from other ORM's. That's not how this thing works. It's really only a way to map the result of queries returning the same set of types to a rust data structure, nothing more.

I see, but if I must return the subset explicitly, I'll need to create one function for each combination of fields, as the consumer of the API would call any of them:

name
name, description
name, description, link
name, link
...

Not really feasible, especially if the database becomes more complex. Otherwise I though I could filter the result after selecting all the fields from the database, but isn't it a bad design to collect extra data for deleting them right after?

And please, can you explain why the third - discouraged - solution is so bad?

So the underlying problem is that you try to do something diesel is not designed for. One main goal of diesel is to check as much information as possible at compile time. This includes that return types of a query match the data structure the result is deserialized into. For your use case this information is only available at run time, so that does not really fit nicely together. It's possible to workaround this "limitation" by a pile of generic code + some code generation to translate the runtime information at some point into compile time information so that you get exactly that list of combinations. As this is really complex and likely compile time intensive I wouldn't recommend going down that way. Depending on your use case you should ask yourself if you really need that level of flexibility. If the answer is yes you likely don't want to use diesels default interface as it's just not designed for this use case. You either can roll out your own query building mechanism on top of diesels fundamental types (I do not have an example for that, but I know of people that have done this) or you need to use something else that fits your needs in a better way.

It's not bad, just discouraged, as you loose quite a bit of type safety going down that way. For example you cannot easily decide if there was a real null value returned from the database or if you set that null value came from the data you supplied, at least not without inspecting the query in detail.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.