2

I have the following code:

    // cargo.toml:
    // serde = { version = "1.0", features = ["derive"] }
    // uuid = { version = "1.2", features = ["serde", "v4"] }
    // sqlx = { version = "0.6.2", features = ["runtime-async-std-native-tls", "sqlite", "postgres", "chrono", "uuid", "macros"]}

    use uuid::{Uuid, fmt::Hyphenated};
    use serde::{Deserialize, Serialize};
    
    #[derive(Debug, Serialize, Deserialize, FromRow, PartialEq)]
    pub struct Transaction {
      #[sqlx(try_from = "Hyphenated")]
      pub t_id: Uuid,
      #[sqlx(try_from = "Option<Hyphenated>")]
      pub corr_id: Option<Uuid>,
    }

In SQLite database, id stored in hyphenated format like "550e8400-e29b-41d4-a716-446655440000". t_id not null, corr_id nullable. macro #[sqlx(try_from = "Hyphenated")] works fine, but I cant figure out, how to use it with Option for corr_id field. Given code panics. Any help is greatly appreciated.

Ilya Dyachenko
  • 95
  • 1
  • 2
  • 5

1 Answers1

2

The compiler tells us that it cannot implicitly convert Hyphenated to an Option<Uuid>,

| #[derive(Debug, Serialize, Deserialize, FromRow, PartialEq)]
|                                         ^^^^^^^ the trait `From<Hyphenated>` is not implemented for `std::option::Option<Uuid>`

and we can't implement external traits for external types. The only choices left seem to be

1. Implement the FromRow trait yourself

Since we know that we'll be using SQLite and the corr_id is a nullable text column, we can implement FromRow for sqlx::sqlite::SqliteRows. If your struct (row) only has these two fields, this is fine but when extending it with additional fields, you'll need to update your FromRow implementation as well.

use sqlx::{sqlite::SqliteRow, FromRow, Row};
use uuid::{fmt::Hyphenated, Uuid};

pub struct Transaction {
    pub t_id: Uuid,
    pub corr_id: Option<Uuid>,
}

impl<'r> FromRow<'r, SqliteRow> for Transaction {
    fn from_row(row: &'r SqliteRow) -> Result<Self, sqlx::Error> {
        let t_id: Hyphenated = row.try_get("t_id")?;
        let corr_id: &str = row.try_get("corr_id")?;
        let corr_id = if corr_id.is_empty() {
            None
        } else {
            let uuid = Uuid::try_parse(&corr_id).map_err(|e| sqlx::Error::ColumnDecode {
                index: "corr_id".to_owned(),
                source: Box::new(e),
            })?;
            Some(uuid)
        };
        Ok(Transaction {
            t_id: t_id.into(),
            corr_id,
        })
    }
}

2. Use a newtype

This way, you can reuse your "nullable" type in other structs if necessary, and can even implement Deref, if you want to make extracting the inner UUID easier. It does come with some extra allocations though, since the incoming bytes are converted first to String, then parsed into Uuid.

use std::ops::Deref;
use sqlx::FromRow;

#[derive(FromRow)]
pub struct Transaction {
    #[sqlx(try_from = "Hyphenated")]
    pub t_id: Uuid,
    #[sqlx(try_from = "String")]
    pub corr_id: NullableUuid,
}

pub struct NullableUuid(Option<Uuid>);

impl TryFrom<String> for NullableUuid {
    type Error = uuid::Error;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        let inner = if value.is_empty() {
            None
        } else {
            let uuid = Uuid::try_parse(&value)?;
            Some(uuid)
        };
        Ok(NullableUuid(inner))
    }
}

impl Deref for NullableUuid {
    type Target = Option<Uuid>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}
Szigeti Péter
  • 160
  • 1
  • 4
  • 1
    Peter, Thanks a lot! With little adjustments both of your suggestions works! I choose approach with new type. It was unhappy giving error `the trait bound NullableUuid: From> is not satisfied required for std::option::Option<_> to implement Into` Fixed with `impl From – Ilya Dyachenko Jan 26 '23 at 23:27
  • 1
    Thanks for the heads up, I fixed the example so it actually compiles now (without needing `From`) – Szigeti Péter Jan 27 '23 at 10:41