Search code examples
sqliterustrust-diesel

How to properly convert bytes to Rust enum with Diesel FromSql trait?


I'm working with Diesel library and Sqlite database, trying to create Rust enum which saves to database as diesel::sql_types::Text, but even with example code it doesn't work as it should.

Here is example code.

My code:

#[derive(PartialEq, Eq, AsExpression, FromSqlRow, Serialize, Deserialize, Clone, Debug)]
#[diesel(sql_type = diesel::sql_types::Text)]
pub enum InspectionType {
    #[serde(rename = "RUNWAY")]
    Runway,

    #[serde(rename = "PAPI")]
    Papi,
}

impl serialize::ToSql<Text, Sqlite> for InspectionType {
    fn to_sql(&self, out: &mut serialize::Output<Sqlite>) -> serialize::Result {
        match *self {
            InspectionType::Runway => out.write_all(b"RUNWAY")?,
            InspectionType::Papi => out.write_all(b"PAPI")?,
        }

        Ok(IsNull::No)
    }
}

impl deserialize::FromSql<Text, Sqlite> for InspectionType {
    fn from_sql(bytes: SqliteValue) -> deserialize::Result<Self> {
        match bytes.as_bytes() {
            b"RUNWAY" => Ok(InspectionType::Runway),
            b"PAPI" => Ok(InspectionType::Papi),
            _ => Err("Unrecognized enum variant".into()),
        }
    }
}

Compiler error:

error[E0599]: no method named `as_bytes` found for struct `SqliteValue` in the current scope
  --> src/app_data/inspection.rs:40:21
   |
40 |         match bytes.as_bytes() {
   |                     ^^^^^^^^ method not found in `SqliteValue<'_, '_, '_>`

Solution

  • It's kind of expected that an example that is written for postgresql does not work for sqlite, as many details differ between these two backends. The important part here is how bind values are passed to the database. For postgresql they are serialized as byte buffer and send over network, for sqlite, which runs in your binary the value is just passed to the database. In addition different databases support different sets of types. Postgresql supports an open set of types, which means your are able to define your own types. For sqlite you are restricted to a small closed set of types, that cannot be extended. That means you need to serialize your data to an actual already existing type.

    Now what that this mean for your example?

    Let's start locking at the relevant documentation for FromSql. Especially that part is important:

    For SQLite, the actual type of DB::RawValue is private API. All implementations of this trait must be written in terms of an existing primitive.

    This means there is no as_bytes() method as pointed out by the compiler. You need to internally use one of the existing implementations. In this case that would be that one for String. This would leave you with an implementation like that one:

    impl deserialize::FromSql<Text, Sqlite> for InspectionType {
        fn from_sql(bytes: SqliteValue) -> deserialize::Result<Self> {
            let value = <String as deserialize::FromSql<Text, Sqlite>>::from_sql(bytes)?;
            match &value as &str {
                "RUNWAY" => Ok(InspectionType::Runway),
                "PAPI" => Ok(InspectionType::Papi),
                _ => Err("Unrecognized enum variant".into()),
            }
        }
    }