aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2026-03-10 13:18:11 +0700
committerShulhan <ms@kilabit.info>2026-03-10 13:23:54 +0700
commitfcb3423fc68a8500b1ddd87e12c75ca1f607ad8b (patch)
tree41f3ef02ea60a80cf2ba649cc1b7b616c3dba948 /lib
parent3f780768650cf8a55d2f7310f09c7a936cf0ae16 (diff)
downloadpakakeh.go-fcb3423fc68a8500b1ddd87e12c75ca1f607ad8b.tar.xz
lib/uuidv7: new package that implements UUID version 7
The uuidv7 package provides type UUIDv7 as the container for UUID version 7 that satisfy the [database/sql], [encoding/gob], and [encoding/json]. The implementation follow RFC 9562.
Diffstat (limited to 'lib')
-rw-r--r--lib/uuidv7/uuidv7.go198
-rw-r--r--lib/uuidv7/uuidv7_example_test.go130
-rw-r--r--lib/uuidv7/uuidv7_test.go88
3 files changed, 416 insertions, 0 deletions
diff --git a/lib/uuidv7/uuidv7.go b/lib/uuidv7/uuidv7.go
new file mode 100644
index 00000000..5a6b6e03
--- /dev/null
+++ b/lib/uuidv7/uuidv7.go
@@ -0,0 +1,198 @@
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: 2026 M. Shulhan <ms@kilabit.info>
+
+// Package uuidv7 implements UUID version 7 as defined in RFC 9562.
+package uuidv7
+
+import (
+ "crypto/rand"
+ "encoding/binary"
+ "encoding/hex"
+ "fmt"
+ "time"
+)
+
+const hexTable = `0123456789ABCDEF`
+const variantMask uint64 = 0x8000_0000_0000_0000
+
+// Now defines the function variable that return the current time in UTC.
+// This identifier is exported to simplify testing.
+var Now = func() time.Time {
+ return time.Now().UTC()
+}
+
+// Rand defines the function variable that return a random uint64.
+// This identifier is exported to simplify testing.
+var Rand = func() (v uint64) {
+ b := make([]byte, 8)
+ rand.Read(b)
+ v, _ = binary.Uvarint(b)
+ return v
+}
+
+// UUIDv7 defines the container for UUID version 7 that satisfy the
+// [database/sql], [encoding/gob], and [encoding/json].
+type UUIDv7 struct {
+ // The high 8 octets store the unix timestamp in milliseconds (6
+ // octets), the version (4 bits), and clock precision (12 bits).
+ high uint64
+
+ // The low 8 octets store the variant (2 bits) and random number (62
+ // bits).
+ low uint64
+}
+
+// Generate generates new UUID version 7.
+func Generate() (id UUIDv7) {
+ now := Now()
+ generate(&now, &id)
+ return id
+}
+
+func generate(t *time.Time, id *UUIDv7) {
+ unixns := uint64(t.UnixNano()) // 1773066002123456700
+ millis := uint64(unixns / 1e6) // 1773066002123
+ id.high = millis << 16 // 0x019c_d2f8_1ecb_0000
+ id.high |= 0x7000 // 0x019c_d2f8_1ecb_7000
+ prec := unixns - (millis * 1e6) // 456700
+ clockPrec := uint64((prec * 4096) / 1e6) // 1870
+ id.high |= clockPrec // 0x019c_d2f8_1ecb_774e
+ rand := Rand() // 0xffff_ffff_ffff_ffff
+ rand >>= 2 // 0x3fff_ffff_ffff_ffff
+ id.low = variantMask | rand // 0xbfff_ffff_ffff_ffff
+}
+
+// Parse parses the UUIDv7 formated string and return it as non-zero id.
+// If the string s is invalid UUIDv7 it will return zero id.
+func Parse(s string) (id UUIDv7) {
+ id.UnmarshalText([]byte(s))
+ return id
+}
+
+// Bytes returns id as a stream of binary.
+func (id UUIDv7) Bytes() (data []byte) {
+ data = make([]byte, 0, binary.MaxVarintLen64*2)
+ data = binary.BigEndian.AppendUint64(data, id.high)
+ data = binary.BigEndian.AppendUint64(data, id.low)
+ return data
+}
+
+// IsZero returns true if all bits is zero.
+func (id UUIDv7) IsZero() bool {
+ return id.high == 0 && id.low == 0
+}
+
+// MarshalBinary encodes the id to binary for [encoding/gob].
+func (id UUIDv7) MarshalBinary() (data []byte, err error) {
+ data = id.Bytes()
+ return data, nil
+}
+
+// MarshalText encodes the id to JSON for [encoding/json].
+func (id UUIDv7) MarshalText() (data []byte, err error) {
+ v := id.String()
+ return []byte(v), nil
+}
+
+// UnmarshalBinary decodes the data into id for [encoding/gob].
+func (id *UUIDv7) UnmarshalBinary(data []byte) (err error) {
+ if len(data) != 16 {
+ return fmt.Errorf(`uuidv7: invalid length %d`, len(data))
+ }
+ ver := data[6] >> 4
+ if ver != 7 {
+ return fmt.Errorf(`uuidv7: invalid version %d`, ver)
+ }
+ id.high = binary.BigEndian.Uint64(data[:8])
+ id.low = binary.BigEndian.Uint64(data[8:])
+ return nil
+}
+
+// UnmarshalText decodes the JSON data into id for [encoding/json].
+func (id *UUIDv7) UnmarshalText(data []byte) (err error) {
+ src := make([]byte, 0, 32)
+ for _, c := range data {
+ if c == '-' {
+ continue
+ }
+ src = append(src, c)
+ }
+ l := len(src)
+ if l != 32 {
+ return fmt.Errorf(`uuidv7: invalid length %d`, l)
+ }
+ if src[12] != '7' {
+ return fmt.Errorf(`uuidv7: invalid version %c`, data[12])
+ }
+
+ l = hex.DecodedLen(len(src[:16]))
+ dst := make([]byte, l)
+ _, err = hex.Decode(dst, src[:16])
+ if err != nil {
+ return fmt.Errorf(`uuidv7: %w`, err)
+ }
+ id.high = binary.BigEndian.Uint64(dst)
+
+ _, err = hex.Decode(dst, src[16:])
+ if err != nil {
+ return fmt.Errorf(`uuidv7: %w`, err)
+ }
+ id.low = binary.BigEndian.Uint64(dst)
+ return nil
+}
+
+// Scan scans the raw database value into id.
+// This method implement [database/sql.Scanner] interface.
+func (id *UUIDv7) Scan(src any) (err error) {
+ switch v := src.(type) {
+ case []byte:
+ err = id.UnmarshalText(v)
+ if err != nil {
+ return err
+ }
+ default:
+ return fmt.Errorf(`uuidv7: Scan: invalid type %T`, src)
+ }
+ return nil
+}
+
+// String returns the human readable format of UUIDv7 in the form of
+// "0xxxxxxx-xxxx-7xxx-8xxx-xxxxxxxxxxxx".
+func (id UUIDv7) String() string {
+ buf := make([]byte, 36)
+ sw := 56
+ x := 0
+ for sw >= 0 {
+ b := byte(id.high >> sw)
+ sw -= 8
+ buf[x] = hexTable[b>>4]
+ buf[x+1] = hexTable[b&0x0F]
+ x += 2
+ if x == 8 || x == 13 {
+ buf[x] = '-'
+ x++
+ }
+ }
+ buf[x] = '-'
+ x++
+ sw = 56
+ for sw >= 0 {
+ b := byte(id.low >> sw)
+ sw -= 8
+ buf[x] = hexTable[b>>4]
+ buf[x+1] = hexTable[b&0x0F]
+ x += 2
+ if x == 23 {
+ buf[x] = '-'
+ x++
+ }
+ }
+ return string(buf)
+}
+
+// Time returns the Unix epoch timestamp in milliseconds stored in the UUIDv7.
+func (id *UUIDv7) Time() time.Time {
+ millis := id.high >> 16
+ t := time.UnixMilli(int64(millis))
+ return t
+}
diff --git a/lib/uuidv7/uuidv7_example_test.go b/lib/uuidv7/uuidv7_example_test.go
new file mode 100644
index 00000000..c0472dd5
--- /dev/null
+++ b/lib/uuidv7/uuidv7_example_test.go
@@ -0,0 +1,130 @@
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: 2026 M. Shulhan <ms@kilabit.info>
+
+package uuidv7_test
+
+import (
+ "bytes"
+ "encoding/gob"
+ "encoding/json"
+ "fmt"
+ "log"
+ "time"
+
+ "git.sr.ht/~shulhan/pakakeh.go/lib/uuidv7"
+)
+
+func ExampleGenerate() {
+ now := time.Date(2026, 3, 9, 14, 20, 0, 123456700, time.UTC)
+ uuidv7.Now = func() time.Time {
+ now = now.Add(time.Second)
+ return now
+ }
+ uuidv7.Rand = func() uint64 {
+ return 0xFFFF_FFFF_FFFF_FFFF
+ }
+
+ id := uuidv7.Generate()
+ fmt.Printf("%s %s\n", id, id.Time().UTC())
+ id = uuidv7.Generate()
+ fmt.Printf("%s %s\n", id, id.Time().UTC())
+
+ // Output:
+ // 019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF 2026-03-09 14:20:01.123 +0000 UTC
+ // 019CD2F8-1ECB-774E-BFFF-FFFFFFFFFFFF 2026-03-09 14:20:02.123 +0000 UTC
+}
+
+func ExampleParse() {
+ id := uuidv7.Parse(``)
+ fmt.Printf("%t %s %s\n", id.IsZero(), id, id.Time().UTC())
+
+ id = uuidv7.Parse(`019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF`)
+ fmt.Printf("%t %s %s\n", id.IsZero(), id, id.Time().UTC())
+
+ // Output:
+ // true 00000000-0000-0000-0000-000000000000 1970-01-01 00:00:00 +0000 UTC
+ // false 019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF 2026-03-09 14:20:01.123 +0000 UTC
+}
+
+func ExampleUUIDv7_MarshalBinary() {
+ now := time.Date(2026, 3, 9, 14, 20, 0, 123456700, time.UTC)
+ uuidv7.Now = func() time.Time {
+ now = now.Add(time.Second)
+ return now
+ }
+ uuidv7.Rand = func() uint64 {
+ return 0xFFFF_FFFF_FFFF_FFFF
+ }
+
+ var buf bytes.Buffer
+ enc := gob.NewEncoder(&buf)
+ id := uuidv7.Generate()
+ err := enc.Encode(id)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("Sent: %s\n", id)
+
+ dec := gob.NewDecoder(&buf)
+ var gotID uuidv7.UUIDv7
+ err = dec.Decode(&gotID)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("Receive: %s\n", gotID)
+
+ // Output:
+ // Sent: 019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF
+ // Receive: 019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF
+}
+
+func ExampleUUIDv7_MarshalText() {
+ now := time.Date(2026, 3, 9, 14, 20, 0, 123456700, time.UTC)
+ uuidv7.Now = func() time.Time {
+ now = now.Add(time.Second)
+ return now
+ }
+ uuidv7.Rand = func() uint64 {
+ return 0xFFFF_FFFF_FFFF_FFFF
+ }
+
+ type T struct {
+ ID uuidv7.UUIDv7
+ }
+
+ t := T{
+ ID: uuidv7.Generate(),
+ }
+ jsonb, err := json.Marshal(t)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("%s\n", jsonb)
+
+ var got T
+ err = json.Unmarshal(jsonb, &got)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("%+v\n", got)
+
+ // Output:
+ // {"ID":"019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF"}
+ // {ID:019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF}
+}
+
+func ExampleUUIDv7_Scan() {
+ var id uuidv7.UUIDv7
+
+ // Scan the value from the database:
+ //
+ // dbc.QueryRow(`SELECT id ...`, &id)
+
+ err := id.Scan([]byte(`017F22E2-79B0-7CC3-98C4-DC0C0C07398F`))
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("%s\n", id)
+ // Output:
+ // 017F22E2-79B0-7CC3-98C4-DC0C0C07398F
+}
diff --git a/lib/uuidv7/uuidv7_test.go b/lib/uuidv7/uuidv7_test.go
new file mode 100644
index 00000000..70ca315c
--- /dev/null
+++ b/lib/uuidv7/uuidv7_test.go
@@ -0,0 +1,88 @@
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: 2026 M. Shulhan <ms@kilabit.info>
+
+package uuidv7
+
+import (
+ "testing"
+
+ "git.sr.ht/~shulhan/pakakeh.go/lib/test"
+)
+
+func TestUUIDv7_Scan(t *testing.T) {
+ listCase := []struct {
+ desc string
+ data any
+ expError string
+ }{{
+ desc: `With empty data`,
+ data: `017F22E2-79B0-7CC3-98C4-DC0C0C07398F`,
+ expError: `uuidv7: Scan: invalid type string`,
+ }, {
+ desc: `With invalid version`,
+ data: []byte(`5c146b14-3c52-4afd-938a-375d0df1fbf6`),
+ expError: `uuidv7: invalid version 2`,
+ }}
+ for _, tc := range listCase {
+ var id UUIDv7
+ err := id.Scan(tc.data)
+ if err != nil {
+ test.Assert(t, tc.desc, tc.expError, err.Error())
+ continue
+ }
+ }
+}
+
+func TestUUIDv7_UnmarshalBinary(t *testing.T) {
+ listCase := []struct {
+ desc string
+ expError string
+ data []byte
+ }{{
+ desc: `With empty data`,
+ expError: `uuidv7: invalid length 0`,
+ }, {
+ desc: `With non-version 7`,
+ data: []byte{1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 1, 2, 3, 4},
+ expError: `uuidv7: invalid version 0`,
+ }}
+ for _, tc := range listCase {
+ var id UUIDv7
+ err := id.UnmarshalBinary(tc.data)
+ if err != nil {
+ test.Assert(t, tc.desc, tc.expError, err.Error())
+ continue
+ }
+ }
+}
+
+func TestUUIDv7_UnmarshalText(t *testing.T) {
+ listCase := []struct {
+ desc string
+ expError string
+ data []byte
+ }{{
+ desc: `With empty data`,
+ expError: `uuidv7: invalid length 0`,
+ }, {
+ desc: `With non-version 7`,
+ data: []byte(`5c146b14-3c52-4afd-938a-375d0df1fbf6`),
+ expError: `uuidv7: invalid version 2`,
+ }, {
+ desc: `With invalid hex (high)`,
+ data: []byte(`X17F22E2-79B0-7CC3-98C4-DC0C0C07398F`),
+ expError: `uuidv7: encoding/hex: invalid byte: U+0058 'X'`,
+ }, {
+ desc: `With invalid hex (low)`,
+ data: []byte(`017F22E2-79B0-7CC3-98C4-DC0C0C07398X`),
+ expError: `uuidv7: encoding/hex: invalid byte: U+0058 'X'`,
+ }}
+ for _, tc := range listCase {
+ var id UUIDv7
+ err := id.UnmarshalText(tc.data)
+ if err != nil {
+ test.Assert(t, tc.desc, tc.expError, err.Error())
+ continue
+ }
+ }
+}