Skip to content

Commit

Permalink
feat: ulid sqlx type
Browse files Browse the repository at this point in the history
This introduces a Ulid struct which implements
sqlx::Type<sqlx::Postgres>, meaning it can be decoding from queries.
Support in sqlx for these custom types seems not entirely fleshed out,
but with some extra code at query time this works fine.
  • Loading branch information
justinrubek committed Mar 26, 2023
1 parent 781735c commit 7cbf755
Show file tree
Hide file tree
Showing 8 changed files with 323 additions and 10 deletions.
151 changes: 150 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/models/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ scylla-dynamodb = { path = "../scylla-dynamodb" }
thiserror = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
ulid = { workspace = true }
lockpad-ulid = { path = "../ulid" }
sqlx = { version = "0.6.3", features = ["offline", "runtime-tokio-rustls", "postgres", "time", "macros", "uuid"] }
54 changes: 52 additions & 2 deletions crates/models/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
use lockpad_ulid::Ulid;
use sqlx::types::Uuid;

pub mod application;
pub mod entity;
pub mod error;
pub mod user;

#[derive(Debug)]
pub struct User {
pub user_id: Option<lockpad_ulid::Ulid>,
// pub user_id: Option<sqlx::types::Uuid>,
pub name: String,
}

pub async fn temp_example() {
use sqlx::postgres::PgPool;
use std::env;
Expand All @@ -11,8 +21,48 @@ pub async fn temp_example() {
.await
.unwrap();

let _query = sqlx::query!("SELECT name FROM users")
.fetch_one(&pool)
// let query = sqlx::query!(r#"SELECT user_id::TEXT as "user_id: Ulid" FROM users"#)
// let query = sqlx::query!(r#"SELECT user_id::UUID as "user_id: Uuid" FROM users"#)

let insert = sqlx::query!(
r#"INSERT INTO users (name, secret) VALUES ($1, $2)"#,
"test",
"secret"
)
.execute(&pool)
.await
.unwrap();

let generated_id = Ulid::generate();
let generated_uuid = Uuid::from(generated_id);
let generated_string = generated_id.to_string();
let uuid_string = generated_uuid.to_string();
let second_insert = sqlx::query!(
r#"
INSERT INTO
users(user_id, name, secret)
SELECT user_id::uuid, name, secret
FROM(
VALUES($1, $2, $3)
) AS data(user_id, name, secret)
"#,
uuid_string,
"generated-test",
"secret"
)
.execute(&pool)
.await
.unwrap();

let query = sqlx::query!(r#"SELECT user_id::UUID as "user_id: Ulid" FROM users"#)
// let query = sqlx::query_as!(User, r#"SELECT name, user_id::UUID as "user_id: Ulid" FROM users"#)
.fetch_all(&pool)
.await
.unwrap();

query.iter().for_each(|user| {
println!("{:?}", user);
let id = user.user_id.unwrap();
println!("{:?}", id);
});
}
14 changes: 14 additions & 0 deletions crates/ulid/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "lockpad-ulid"
version.workspace = true
edition.workspace = true

[dependencies]
anyhow.workspace = true
rusty_ulid = { version = "2.0.0", features = ["chrono", "rand", "serde"], default-features = false }
sqlx = { version = "0.6.3", features = ["offline", "runtime-tokio-rustls", "postgres", "time", "macros", "uuid"] }
# serde.workspace = true
# serde_json = "1.0.87"
thiserror.workspace = true
tokio.workspace = true
tracing.workspace = true
7 changes: 7 additions & 0 deletions crates/ulid/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("todo")]
Todo,
}

pub type Result<T> = std::result::Result<T, Error>;
66 changes: 66 additions & 0 deletions crates/ulid/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use sqlx::{encode::IsNull, postgres::PgHasArrayType, Decode, Encode};
pub mod error;

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Ulid(rusty_ulid::Ulid);

impl Ulid {
pub fn generate() -> Self {
Ulid(rusty_ulid::Ulid::generate())
}
}

impl From<Ulid> for rusty_ulid::Ulid {
fn from(ulid: Ulid) -> Self {
ulid.0
}
}

impl std::str::FromStr for Ulid {
type Err = rusty_ulid::DecodingError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
rusty_ulid::Ulid::from_str(s).map(Ulid)
}
}

impl std::fmt::Display for Ulid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

impl sqlx::Type<sqlx::Postgres> for Ulid {
fn type_info() -> sqlx::postgres::PgTypeInfo {
sqlx::postgres::PgTypeInfo::with_name("ulid")
}
}

impl PgHasArrayType for Ulid {
fn array_type_info() -> sqlx::postgres::PgTypeInfo {
sqlx::postgres::PgTypeInfo::with_name("_ulid")
}
}

impl Encode<'_, sqlx::Postgres> for Ulid {
fn encode_by_ref(&self, buf: &mut sqlx::postgres::PgArgumentBuffer) -> IsNull {
let bytes: [u8; 16] = self.0.into();
buf.extend_from_slice(&bytes);
IsNull::No
}
}

impl Decode<'_, sqlx::Postgres> for Ulid {
fn decode(value: sqlx::postgres::PgValueRef<'_>) -> Result<Self, sqlx::error::BoxDynError> {
let bytes = value.as_bytes()?;
let ulid = rusty_ulid::Ulid::try_from(bytes)?;
Ok(Ulid(ulid))
}
}

impl std::convert::From<Ulid> for sqlx::types::Uuid {
fn from(ulid: Ulid) -> Self {
let bytes: [u8; 16] = ulid.0.into();
sqlx::types::Uuid::from_bytes(bytes)
}
}
Loading

0 comments on commit 7cbf755

Please sign in to comment.