Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit a5419bc

Browse files
Fix timestamp overflow in UUIDv7 implementation.
The uuidv7_interval() function previously converted a shifted microsecond-precision timestamp (64-bit integer) to another 64-bit integer representing a timestamp with nanosecond precision. This conversion caused overflow for dates beyond the year 2262. The millisecond and sub-millisecond parts were then extracted from this nanosecond-precision timestamp and stored in UUIDv7 values. With this commit, the millisecond and sub-millisecond parts are stored directly into the UUIDv7 value without being converted back to a nanosecond precision timestamp. Following RFC 9562, the timestamp is stored as an unsigned integer, enabling support for dates up to the year 10889. Reported and fixed by Andrey Borodin, with cosmetic changes and regression tests by me. Reported-by: Andrey Borodin <[email protected]> Author: Andrey Borodin <[email protected]> Discussion: https://postgr.es/m/[email protected]
1 parent 8e993bf commit a5419bc

File tree

3 files changed

+44
-17
lines changed

3 files changed

+44
-17
lines changed

src/backend/utils/adt/uuid.c

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#define NS_PER_S INT64CONST(1000000000)
3030
#define NS_PER_MS INT64CONST(1000000)
3131
#define NS_PER_US INT64CONST(1000)
32+
#define US_PER_MS INT64CONST(1000)
3233

3334
/*
3435
* UUID version 7 uses 12 bits in "rand_a" to store 1/4096 (or 2^12) fractions of
@@ -69,6 +70,7 @@ static bool uuid_abbrev_abort(int memtupcount, SortSupport ssup);
6970
static Datum uuid_abbrev_convert(Datum original, SortSupport ssup);
7071
static inline void uuid_set_version(pg_uuid_t *uuid, unsigned char version);
7172
static inline int64 get_real_time_ns_ascending();
73+
static pg_uuid_t *generate_uuidv7(uint64 unix_ts_ms, uint32 sub_ms);
7274

7375
Datum
7476
uuid_in(PG_FUNCTION_ARGS)
@@ -523,17 +525,17 @@ get_real_time_ns_ascending()
523525
* described in the RFC. This method utilizes 12 bits from the "rand_a" bits
524526
* to store a 1/4096 (or 2^12) fraction of sub-millisecond precision.
525527
*
526-
* ns is a number of nanoseconds since start of the UNIX epoch. This value is
528+
* unix_ts_ms is a number of milliseconds since start of the UNIX epoch,
529+
* and sub_ms is a number of nanoseconds within millisecond. These values are
527530
* used for time-dependent bits of UUID.
531+
*
532+
* NB: all numbers here are unsigned, unix_ts_ms cannot be negative per RFC.
528533
*/
529534
static pg_uuid_t *
530-
generate_uuidv7(int64 ns)
535+
generate_uuidv7(uint64 unix_ts_ms, uint32 sub_ms)
531536
{
532537
pg_uuid_t *uuid = palloc(UUID_LEN);
533-
int64 unix_ts_ms;
534-
int32 increased_clock_precision;
535-
536-
unix_ts_ms = ns / NS_PER_MS;
538+
uint32 increased_clock_precision;
537539

538540
/* Fill in time part */
539541
uuid->data[0] = (unsigned char) (unix_ts_ms >> 40);
@@ -547,7 +549,7 @@ generate_uuidv7(int64 ns)
547549
* sub-millisecond timestamp fraction (SUBMS_BITS bits, not
548550
* SUBMS_MINIMAL_STEP_BITS)
549551
*/
550-
increased_clock_precision = ((ns % NS_PER_MS) * (1 << SUBMS_BITS)) / NS_PER_MS;
552+
increased_clock_precision = (sub_ms * (1 << SUBMS_BITS)) / NS_PER_MS;
551553

552554
/* Fill the increased clock precision to "rand_a" bits */
553555
uuid->data[6] = (unsigned char) (increased_clock_precision >> 8);
@@ -586,7 +588,8 @@ generate_uuidv7(int64 ns)
586588
Datum
587589
uuidv7(PG_FUNCTION_ARGS)
588590
{
589-
pg_uuid_t *uuid = generate_uuidv7(get_real_time_ns_ascending());
591+
int64 ns = get_real_time_ns_ascending();
592+
pg_uuid_t *uuid = generate_uuidv7(ns / NS_PER_MS, ns % NS_PER_MS);
590593

591594
PG_RETURN_UUID_P(uuid);
592595
}
@@ -601,13 +604,13 @@ uuidv7_interval(PG_FUNCTION_ARGS)
601604
TimestampTz ts;
602605
pg_uuid_t *uuid;
603606
int64 ns = get_real_time_ns_ascending();
607+
int64 us;
604608

605609
/*
606610
* Shift the current timestamp by the given interval. To calculate time
607611
* shift correctly, we convert the UNIX epoch to TimestampTz and use
608-
* timestamptz_pl_interval(). Since this calculation is done with
609-
* microsecond precision, we carry nanoseconds from original ns value to
610-
* shifted ns value.
612+
* timestamptz_pl_interval(). This calculation is done with microsecond
613+
* precision.
611614
*/
612615

613616
ts = (TimestampTz) (ns / NS_PER_US) -
@@ -618,14 +621,11 @@ uuidv7_interval(PG_FUNCTION_ARGS)
618621
TimestampTzGetDatum(ts),
619622
IntervalPGetDatum(shift)));
620623

621-
/*
622-
* Convert a TimestampTz value back to an UNIX epoch and back nanoseconds.
623-
*/
624-
ns = (ts + (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * SECS_PER_DAY * USECS_PER_SEC)
625-
* NS_PER_US + ns % NS_PER_US;
624+
/* Convert a TimestampTz value back to an UNIX epoch timestamp */
625+
us = ts + (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * SECS_PER_DAY * USECS_PER_SEC;
626626

627627
/* Generate an UUIDv7 */
628-
uuid = generate_uuidv7(ns);
628+
uuid = generate_uuidv7(us / US_PER_MS, (us % US_PER_MS) * NS_PER_US + ns % NS_PER_US);
629629

630630
PG_RETURN_UUID_P(uuid);
631631
}

src/test/regress/expected/uuid.out

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,21 @@ SELECT array_agg(id ORDER BY guid_field) FROM guid3;
233233
{1,2,3,4,5,6,7,8,9,10}
234234
(1 row)
235235

236+
-- Check the timestamp offsets for v7.
237+
--
238+
-- generate UUIDv7 values with timestamps ranging from 1970 (the Unix epoch year)
239+
-- to 10888 (one year before the maximum possible year), and then verify that
240+
-- the extracted timestamps from these UUIDv7 values have not overflowed.
241+
WITH uuidts AS (
242+
SELECT y, ts as ts, lag(ts) OVER (ORDER BY y) AS prev_ts
243+
FROM (SELECT y, uuid_extract_timestamp(uuidv7((y || ' years')::interval)) AS ts
244+
FROM generate_series(1970 - extract(year from now())::int, 10888 - extract(year from now())::int) y)
245+
)
246+
SELECT y, ts, prev_ts FROM uuidts WHERE ts < prev_ts;
247+
y | ts | prev_ts
248+
---+----+---------
249+
(0 rows)
250+
236251
-- extract functions
237252
-- version
238253
SELECT uuid_extract_version('11111111-1111-5111-8111-111111111111'); -- 5

src/test/regress/sql/uuid.sql

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,18 @@ SELECT count(DISTINCT guid_field) FROM guid1;
119119
INSERT INTO guid3 (guid_field) SELECT uuidv7() FROM generate_series(1, 10);
120120
SELECT array_agg(id ORDER BY guid_field) FROM guid3;
121121

122+
-- Check the timestamp offsets for v7.
123+
--
124+
-- generate UUIDv7 values with timestamps ranging from 1970 (the Unix epoch year)
125+
-- to 10888 (one year before the maximum possible year), and then verify that
126+
-- the extracted timestamps from these UUIDv7 values have not overflowed.
127+
WITH uuidts AS (
128+
SELECT y, ts as ts, lag(ts) OVER (ORDER BY y) AS prev_ts
129+
FROM (SELECT y, uuid_extract_timestamp(uuidv7((y || ' years')::interval)) AS ts
130+
FROM generate_series(1970 - extract(year from now())::int, 10888 - extract(year from now())::int) y)
131+
)
132+
SELECT y, ts, prev_ts FROM uuidts WHERE ts < prev_ts;
133+
122134
-- extract functions
123135

124136
-- version

0 commit comments

Comments
 (0)