Overview

You will learn how to deal with Postgres composite types using Diesel.rs without any additional dependencies. Currently it is quite hard to look for documentation on how to do it and by writing this blog post hopefully will help someone. If you just want to see the code example, click here.

Setup

In this example, we will have a Person struct with the following properties

#[derive(Debug, Clone, Queryable, Identifiable)]
pub struct Person {
    pub id: i32,
    pub name: String,
    pub age: i32,
    pub pets: Vec<Pet>,
}


#[derive(Debug, Clone, FromSqlRow, AsExpression, PartialEq)]
#[sql_type = "PgPet"]
pub struct Pet {
    pub name: String,
    pub animal_type: AnimalType,
}


#[derive(Debug, Clone, FromSqlRow, AsExpression, PartialEq)]
#[sql_type = "PgAnimalType"]
pub enum AnimalType {
    Cat,
    Fish,
}
CREATE TYPE animal_type AS ENUM (
    'Cat',
    'Fish'
);

CREATE TYPE pet AS
(
    name        text,
    animal_type animal_type
);

CREATE TABLE persons
(
    id   serial not null
        constraint person_pk
            primary key,
    name varchar not null,
    age  int not null,
    pets pet[] not null
);

Notice that the custom types have #[sql_type = "PgType"]. This means that you will have to create types that will "point" to the real postgres type.


#[derive(SqlType)]
#[postgres(type_name = "pet")]
pub struct PgPet;

#[derive(SqlType)]
#[postgres(type_name = "animal_type")]
pub struct PgAnimalType;

Now when you generate schema.rs, it will have the wrong types. So you need to replace it with the following. Take note at the imports.

table! {
    use diesel::sql_types::*;
    use crate::PgPet;
    persons (id) {
        id -> Int4,
        name -> Varchar,
        age -> Int4,
        pets -> Array<PgPet>, //the original is Pg_pets
    }
}

If you try to compile the program the compiler will complain that you are missing some trait implementations, namely ToSql and FromSql for those types. You can implement them like so. Do note that a type is represented as a tuple from the database side. So using WriteTuple and Record<(T,T2)> works.

impl ToSql<PgAnimalType, Pg> for AnimalType {
    fn to_sql<W: Write>(&self, out: &mut Output<W, Pg>) -> serialize::Result {
        let animal_type = format!("{:?}", self);
        ToSql::<Text, Pg>::to_sql(&animal_type, out)
    }
}

impl FromSql<PgAnimalType, Pg> for AnimalType {
    fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<Self> {
        // You can find the From impl in the github repo
        FromSql::<Text, Pg>::from_sql(bytes)
            .map(|v: String| AnimalType::from(v.as_str()))
    }
}

impl ToSql<PgPet, Pg> for Pet {
    fn to_sql<W: Write>(&self, out: &mut Output<W, Pg>) -> serialize::Result {
        WriteTuple::<(Text, PgAnimalType)>::write_tuple(
            &(self.name.clone(), self.animal_type.clone()),
            out,
        )
    }
}

impl FromSql<PgPet, Pg> for Pet {
    fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<Self> {
        let (name, animal_type) = 
            FromSql::<Record<(Text, PgAnimalType)>, Pg>::from_sql(bytes)?;

        Ok(Pet { name, animal_type })
    }
}

Results

fn main() -> Result<(), Box<dyn std::error::Error>> {
    use schema::persons::dsl::*;

    let conn = establish_connection();

    let inverse = NewPerson::new(
        "iNverse",
        21,
        vec![
            Pet {
                name: "John".into(),
                animal_type: AnimalType::Cat,
            },
            Pet {
                name: "Cena".into(),
                animal_type: AnimalType::Fish,
            },
        ],
    );

    let res = insert_into(persons)
        .values(&inverse)
        .get_result::<Person>(&conn)?;

    dbg!(res);
    Ok(())
}

The code above will print the following to the console.

[src/main.rs:133] res = Person {
    id: 1,
    name: "iNverse",
    age: 21,
    pets: [
        Pet {
            name: "John",
            animal_type: Cat,
        },
        Pet {
            name: "Cena",
            animal_type: Fish,
        },
    ],
}

That's all that you have to do. Unfortunately it is quite verbose and as of now I can't find any library that simplify things.

Questions

I got an error saying I got the wrong type.

Take a look at this and try to match the types accordingly.

I am just using enums. Is there any library that can help?

Yes. Take a look at diesel-derive-enum