How can I derive Queryable for a type with a custom field that maps to more than one column with diesel?

Viewed 1341

I am using the Diesel crate to perform some database work. In some tables, two columns of the table should be treated together as a single key.

This pattern is repeated in many places in the database, so it would be nice to avoid a heap of repeated copy-paste code to handle this. However, I can't convince Diesel to automatically produce a type I can use in queries or inserts.

Consider the table

table! {
    records (iid) {
        iid -> Integer,
        id_0 -> BigInt,
        id_1 -> BigInt,
        data -> Text,
    }
}

and the ideal types

#[derive(Debug, Copy, Clone, FromSqlRow)]
pub struct RecordId {
    id_0: i64,
    id_1: i64,
}

#[derive(Queryable, Debug)]
pub struct Record {
    pub iid: i32,
    pub id: RecordId,
    pub data: String,
}

This code compiles OK, but when I try to use it I get an error, for example:

pub fn find(connection: &SqliteConnection) -> types::Record {
    records
        .find(1)
        .get_result::<types::Record>(connection)
        .unwrap()
}

produces:

error[E0277]: the trait bound `(i32, types::RecordId, std::string::String): diesel::Queryable<(diesel::sql_types::Integer, diesel::sql_types::BigInt, diesel::sql_types::BigInt, diesel::sql_types::Text), _>` is not satisfied
  --> src/main.rs:76:21
   |
76 |     records.find(1).get_result::<types::Record>(connection).unwrap()
   |                     ^^^^^^^^^^ the trait `diesel::Queryable<(diesel::sql_types::Integer, diesel::sql_types::BigInt, diesel::sql_types::BigInt, diesel::sql_types::Text), _>` is not implemented for `(i32, types::RecordId, std::string::String)`
   |
   = help: the following implementations were found:
             <(A, B, C) as diesel::Queryable<(SA, SB, SC), __DB>>
             <(A, B, C) as diesel::Queryable<diesel::sql_types::Record<(SA, SB, SC)>, diesel::pg::Pg>>
   = note: required because of the requirements on the impl of `diesel::Queryable<(diesel::sql_types::Integer, diesel::sql_types::BigInt, diesel::sql_types::BigInt, diesel::sql_types::Text), _>` for `types::Record`
   = note: required because of the requirements on the impl of `diesel::query_dsl::LoadQuery<_, types::Record>` for `diesel::query_builder::SelectStatement<types::records::table, diesel::query_builder::select_clause::DefaultSelectClause, diesel::query_builder::distinct_clause::NoDistinctClause, diesel::query_builder::where_clause::WhereClause<diesel::expression::operators::Eq<types::records::columns::iid, diesel::expression::bound::Bound<diesel::sql_types::Integer, i32>>>>`

If I create a version which does not contain the RecordId but has the sub-pieces directly, then there is no error:

pub struct RecordDirect {
    pub iid: i32,
    pub id_0: i64,
    pub id_1: i64,
    pub data: String,
}

// ...

pub fn find_direct(connection: &SqliteConnection) -> types::RecordDirect {
    records
        .find(1)
        .get_result::<types::RecordDirect>(connection)
        .unwrap()
}

Similarly, I can manually implement the Queryable trait and that works OK too,

#[derive(Debug)]
pub struct RecordManual {
    pub iid: i32,
    pub id: RecordId,
    pub data: String,
}

impl Queryable<records::SqlType, diesel::sqlite::Sqlite> for RecordManual {
    type Row = (i32, i64, i64, String);
    fn build(row: Self::Row) -> Self {
        RecordManual {
            iid: row.0,
            id: RecordId {
                id_0: row.1,
                id_1: row.2,
            },
            data: row.3,
        }
    }
}

// ...

pub fn find_manual(connection: &SqliteConnection) -> types::RecordManual {
    records
        .find(1)
        .get_result::<types::RecordManual>(connection)
        .unwrap()
}

This case is ugly to maintain and I could not work out how to get it working for insertion — manually implementing Insertable seems a little trickier than Queryable.

To make this easier for anyone looking at it to play with, I've created a repository containing an almost compiling small reproducer containing the code blocks from this post. (Normally I'd put it up on the rust-playground, but that doesn't support diesel). You can find that code at https://github.com/mikeando/diesel_custom_type_demo.

Is there a way to make the #[derive(Queryable)] (and #[derive(Insertable)]) work for these kinds of cases?


A minimal reproducer for the failing initial case is:

#[macro_use]
extern crate diesel;

use diesel::prelude::*;

mod types {
    use diesel::deserialize::Queryable;
    use diesel::sqlite::SqliteConnection;

    table! {
        records (iid) {
            iid -> Integer,
            id_0 -> BigInt,
            id_1 -> BigInt,
            data -> Text,
        }
    }

    #[derive(Debug, Copy, Clone, FromSqlRow)]
    pub struct RecordId {
        id_0: i64,
        id_1: i64,
    }

    // Using a RecordId in a Record compiles, but 
    // produces an error when used in an actual query
    #[derive(Queryable, Debug)]
    pub struct Record {
        pub iid: i32,
        pub id: RecordId,
        pub data: String,
    }
}

use types::records::dsl::*;

pub fn find(connection:&SqliteConnection) -> types::Record {
    records.find(1).get_result::<types::Record>(connection).unwrap()
}
1 Answers

Is there a way to make the #[derive(Queryable)] (and #[derive(Insertable)]) work for these kinds of cases?

For #[derive(Insertable)] this is simply possible by adding a #[diesel(embedded)] to your id field and a #[derive(Insertable)] on both structs. See the documentation of Insertable for details. For #[derive(Queryable)] this is not possible because Queryable is supposed to be a plain mapping from query result to struct, with the basic assumption that the "shape" of the output remains the same (at least for the derive).

Related