Using enums with Diesel
On this page
Using custom enums with Diesel requires implementation of two traits for the enum: FromSql
and ToSql
.
- The
FromSql
pattern involves matching primitives and then constructing the enum. - The
ToSql
pattern involves matching the enum and then delegating construction to the primitive’sto_sql
method.
Why
- The database columns store raw bytes of data, so if we want to store data there, we need to represent it as raw bytes before writing it.
- Diesel expects us to show it how we want to convert our enum into the bytes that our database wants.
- We need to implement the
FromSql
andToSql
traits because there’s no direct correlation between a Rust enum and a Postgresql type (not even an enum type).
What’s the alternative?
Converting straight to bytes tends to be complex and error-prone because we’d have to write to some buffers and possibly incorporate some unsafe stuff. As an example you should NOT follow, here’s how we might deal with this without using diesel’s traits:
// An unsafe, error-prone approach without Diesel
impl AccountType {
fn to_raw_bytes(&self) -> Result<Vec<u8>, &'static str> {
// Convert enum to i16 first
let num: i16 = match *self {
AccountType::Guest => 0,
AccountType::Member => 1,
AccountType::Moderator => 2,
AccountType::Admin => 3,
};
// Create buffer - i16 needs 2 bytes
let mut bytes = vec![0u8; 2];
// Handle endianness (are we little or big?)
// Different DBs might expect different byte orders
let num_bytes = if cfg!(target_endian = "little") {
num.to_le_bytes()
} else {
num.to_be_bytes()
};
// Copy bytes to our buffer
// This could panic if sizes mismatch
bytes.copy_from_slice(&num_bytes);
// Hope we got alignment right
// Hope we handled platform differences correctly
// Hope we didn't mess up the byte ordering
// ...and many more edge cases to worry about
Ok(bytes)
}
}
Using diesel, though, we can just:
impl<DB> ToSql<SmallInt, DB> for AccountType
where
DB: Backend,
i16: ToSql<SmallInt, DB>,
{
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> serialize::Result {
match *self {
AccountType::Guest => 0_i16.to_sql(out),
AccountType::Member => 1_i16.to_sql(out),
AccountType::Moderator => 2_i16.to_sql(out),
AccountType::Admin => 3_i16.to_sql(out)
}
}
}
This lets us rely on diesel for the byte-level stuff so that we can focus on our domain logic.
Example
Let’s say we have an enum to represent some account’s type:
#[repr(i16)]
#[derive(AsExpression, Debug, Clone, Copy, Serialize, Deserialize, FromSqlRow)]
#[diesel(sql_type = SmallInt)]
pub enum AccountType {
Guest = 0,
Member = 1,
Moderator = 2,
Admin = 3
}
Here we configure the enum AccountType
to be represented as an i16
, which plays nicely with
Postresql’s native i16
type.
To use this in query and insert statements, we must implement FromSql
and ToSql
respectively.
Making an enum queryable
To make this enum queryable, we implement FromSql
for it. To do that, we’ll match
on the i16
primitive coming from the database and return its corresponding enum equivalent:
impl<DB> FromSql<SmallInt, DB> for AccountType
where
DB: Backend,
i16: FromSql<SmallInt, DB>,
{
fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result<Self> {
match i16::from_sql(bytes)? {
0 => Ok(AccountType::Guest),
1 => Ok(AccountType::Member),
2 => Ok(AccountType::Moderator),
3 => Ok(AccountType::Admin),
x => Err(format!("Unrecognized variant {}", x).into())
}
}
}
Making an enum insertable
To make this enum insertable, we implement ToSql
for it. To do that, we’ll
match on the enum and then delegate to the primitive’s to_sql
:
impl<DB> ToSql<SmallInt, DB> for AccountType
where
DB: Backend,
i16: ToSql<SmallInt, DB>,
{
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> serialize::Result {
match *self {
AccountType::Guest => 0_i16.to_sql(out),
AccountType::Member => 1_i16.to_sql(out),
AccountType::Moderator => 2_i16.to_sql(out),
AccountType::Admin => 3_i16.to_sql(out)
}
}
}