Skip to content

Commit 1fa1f90

Browse files
committed
feat(uuidv7): add support for extracting timestamps
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]>
1 parent bddbd5d commit 1fa1f90

File tree

3 files changed

+77
-27
lines changed

3 files changed

+77
-27
lines changed

Cargo.lock

Lines changed: 0 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@ pushid = "0.0.1"
3737
sonyflake = "0.2"
3838
timeflake-rs = "0.3"
3939
ulid = "1.1"
40-
uuid7 = "0.7"
41-
uuid = { version = "1.6", features = [ "v6" ] }
40+
uuid = { version = "1.6", features = [ "v6", "v7" ] }
4241
xid = "1.0"
4342
miette = { version = "5.10", features = ["fancy"] }
4443
time = "0.3"

src/uuid_v7.rs

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
1-
use pgrx::*;
2-
use uuid7::uuid7;
1+
use std::io::{Error as IoError, ErrorKind};
2+
use std::str::FromStr;
3+
4+
use chrono::NaiveDateTime;
5+
use pgrx::pg_extern;
6+
use uuid::Uuid;
7+
8+
use crate::common::{naive_datetime_to_pg_timestamptz, OrPgrxError};
9+
10+
/// Generate a new UUIDv6
11+
fn new_uuidv7() -> Uuid {
12+
Uuid::now_v7()
13+
}
314

415
/// Generate a UUID v7
516
#[pg_extern]
617
fn idkit_uuidv7_generate() -> String {
7-
uuid7().to_string()
18+
new_uuidv7().as_hyphenated().to_string()
819
}
920

1021
/// Generate a UUID v7, producing a Postgres text object
@@ -16,18 +27,51 @@ fn idkit_uuidv7_generate_text() -> String {
1627
/// Generate a UUID v7, producing a Postgres uuid object
1728
#[pg_extern]
1829
fn idkit_uuidv7_generate_uuid() -> pgrx::Uuid {
19-
pgrx::Uuid::from_slice(uuid7().as_bytes())
20-
.unwrap_or_else(|e| error!("{}", format!("failed to generate/parse uuidv7: {}", e)))
30+
pgrx::Uuid::from_slice(new_uuidv7().as_bytes())
31+
.map_err(|e| IoError::new(ErrorKind::Other, format!("{e:?}")))
32+
.or_pgrx_error("failed to convert UUIDv7 to Postgres uuid type")
33+
}
34+
35+
/// Retrieve a `timestamptz` (with millisecond precision) from a given textual UUIDv7
36+
///
37+
/// # Panics
38+
///
39+
/// This function panics (with a [`pgrx::error`]) when the timezone can't be created
40+
#[pg_extern]
41+
fn idkit_uuidv7_extract_timestamptz(val: String) -> pgrx::TimestampWithTimeZone {
42+
let (secs, nanos) = Uuid::from_str(val.as_str())
43+
.or_pgrx_error(format!("[{val}] is an invalid UUIDv7"))
44+
.get_timestamp()
45+
.or_pgrx_error("failed to extract timestamp")
46+
.to_unix();
47+
if secs > i64::MAX as u64 {
48+
pgrx::error!(
49+
"value [{secs}] seconds is larger than the max signed 64bit integer [{}]",
50+
i64::MAX
51+
);
52+
}
53+
naive_datetime_to_pg_timestamptz(
54+
NaiveDateTime::from_timestamp_opt(secs as i64, nanos)
55+
.or_pgrx_error("failed to create timestamp from UUIDV7 [{val}]")
56+
.and_utc(),
57+
format!("failed to convert timestamp for UUIDV7 [{val}]"),
58+
)
2159
}
2260

2361
//////////
2462
// Test //
2563
//////////
2664

2765
#[cfg(any(test, feature = "pg_test"))]
28-
#[pg_schema]
66+
#[pgrx::pg_schema]
2967
mod tests {
30-
use pgrx::*;
68+
use chrono::{DateTime, Utc};
69+
use pgrx::datum::datetime_support::ToIsoString;
70+
use pgrx::pg_test;
71+
72+
use crate::uuid_v7::idkit_uuidv7_extract_timestamptz;
73+
use crate::uuid_v7::idkit_uuidv7_generate;
74+
use crate::uuid_v7::idkit_uuidv7_generate_uuid;
3175

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

83+
/// Basic length test for bytes
84+
#[pg_test]
85+
fn test_uuidv7_len_uuid() {
86+
assert_eq!(idkit_uuidv7_generate_uuid().len(), 16);
87+
}
88+
3989
/// Check version integer in UUID string
4090
#[pg_test]
4191
fn test_uuidv7_version_int() {
@@ -44,4 +94,23 @@ mod tests {
4494
assert!(c9.is_some());
4595
assert_eq!(c9.unwrap(), '7');
4696
}
97+
98+
/// Ensure timestamps extracted from CUIDs are valid
99+
#[pg_test]
100+
fn test_uuidv7_extract_timestamptz() {
101+
let timestamp = idkit_uuidv7_extract_timestamptz(idkit_uuidv7_generate());
102+
let parsed: DateTime<Utc> = DateTime::parse_from_rfc3339(&timestamp.to_iso_string())
103+
.expect("extracted timestamp as ISO string parsed to UTC DateTime")
104+
.into();
105+
assert!(
106+
Utc::now().signed_duration_since(parsed).num_seconds() < 3,
107+
"extracted, printed & re-parsed uuidv7 timestamp is from recent past (within 3s)"
108+
);
109+
}
110+
111+
/// Ensure an existing, hardcoded timestamp works for extraction
112+
#[pg_test]
113+
fn test_uuidv7_extract_timestamptz_existing() {
114+
idkit_uuidv7_extract_timestamptz("016b0dd7-0cbb-691e-8548-9888e89d0527".into());
115+
}
47116
}

0 commit comments

Comments
 (0)