Skip to content

Commit

Permalink
feat(uuidv7): add support for extracting timestamps
Browse files Browse the repository at this point in the history
Up until now extracting timestampts were not available for uuidv7
UUIDs generated with pg_idkit.

This commit add support for extrating timestampts from UUIDv7 values,
and also refactors to align on support from the `uuid` crate's uuidv7
implementation, rather than using `uuidv7`.

Signed-off-by: vados <[email protected]>
  • Loading branch information
t3hmrman committed Dec 7, 2023
1 parent bddbd5d commit 9e52321
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 27 deletions.
18 changes: 0 additions & 18 deletions Cargo.lock

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

3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ pushid = "0.0.1"
sonyflake = "0.2"
timeflake-rs = "0.3"
ulid = "1.1"
uuid7 = "0.7"
uuid = { version = "1.6", features = [ "v6" ] }
uuid = { version = "1.6", features = [ "v6", "v7" ] }
xid = "1.0"
miette = { version = "5.10", features = ["fancy"] }
time = "0.3"
Expand Down
83 changes: 76 additions & 7 deletions src/uuid_v7.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
use pgrx::*;
use uuid7::uuid7;
use std::io::{Error as IoError, ErrorKind};
use std::str::FromStr;

use chrono::NaiveDateTime;
use pgrx::pg_extern;
use uuid::Uuid;

use crate::common::{naive_datetime_to_pg_timestamptz, OrPgrxError};

/// Generate a new UUIDv6
fn new_uuidv7() -> Uuid {
Uuid::now_v7()
}

/// Generate a UUID v7
#[pg_extern]
fn idkit_uuidv7_generate() -> String {
uuid7().to_string()
new_uuidv7().as_hyphenated().to_string()
}

/// Generate a UUID v7, producing a Postgres text object
Expand All @@ -16,18 +27,51 @@ fn idkit_uuidv7_generate_text() -> String {
/// Generate a UUID v7, producing a Postgres uuid object
#[pg_extern]
fn idkit_uuidv7_generate_uuid() -> pgrx::Uuid {
pgrx::Uuid::from_slice(uuid7().as_bytes())
.unwrap_or_else(|e| error!("{}", format!("failed to generate/parse uuidv7: {}", e)))
pgrx::Uuid::from_slice(new_uuidv7().as_bytes())
.map_err(|e| IoError::new(ErrorKind::Other, format!("{e:?}")))
.or_pgrx_error("failed to convert UUIDv7 to Postgres uuid type")
}

/// Retrieve a `timestamptz` (with millisecond precision) from a given textual UUIDv7
///
/// # Panics
///
/// This function panics (with a [`pgrx::error`]) when the timezone can't be created
#[pg_extern]
fn idkit_uuidv7_extract_timestamptz(val: String) -> pgrx::TimestampWithTimeZone {
let (secs, nanos) = Uuid::from_str(val.as_str())
.or_pgrx_error(format!("[{val}] is an invalid UUIDv7"))
.get_timestamp()
.or_pgrx_error("failed to extract timestamp")
.to_unix();
if secs > i64::MAX as u64 {
pgrx::error!(
"value [{secs}] seconds is larger than the max signed 64bit integer [{}]",
i64::MAX
);
}
naive_datetime_to_pg_timestamptz(
NaiveDateTime::from_timestamp_opt(secs as i64, nanos)
.or_pgrx_error("failed to create timestamp from UUIDV7 [{val}]")
.and_utc(),
format!("failed to convert timestamp for UUIDV7 [{val}]"),
)
}

//////////
// Test //
//////////

#[cfg(any(test, feature = "pg_test"))]
#[pg_schema]
#[pgrx::pg_schema]
mod tests {
use pgrx::*;
use chrono::{DateTime, Utc};
use pgrx::datum::datetime_support::ToIsoString;
use pgrx::pg_test;

use crate::uuid_v7::idkit_uuidv7_extract_timestamptz;
use crate::uuid_v7::idkit_uuidv7_generate;
use crate::uuid_v7::idkit_uuidv7_generate_uuid;

/// Basic length test
#[pg_test]
Expand All @@ -36,6 +80,12 @@ mod tests {
assert_eq!(generated.len(), 36);
}

/// Basic length test for bytes
#[pg_test]
fn test_uuidv7_len_uuid() {
assert_eq!(idkit_uuidv7_generate_uuid().len(), 16);
}

/// Check version integer in UUID string
#[pg_test]
fn test_uuidv7_version_int() {
Expand All @@ -44,4 +94,23 @@ mod tests {
assert!(c9.is_some());
assert_eq!(c9.unwrap(), '7');
}

/// Ensure timestamps extracted from CUIDs are valid
#[pg_test]
fn test_uuidv7_extract_timestamptz() {
let timestamp = idkit_uuidv7_extract_timestamptz(idkit_uuidv7_generate());
let parsed: DateTime<Utc> = DateTime::parse_from_rfc3339(&timestamp.to_iso_string())
.expect("extracted timestamp as ISO string parsed to UTC DateTime")
.into();
assert!(
Utc::now().signed_duration_since(parsed).num_seconds() < 3,
"extracted, printed & re-parsed uuidv7 timestamp is from recent past (within 3s)"
);
}

/// Ensure an existing, hardcoded timestamp works for extraction
#[pg_test]
fn test_uuidv7_extract_timestamptz_existing() {
idkit_uuidv7_extract_timestamptz("016b0dd7-0cbb-691e-8548-9888e89d0527".into());
}
}

0 comments on commit 9e52321

Please sign in to comment.