Rust Enums in SQLite with Diesel

Written by:Matsimitsu(Robert Beekman)MatsimitsuRobert Beekman

Hello, future me. Who can't remember how to do this again 👋.

This blog's back end is written in Rust, and ideally, my API is consistent and typed. For a few fields, I'd like to use a Rust Enum to encapsulate certain options, but by default, Diesel, the Rust database ORM I'm using to interface with my SQLite database, only maps to primitive types such as i32 and String.

pub enum State {
    New,
    Processed,
    Failed,
}

In order to use Enums, we need to implement a few traits. In my case, I use SQLite's TEXT field and not an Int/ TinyInt because it's for my personal blog, and here I place readability higher than efficiency.

If I open the Database with another client after two years, I don't want to read the code to see what the Int 3 was again for the state field.

First, let's ensure we can cast the enum from/to a String by implementing the Display and TryFrom traits.

impl fmt::Display for State {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            State::New => write!(f, "New"),
            State::Processed => write!(f, "Processed"),
            State::Failed => write!(f, "Failed"),
        }
    }
}

impl TryFrom<&str> for State {
    type Error = String;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        match value {
            "New" => Ok(State::New),
            "Processed" => Ok(State::Processed),
            "Failed" => Ok(State::Failed),
            _ => Err(format!("Unknown state: {}", value)),
        }
    }
}

This way, we can convert the enum to a string and back again.

assert_eq!(Ok(State::New), "New".try_into());
assert_eq!("New", State::New.to_string());

Next, we need to add a few Diesel derives to the enum:

#[derive(FromSqlRow, Debug, AsExpression)]
#[diesel(sql_type = Text)]
pub enum State {
    New,
    Processed,
    Failed,
}

This tells Diesel to use a Text field for the value, and that it can be used in expressions.

Finally, we can implement the FromSql and ToSql traits that convert the enum to the SQL type and back again. We use the previously implemented Display and TryFrom here, so our String and Database representations use the same string values.

impl FromSql<Text, Sqlite> for State {
    fn from_sql(bytes: SqliteValue) -> diesel::deserialize::Result<Self> {
        let t = <String as FromSql<Text, Sqlite>>::from_sql(bytes)?;
        Ok(t.as_str().try_into()?)
    }
}

impl ToSql<Text, Sqlite> for State {
    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> diesel::serialize::Result {
        out.set_value(self.to_string());
        Ok(diesel::serialize::IsNull::No)
    }
}

Now that we’ve implemented the traits, we can define the field as Text in the database, and use the State enum in the in- and output.

diesel::table! {
    tasks (id) {
        id -> Text,
        state -> Text,
    }
}

#[derive(Selectable, Queryable, Associations, Clone, Debug)]
pub struct Task {
    pub id: String,
    pub state: State,
}

And for ultimate lazyness, here’s the complete code:

use diesel::{
    deserialize::FromSql,
    serialize::{Output, ToSql},
    sql_types::Text,
    sqlite::{Sqlite, SqliteValue},
    AsExpression, FromSqlRow,
};
use std::fmt;

#[derive(FromSqlRow, Debug, AsExpression)]
#[diesel(sql_type = Text)]
pub enum State {
    New,
    Processed,
    Failed,
}

impl fmt::Display for State {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            State::New => write!(f, "New"),
            State::Processed => write!(f, "Processed"),
            State::Failed => write!(f, "Failed"),
        }
    }
}

impl TryFrom<&str> for State {
    type Error = String;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        match value {
            "New" => Ok(State::New),
            "Processed" => Ok(State::Processed),
            "Failed" => Ok(State::Failed),
            _ => Err(format!("Unknown state: {}", value)),
        }
    }
}

impl FromSql<Text, Sqlite> for State {
    fn from_sql(bytes: SqliteValue) -> diesel::deserialize::Result<Self> {
        let t = <String as FromSql<Text, Sqlite>>::from_sql(bytes)?;
        Ok(t.as_str().try_into()?)
    }
}

impl ToSql<Text, Sqlite> for State {
    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> diesel::serialize::Result {
        out.set_value(self.to_string());
        Ok(diesel::serialize::IsNull::No)
    }
}

Like

Webmentions

No mentions yet.