rustrust-sqlx

Rust: sqlx try_from Option<Uuid>


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.


Solution

  • 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
        }
    }