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.
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
FromRow
trait yourselfSince we know that we'll be using SQLite and the corr_id
is a nullable text column, we can implement FromRow
for sqlx::sqlite::SqliteRow
s. 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,
})
}
}
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
}
}