diesel-rs/diesel

Failed to save array of enums

DylanVerstraete opened this issue · 3 comments

Setup

Versions

  • 1.72.0
  • 2.1.3
  • Postgres 15.4
  • MacOs

Feature Flags

  • ["postgres", "chrono"]

Problem Description

I'm trying to persist a list of enums on an object. I'm getting a confusing error that I don't understand.

I have following rust code:

#[derive(Debug, Serialize, Deserialize, PartialEq, ToSchema, FromSqlRow, AsExpression, Clone)]
#[diesel(sql_type = crate::postgres::schema::sql_types::Action)]
pub enum Action {
    Read,
    Write,
    Delete,
    List,
    Admin,
}

// Implement ToSql for the custom enum
impl ToSql<crate::postgres::schema::sql_types::Action, Pg> for Action {
    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result {
        match *self {
            Action::Read => out.write_all(b"read")?,
            Action::Write => out.write_all(b"write")?,
            Action::Delete => out.write_all(b"delete")?,
            Action::List => out.write_all(b"list")?,
            Action::Admin => out.write_all(b"admin")?,
        }
        Ok(IsNull::No)
    }
}

// Implement FromSql for the custom enum
impl FromSql<crate::postgres::schema::sql_types::Action, Pg> for Action {
    fn from_sql(bytes: PgValue) -> deserialize::Result<Self> {
        match bytes.as_bytes() {
            b"read" => Ok(Action::Read),
            b"write" => Ok(Action::Write),
            b"delete" => Ok(Action::Delete),
            b"list" => Ok(Action::List),
            b"admin" => Ok(Action::Admin),
            _ => Err("Unrecognized enum variant".into()),
        }
    }
}

#[derive(
    Debug, Serialize, Deserialize, Identifiable, Associations, Queryable, Selectable, PartialEq,
)]
#[diesel(belongs_to(Credentials))]
#[diesel(table_name = directory_actions)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct DirectoryActions {
    pub id: i32,
    pub credentials_id: i32,
    pub directory_path: String,
    pub actions: Vec<Option<Action>>,
}

Sql code:

CREATE TYPE Action AS ENUM ('read', 'write', 'delete', 'list', 'admin'); 

CREATE TABLE 
    directory_actions (
        id SERIAL PRIMARY KEY,
        credentials_id INTEGER REFERENCES credentials(id) NOT NULL,
        directory_path VARCHAR(255) NOT NULL,
        actions Action[] NOT NULL check (actions <> '{}' and array_position(actions, null) is null)
    );

Diesel schema:

pub mod sql_types {
    #[derive(diesel::sql_types::SqlType)]
    #[diesel(postgres_type(name = "action"))]
    pub struct Action;
}

diesel::table! {
    use diesel::sql_types::*;
    use super::sql_types::Action;

    directory_actions (id) {
        id -> Int4,
        credentials_id -> Int4,
        #[max_length = 255]
        directory_path -> Varchar,
        actions -> Array<Nullable<Action>>,
    }
}

Insertion code:

    diesel::insert_into(directory_actions_schema::table)
        .values(&directory_actions)
        .execute(connection)
        .await
        .map_err(|e| {
            log::error!("Error saving credentials to db: {}", e);
            Error::FailedToCreate
        })?;

What is the expected output?

Not an error

What is the actual output?

Failed to find a type oid for action

Are you seeing any additional errors?

No

Steps to reproduce

Checklist

  • This issue can be reproduced on Rust's stable channel. (Your issue will be
    closed if this is not the case)
  • This issue can be reproduced without requiring a third party crate

Thanks for opening this bug report.

I've tried the following code:

use std::io::Write;

use diesel::connection::SimpleConnection;
use diesel::deserialize;
use diesel::deserialize::FromSql;
use diesel::deserialize::FromSqlRow;
use diesel::expression::AsExpression;
use diesel::pg::Pg;
use diesel::pg::PgValue;
use diesel::prelude::*;
use diesel::serialize;
use diesel::serialize::IsNull;
use diesel::serialize::Output;
use diesel::serialize::ToSql;

pub mod postgres {
    pub mod schema {
        pub mod sql_types {
            #[derive(diesel::sql_types::SqlType)]
            #[diesel(postgres_type(name = "action"))]
            pub struct Action;
        }

        diesel::table! {
            use diesel::sql_types::*;
            use super::sql_types::Action;

            directory_actions (id) {
                id -> Int4,
                credentials_id -> Int4,
                #[max_length = 255]
                directory_path -> Varchar,
                actions -> Array<Nullable<Action>>,
            }
        }
    }
}

#[derive(Debug, FromSqlRow, AsExpression, Clone)]
#[diesel(sql_type = crate::postgres::schema::sql_types::Action)]
pub enum Action {
    Read,
    Write,
    Delete,
    List,
    Admin,
}

// Implement ToSql for the custom enum
impl ToSql<crate::postgres::schema::sql_types::Action, Pg> for Action {
    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result {
        match *self {
            Action::Read => out.write_all(b"read")?,
            Action::Write => out.write_all(b"write")?,
            Action::Delete => out.write_all(b"delete")?,
            Action::List => out.write_all(b"list")?,
            Action::Admin => out.write_all(b"admin")?,
        }
        Ok(IsNull::No)
    }
}

// Implement FromSql for the custom enum
impl FromSql<crate::postgres::schema::sql_types::Action, Pg> for Action {
    fn from_sql(bytes: PgValue) -> deserialize::Result<Self> {
        match bytes.as_bytes() {
            b"read" => Ok(Action::Read),
            b"write" => Ok(Action::Write),
            b"delete" => Ok(Action::Delete),
            b"list" => Ok(Action::List),
            b"admin" => Ok(Action::Admin),
            _ => Err("Unrecognized enum variant".into()),
        }
    }
}

#[derive(Debug, Identifiable, Queryable, Selectable, Insertable)]
#[diesel(table_name = crate::postgres::schema::directory_actions)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct DirectoryActions {
    pub id: i32,
    pub credentials_id: i32,
    pub directory_path: String,
    pub actions: Vec<Option<Action>>,
}

fn main() {
    let mut conn = PgConnection::establish("postgres://localhost/diesel_test").unwrap();
    conn.begin_test_transaction().unwrap();

    conn.batch_execute("CREATE TYPE Action AS ENUM ('read', 'write', 'delete', 'list', 'admin');

CREATE TABLE
    directory_actions (
        id SERIAL PRIMARY KEY,
        credentials_id INTEGER  NOT NULL,
        directory_path VARCHAR(255) NOT NULL,
        actions Action[] NOT NULL check (actions <> '{}' and array_position(actions, null) is null)
    );").unwrap();

    let directory_actions = vec![DirectoryActions {
        id: 1,
        credentials_id: 1,
        directory_path: "".into(),
        actions: vec![Some(Action::Read)],
    }];

    diesel::insert_into(crate::postgres::schema::directory_actions::table)
        .values(&directory_actions)
        .execute(&mut conn)
        .unwrap();

    let res = crate::postgres::schema::directory_actions::table
        .select(DirectoryActions::as_select())
        .load(&mut conn)
        .unwrap();
    dbg!(res);
}

This runs without issues for me. I'm closing this issue as cannot reproduce. If you still hit this bug, feel free to provide a minimal self contained reproducible example (something someone can just do git clone … && cargo run).

Thanks for you swift reply, your example indeed seems to work. So something is wrong in my project and I will try to figure out what. The one thing that is different is that we are using diesel-async, could that be the problem?

The one thing that is different is that we are using diesel-async, could that be the problem?

If that's the problem you are in the wrong issue tracker here, as diesel-async is a different project. If you open an issue there please try to remember providing a complete example.