Search code examples
rustrust-diesel

How do I implement Queryable and Insertable for custom field types in Diesel?


I have an SQL table that I want to work with through Diesel:

CREATE TABLE records (
    id BIGSERIAL PRIMARY KEY,
    record_type SMALLINT NOT NULL,
    value DECIMAL(10, 10) NOT NULL
)

This table generates the following schema:

table! {
    records (id) {
        id -> Int8,
        record_type -> Int2,
        value -> Numeric,
    }
}

Diesel exports decimals as bigdecimal::BigDecimal, but I'd like to work with decimal::d128 instead. I also want to map record_type to an enum, so I declare my model like this:

use decimal::d128;

pub enum RecordType {
    A,
    B,
}

pub struct Record {
    pub id: i64,
    pub record_type: RecordType,
    pub value: d128,
}

I can't use #derive(Queryable, Insertable) because of non-standard type mapping, so I try to implement these traits myself:

impl Queryable<records::SqlType, Pg> for Record {
    type Row = (i64, i16, BigDecimal);

    fn build(row: Self::Row) -> Self {
        Record {
            id: row.0,
            record_type: match row.1 {
                1 => RecordType::A,
                2 => RecordType::B,
                _ => panic!("Wrong record type"),
            },
            value: d128!(format!("{}", row.2)),
        }
    }
}

I can't figure out how to implement Insertable. What is the Values associated type? Diesel's documentation is not very clear on this.

Maybe there's a better way to achieve what I'm trying to do?

Cargo.toml:

[dependencies]
bigdecimal = "0.0.10"
decimal = "2.0.4"
diesel = { version = "1.1.1", features = ["postgres", "bigdecimal", "num-bigint", "num-integer", "num-traits"] }
dotenv = "0.9.0"

Solution

  • I find it more convenient to create newtype wrappers that implement ToSql and FromSql. You can then build with these basic blocks to create larger types that can derive Queryable / Insertable.

    This example only shows how to perform the mapping of the enum to and from a SmallInt, but the case for the decimal would be the same. The only difference would be in how you perform the transformations:

    #[macro_use]
    extern crate diesel;
    
    mod types {
        use diesel::sql_types::*;
        use diesel::backend::Backend;
        use diesel::deserialize::{self, FromSql};
        use diesel::serialize::{self, ToSql, Output};
        use std::io;
    
        table! {
            records (id) {
                id -> BigInt,
                record_type -> SmallInt,
            }
        }
    
        #[derive(Debug, Copy, Clone, AsExpression, FromSqlRow)]
        #[sql_type = "SmallInt"]
        pub enum RecordType {
            A,
            B,
        }
    
        impl<DB: Backend> ToSql<SmallInt, DB> for RecordType
        where
            i16: ToSql<SmallInt, DB>,
        {
            fn to_sql<W>(&self, out: &mut Output<W, DB>) -> serialize::Result
            where
                W: io::Write,
            {
                let v = match *self {
                    RecordType::A => 1,
                    RecordType::B => 2,
                };
                v.to_sql(out)
            }
        }
    
        impl<DB: Backend> FromSql<SmallInt, DB> for RecordType
        where
            i16: FromSql<SmallInt, DB>,
        {
            fn from_sql(bytes: Option<&DB::RawValue>) -> deserialize::Result<Self> {
                let v = i16::from_sql(bytes)?;
                Ok(match v {
                    1 => RecordType::A,
                    2 => RecordType::B,
                    _ => return Err("replace me with a real error".into()),
                })
            }
        }
    
        #[derive(Insertable, Queryable, Debug)]
        #[table_name = "records"]
        pub struct Record {
            pub id: i64,
            pub record_type: RecordType,
        }
    }
    

    There's a draft guide describing all the derives and their annotations, but it doesn't yet mention #[sql_type] for an entire type. This lets Diesel know what kind of underlying storage is needed inside of the database.

    See also the Diesel tests for custom types.