From 7a67525477c01341f8faaa7b14af5038e3966bbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20-=20=E3=82=A2=E3=83=AC=E3=83=83=E3=82=AF=E3=82=B9?= Date: Thu, 9 Apr 2026 18:26:19 +0200 Subject: [PATCH 1/5] feat(dbtype): add UUIDv7 floor/ceil bounds and timestamp extraction --- dbtype/uuidv7.go | 49 ++++++++++++++++++++++++++++++++ dbtype/uuidv7_test.go | 66 +++++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 ++ 4 files changed, 118 insertions(+) create mode 100644 dbtype/uuidv7.go create mode 100644 dbtype/uuidv7_test.go diff --git a/dbtype/uuidv7.go b/dbtype/uuidv7.go new file mode 100644 index 0000000..1759374 --- /dev/null +++ b/dbtype/uuidv7.go @@ -0,0 +1,49 @@ +package dbtype + +import ( + "time" + + "github.com/google/uuid" +) + +// UUIDv7Time extracts the millisecond-precision creation timestamp +// embedded in a UUIDv7 (bytes 0-5, big-endian uint48 ms since epoch). +func UUIDv7Time(u uuid.UUID) time.Time { + ms := int64(u[0])<<40 | int64(u[1])<<32 | int64(u[2])<<24 | + int64(u[3])<<16 | int64(u[4])<<8 | int64(u[5]) + return time.UnixMilli(ms) +} + +func setUUIDv7Timestamp(u *uuid.UUID, t time.Time) { + ms := uint64(t.UnixMilli()) + u[0] = byte(ms >> 40) + u[1] = byte(ms >> 32) + u[2] = byte(ms >> 24) + u[3] = byte(ms >> 16) + u[4] = byte(ms >> 8) + u[5] = byte(ms) +} + +// FloorUUIDv7 returns the lowest valid UUIDv7 for timestamp t. +// Version and variant bits are set; all random bits are zero. +func FloorUUIDv7(t time.Time) uuid.UUID { + var u uuid.UUID + setUUIDv7Timestamp(&u, t) + u[6] = 0x70 // version 7, rand_a = 0 + u[8] = 0x80 // variant 10xxxxxx, rand_b = 0 + return u +} + +// CeilUUIDv7 returns the highest valid UUIDv7 for timestamp t. +// Version and variant bits are set; all random bits are one. +func CeilUUIDv7(t time.Time) uuid.UUID { + var u uuid.UUID + setUUIDv7Timestamp(&u, t) + u[6] = 0x7f // version 7, rand_a high nibble all 1s + u[7] = 0xff // rand_a low byte all 1s + u[8] = 0xbf // variant 10, 6 bits all 1s + for i := 9; i < 16; i++ { + u[i] = 0xff + } + return u +} diff --git a/dbtype/uuidv7_test.go b/dbtype/uuidv7_test.go new file mode 100644 index 0000000..97b5ae4 --- /dev/null +++ b/dbtype/uuidv7_test.go @@ -0,0 +1,66 @@ +package dbtype_test + +import ( + "testing" + "time" + + "github.com/google/uuid" + "github.com/goware/pgkit/v2/dbtype" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFloorUUIDv7_VersionAndVariant(t *testing.T) { + u := dbtype.FloorUUIDv7(time.Now()) + assert.Equal(t, uuid.Version(7), u.Version()) + assert.Equal(t, uuid.RFC4122, u.Variant()) +} + +func TestCeilUUIDv7_VersionAndVariant(t *testing.T) { + u := dbtype.CeilUUIDv7(time.Now()) + assert.Equal(t, uuid.Version(7), u.Version()) + assert.Equal(t, uuid.RFC4122, u.Variant()) +} + +func TestFloorUUIDv7_LessThanNewV7(t *testing.T) { + now := time.Now() + floor := dbtype.FloorUUIDv7(now) + for range 100 { + v7, err := uuid.NewV7() + require.NoError(t, err) + assert.LessOrEqual(t, floor.String(), v7.String()) + } +} + +func TestCeilUUIDv7_GreaterThanFloor(t *testing.T) { + now := time.Now() + floor := dbtype.FloorUUIDv7(now) + ceil := dbtype.CeilUUIDv7(now) + assert.Greater(t, ceil.String(), floor.String()) +} + +func TestFloorCeilUUIDv7_BracketRealID(t *testing.T) { + v7, err := uuid.NewV7() + require.NoError(t, err) + ts := dbtype.UUIDv7Time(v7) + + floor := dbtype.FloorUUIDv7(ts) + ceil := dbtype.CeilUUIDv7(ts) + assert.LessOrEqual(t, floor.String(), v7.String()) + assert.GreaterOrEqual(t, ceil.String(), v7.String()) +} + +func TestFloorUUIDv7_TimestampRoundTrip(t *testing.T) { + ts := time.Date(2026, 3, 27, 12, 0, 0, 0, time.UTC) + floor := dbtype.FloorUUIDv7(ts) + got := dbtype.UUIDv7Time(floor) + assert.Equal(t, ts.UnixMilli(), got.UnixMilli()) +} + +func TestUUIDv7Time_ExtractsTimestamp(t *testing.T) { + v7, err := uuid.NewV7() + require.NoError(t, err) + got := dbtype.UUIDv7Time(v7) + assert.False(t, got.IsZero()) + assert.WithinDuration(t, time.Now(), got, time.Second) +} diff --git a/go.mod b/go.mod index b1b28bb..ba186ba 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ toolchain go1.24.3 require ( github.com/Masterminds/squirrel v1.5.4 github.com/georgysavva/scany/v2 v2.1.4 + github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.7.6 github.com/stretchr/testify v1.11.1 ) diff --git a/go.sum b/go.sum index 9c238b9..4943359 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/georgysavva/scany/v2 v2.1.4 h1:nrzHEJ4oQVRoiKmocRqA1IyGOmM/GQOEsg9UjM github.com/georgysavva/scany/v2 v2.1.4/go.mod h1:fqp9yHZzM/PFVa3/rYEC57VmDx+KDch0LoqrJzkvtos= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= From 3ff38e9a8b53f8ee8bec0e7860257b0d2c0ea243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20-=20=E3=82=A2=E3=83=AC=E3=83=83=E3=82=AF=E3=82=B9?= Date: Thu, 9 Apr 2026 18:26:27 +0200 Subject: [PATCH 2/5] feat(dbtype): add Int64ID floor/ceil bounds and timestamp extraction --- dbtype/int64id.go | 25 ++++++++++++++++++ dbtype/int64id_test.go | 60 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 dbtype/int64id.go create mode 100644 dbtype/int64id_test.go diff --git a/dbtype/int64id.go b/dbtype/int64id.go new file mode 100644 index 0000000..0c5f282 --- /dev/null +++ b/dbtype/int64id.go @@ -0,0 +1,25 @@ +package dbtype + +import "time" + +// Int64IDBits is the number of random bits in a typeid Int64. +// Bit layout: [48-bit unix ms timestamp][15-bit crypto/rand] = 63 bits, always positive. +const Int64IDBits = 15 + +// Int64IDTime extracts the millisecond-precision creation timestamp +// from an Int64 ID that uses the typeid bit layout. +func Int64IDTime(id int64) time.Time { + return time.UnixMilli(id >> Int64IDBits) +} + +// FloorInt64ID returns the lowest possible Int64 ID for timestamp t. +// All 15 random bits are zero. +func FloorInt64ID(t time.Time) int64 { + return t.UnixMilli() << Int64IDBits +} + +// CeilInt64ID returns the highest possible Int64 ID for timestamp t. +// All 15 random bits are one. +func CeilInt64ID(t time.Time) int64 { + return t.UnixMilli()< Date: Thu, 9 Apr 2026 18:26:34 +0200 Subject: [PATCH 3/5] feat(dbtype): add TimeRange squirrel.Sqlizer for time-based ID queries --- dbtype/timerange.go | 65 +++++++++++++++++++++++++++ dbtype/timerange_test.go | 95 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 dbtype/timerange.go create mode 100644 dbtype/timerange_test.go diff --git a/dbtype/timerange.go b/dbtype/timerange.go new file mode 100644 index 0000000..eb490dd --- /dev/null +++ b/dbtype/timerange.go @@ -0,0 +1,65 @@ +package dbtype + +import ( + "fmt" + "time" +) + +// TimeRange holds optional floor/ceil bounds for time-based ID range queries +// against a primary key column. It implements squirrel.Sqlizer, so it can be +// passed directly to squirrel.Where(). +// +// Construct via [UUIDv7Range] or [Int64IDRange]. +type TimeRange struct { + column string + floor any + ceil any +} + +// UUIDv7Range builds a TimeRange that brackets column with [FloorUUIDv7] / [CeilUUIDv7]. +// Nil since or until leaves that side unbounded. +func UUIDv7Range(column string, since, until *time.Time) TimeRange { + r := TimeRange{column: column} + if since != nil { + r.floor = FloorUUIDv7(*since).String() + } + if until != nil { + r.ceil = CeilUUIDv7(*until).String() + } + return r +} + +// Int64IDRange builds a TimeRange that brackets column with [FloorInt64ID] / [CeilInt64ID]. +// Nil since or until leaves that side unbounded. +func Int64IDRange(column string, since, until *time.Time) TimeRange { + r := TimeRange{column: column} + if since != nil { + r.floor = FloorInt64ID(*since) + } + if until != nil { + r.ceil = CeilInt64ID(*until) + } + return r +} + +// Floor returns the lower-bound value and true, or (nil, false) if unbounded. +func (r TimeRange) Floor() (any, bool) { return r.floor, r.floor != nil } + +// Ceil returns the upper-bound value and true, or (nil, false) if unbounded. +func (r TimeRange) Ceil() (any, bool) { return r.ceil, r.ceil != nil } + +// ToSql implements squirrel.Sqlizer. +// Returns "column BETWEEN ? AND ?", "column >= ?", "column <= ?", +// or "1=1" depending on which bounds are set. +func (r TimeRange) ToSql() (string, []any, error) { + switch { + case r.floor != nil && r.ceil != nil: + return fmt.Sprintf("%s BETWEEN ? AND ?", r.column), []any{r.floor, r.ceil}, nil + case r.floor != nil: + return fmt.Sprintf("%s >= ?", r.column), []any{r.floor}, nil + case r.ceil != nil: + return fmt.Sprintf("%s <= ?", r.column), []any{r.ceil}, nil + default: + return "1=1", nil, nil + } +} diff --git a/dbtype/timerange_test.go b/dbtype/timerange_test.go new file mode 100644 index 0000000..54f7ca7 --- /dev/null +++ b/dbtype/timerange_test.go @@ -0,0 +1,95 @@ +package dbtype_test + +import ( + "testing" + "time" + + "github.com/goware/pgkit/v2/dbtype" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTimeRange_UUIDv7_BothBounds(t *testing.T) { + since := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + until := time.Date(2026, 3, 27, 0, 0, 0, 0, time.UTC) + r := dbtype.UUIDv7Range("id", &since, &until) + + sql, args, err := r.ToSql() + require.NoError(t, err) + assert.Equal(t, "id BETWEEN ? AND ?", sql) + assert.Len(t, args, 2) +} + +func TestTimeRange_UUIDv7_SinceOnly(t *testing.T) { + since := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + r := dbtype.UUIDv7Range("id", &since, nil) + + sql, args, err := r.ToSql() + require.NoError(t, err) + assert.Equal(t, "id >= ?", sql) + assert.Len(t, args, 1) +} + +func TestTimeRange_UUIDv7_UntilOnly(t *testing.T) { + until := time.Date(2026, 3, 27, 0, 0, 0, 0, time.UTC) + r := dbtype.UUIDv7Range("id", nil, &until) + + sql, args, err := r.ToSql() + require.NoError(t, err) + assert.Equal(t, "id <= ?", sql) + assert.Len(t, args, 1) +} + +func TestTimeRange_UUIDv7_NeitherBound(t *testing.T) { + r := dbtype.UUIDv7Range("id", nil, nil) + + sql, args, err := r.ToSql() + require.NoError(t, err) + assert.Equal(t, "1=1", sql) + assert.Empty(t, args) +} + +func TestTimeRange_Int64ID_BothBounds(t *testing.T) { + since := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + until := time.Date(2026, 3, 27, 0, 0, 0, 0, time.UTC) + r := dbtype.Int64IDRange("id", &since, &until) + + sql, args, err := r.ToSql() + require.NoError(t, err) + assert.Equal(t, "id BETWEEN ? AND ?", sql) + assert.Len(t, args, 2) +} + +func TestTimeRange_FloorCeil_Accessors(t *testing.T) { + since := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + r := dbtype.UUIDv7Range("id", &since, nil) + + _, ok := r.Floor() + assert.True(t, ok) + _, ok = r.Ceil() + assert.False(t, ok) +} + +func TestTimeRange_UUIDv7_BoundValues(t *testing.T) { + since := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + until := time.Date(2026, 3, 27, 0, 0, 0, 0, time.UTC) + r := dbtype.UUIDv7Range("id", &since, &until) + + _, args, err := r.ToSql() + require.NoError(t, err) + + assert.Equal(t, dbtype.FloorUUIDv7(since).String(), args[0]) + assert.Equal(t, dbtype.CeilUUIDv7(until).String(), args[1]) +} + +func TestTimeRange_Int64ID_BoundValues(t *testing.T) { + since := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + until := time.Date(2026, 3, 27, 0, 0, 0, 0, time.UTC) + r := dbtype.Int64IDRange("id", &since, &until) + + _, args, err := r.ToSql() + require.NoError(t, err) + + assert.Equal(t, dbtype.FloorInt64ID(since), args[0]) + assert.Equal(t, dbtype.CeilInt64ID(until), args[1]) +} From 58f8b17badf5787e993f53a160758ece637ffa48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20-=20=E3=82=A2=E3=83=AC=E3=83=83=E3=82=AF=E3=82=B9?= Date: Thu, 9 Apr 2026 18:29:50 +0200 Subject: [PATCH 4/5] refactor(dbtype): drop google/uuid dep, use [16]byte for UUIDv7 Replace uuid.UUID with raw [16]byte throughout. Add FormatUUID helper for string representation. No external dependencies added. --- dbtype/timerange.go | 4 +-- dbtype/timerange_test.go | 4 +-- dbtype/uuidv7.go | 31 ++++++++++++++++------ dbtype/uuidv7_test.go | 56 +++++++++++++++++++++++++--------------- go.mod | 1 - go.sum | 2 -- 6 files changed, 62 insertions(+), 36 deletions(-) diff --git a/dbtype/timerange.go b/dbtype/timerange.go index eb490dd..343575c 100644 --- a/dbtype/timerange.go +++ b/dbtype/timerange.go @@ -21,10 +21,10 @@ type TimeRange struct { func UUIDv7Range(column string, since, until *time.Time) TimeRange { r := TimeRange{column: column} if since != nil { - r.floor = FloorUUIDv7(*since).String() + r.floor = FormatUUID(FloorUUIDv7(*since)) } if until != nil { - r.ceil = CeilUUIDv7(*until).String() + r.ceil = FormatUUID(CeilUUIDv7(*until)) } return r } diff --git a/dbtype/timerange_test.go b/dbtype/timerange_test.go index 54f7ca7..0af00eb 100644 --- a/dbtype/timerange_test.go +++ b/dbtype/timerange_test.go @@ -78,8 +78,8 @@ func TestTimeRange_UUIDv7_BoundValues(t *testing.T) { _, args, err := r.ToSql() require.NoError(t, err) - assert.Equal(t, dbtype.FloorUUIDv7(since).String(), args[0]) - assert.Equal(t, dbtype.CeilUUIDv7(until).String(), args[1]) + assert.Equal(t, dbtype.FormatUUID(dbtype.FloorUUIDv7(since)), args[0]) + assert.Equal(t, dbtype.FormatUUID(dbtype.CeilUUIDv7(until)), args[1]) } func TestTimeRange_Int64ID_BoundValues(t *testing.T) { diff --git a/dbtype/uuidv7.go b/dbtype/uuidv7.go index 1759374..347490c 100644 --- a/dbtype/uuidv7.go +++ b/dbtype/uuidv7.go @@ -1,20 +1,35 @@ package dbtype import ( + "encoding/hex" "time" - - "github.com/google/uuid" ) // UUIDv7Time extracts the millisecond-precision creation timestamp // embedded in a UUIDv7 (bytes 0-5, big-endian uint48 ms since epoch). -func UUIDv7Time(u uuid.UUID) time.Time { +func UUIDv7Time(u [16]byte) time.Time { ms := int64(u[0])<<40 | int64(u[1])<<32 | int64(u[2])<<24 | int64(u[3])<<16 | int64(u[4])<<8 | int64(u[5]) return time.UnixMilli(ms) } -func setUUIDv7Timestamp(u *uuid.UUID, t time.Time) { +// FormatUUID formats a [16]byte as a standard UUID string +// (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx). +func FormatUUID(u [16]byte) string { + var buf [36]byte + hex.Encode(buf[0:8], u[0:4]) + buf[8] = '-' + hex.Encode(buf[9:13], u[4:6]) + buf[13] = '-' + hex.Encode(buf[14:18], u[6:8]) + buf[18] = '-' + hex.Encode(buf[19:23], u[8:10]) + buf[23] = '-' + hex.Encode(buf[24:36], u[10:16]) + return string(buf[:]) +} + +func setUUIDv7Timestamp(u *[16]byte, t time.Time) { ms := uint64(t.UnixMilli()) u[0] = byte(ms >> 40) u[1] = byte(ms >> 32) @@ -26,8 +41,8 @@ func setUUIDv7Timestamp(u *uuid.UUID, t time.Time) { // FloorUUIDv7 returns the lowest valid UUIDv7 for timestamp t. // Version and variant bits are set; all random bits are zero. -func FloorUUIDv7(t time.Time) uuid.UUID { - var u uuid.UUID +func FloorUUIDv7(t time.Time) [16]byte { + var u [16]byte setUUIDv7Timestamp(&u, t) u[6] = 0x70 // version 7, rand_a = 0 u[8] = 0x80 // variant 10xxxxxx, rand_b = 0 @@ -36,8 +51,8 @@ func FloorUUIDv7(t time.Time) uuid.UUID { // CeilUUIDv7 returns the highest valid UUIDv7 for timestamp t. // Version and variant bits are set; all random bits are one. -func CeilUUIDv7(t time.Time) uuid.UUID { - var u uuid.UUID +func CeilUUIDv7(t time.Time) [16]byte { + var u [16]byte setUUIDv7Timestamp(&u, t) u[6] = 0x7f // version 7, rand_a high nibble all 1s u[7] = 0xff // rand_a low byte all 1s diff --git a/dbtype/uuidv7_test.go b/dbtype/uuidv7_test.go index 97b5ae4..86e1f73 100644 --- a/dbtype/uuidv7_test.go +++ b/dbtype/uuidv7_test.go @@ -1,53 +1,68 @@ package dbtype_test import ( + "crypto/rand" "testing" "time" - "github.com/google/uuid" "github.com/goware/pgkit/v2/dbtype" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +// newTestUUIDv7 generates a valid UUIDv7 for testing without external deps. +func newTestUUIDv7() [16]byte { + var u [16]byte + ms := uint64(time.Now().UnixMilli()) + u[0] = byte(ms >> 40) + u[1] = byte(ms >> 32) + u[2] = byte(ms >> 24) + u[3] = byte(ms >> 16) + u[4] = byte(ms >> 8) + u[5] = byte(ms) + _, _ = rand.Read(u[6:]) + u[6] = (u[6] & 0x0f) | 0x70 // version 7 + u[8] = (u[8] & 0x3f) | 0x80 // variant RFC4122 + return u +} + func TestFloorUUIDv7_VersionAndVariant(t *testing.T) { u := dbtype.FloorUUIDv7(time.Now()) - assert.Equal(t, uuid.Version(7), u.Version()) - assert.Equal(t, uuid.RFC4122, u.Variant()) + assert.Equal(t, byte(0x70), u[6]&0xf0, "version nibble") + assert.Equal(t, byte(0x80), u[8]&0xc0, "variant bits") } func TestCeilUUIDv7_VersionAndVariant(t *testing.T) { u := dbtype.CeilUUIDv7(time.Now()) - assert.Equal(t, uuid.Version(7), u.Version()) - assert.Equal(t, uuid.RFC4122, u.Variant()) + assert.Equal(t, byte(0x70), u[6]&0xf0, "version nibble") + assert.Equal(t, byte(0x80), u[8]&0xc0, "variant bits") } func TestFloorUUIDv7_LessThanNewV7(t *testing.T) { now := time.Now() - floor := dbtype.FloorUUIDv7(now) + floor := dbtype.FormatUUID(dbtype.FloorUUIDv7(now)) for range 100 { - v7, err := uuid.NewV7() - require.NoError(t, err) - assert.LessOrEqual(t, floor.String(), v7.String()) + v7 := newTestUUIDv7() + assert.LessOrEqual(t, floor, dbtype.FormatUUID(v7)) } } func TestCeilUUIDv7_GreaterThanFloor(t *testing.T) { now := time.Now() - floor := dbtype.FloorUUIDv7(now) - ceil := dbtype.CeilUUIDv7(now) - assert.Greater(t, ceil.String(), floor.String()) + floor := dbtype.FormatUUID(dbtype.FloorUUIDv7(now)) + ceil := dbtype.FormatUUID(dbtype.CeilUUIDv7(now)) + assert.Greater(t, ceil, floor) } func TestFloorCeilUUIDv7_BracketRealID(t *testing.T) { - v7, err := uuid.NewV7() - require.NoError(t, err) + v7 := newTestUUIDv7() ts := dbtype.UUIDv7Time(v7) - floor := dbtype.FloorUUIDv7(ts) - ceil := dbtype.CeilUUIDv7(ts) - assert.LessOrEqual(t, floor.String(), v7.String()) - assert.GreaterOrEqual(t, ceil.String(), v7.String()) + floor := dbtype.FormatUUID(dbtype.FloorUUIDv7(ts)) + ceil := dbtype.FormatUUID(dbtype.CeilUUIDv7(ts)) + id := dbtype.FormatUUID(v7) + assert.LessOrEqual(t, floor, id) + assert.GreaterOrEqual(t, ceil, id) } func TestFloorUUIDv7_TimestampRoundTrip(t *testing.T) { @@ -58,9 +73,8 @@ func TestFloorUUIDv7_TimestampRoundTrip(t *testing.T) { } func TestUUIDv7Time_ExtractsTimestamp(t *testing.T) { - v7, err := uuid.NewV7() - require.NoError(t, err) + v7 := newTestUUIDv7() got := dbtype.UUIDv7Time(v7) - assert.False(t, got.IsZero()) + require.False(t, got.IsZero()) assert.WithinDuration(t, time.Now(), got, time.Second) } diff --git a/go.mod b/go.mod index ba186ba..b1b28bb 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ toolchain go1.24.3 require ( github.com/Masterminds/squirrel v1.5.4 github.com/georgysavva/scany/v2 v2.1.4 - github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.7.6 github.com/stretchr/testify v1.11.1 ) diff --git a/go.sum b/go.sum index 4943359..9c238b9 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,6 @@ github.com/georgysavva/scany/v2 v2.1.4 h1:nrzHEJ4oQVRoiKmocRqA1IyGOmM/GQOEsg9UjM github.com/georgysavva/scany/v2 v2.1.4/go.mod h1:fqp9yHZzM/PFVa3/rYEC57VmDx+KDch0LoqrJzkvtos= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= From 89270fd8c712006480f18cbad256cf65d1b396ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20-=20=E3=82=A2=E3=83=AC=E3=83=83=E3=82=AF=E3=82=B9?= Date: Fri, 10 Apr 2026 14:10:19 +0200 Subject: [PATCH 5/5] refactor(dbtype): pass [16]byte directly to TimeRange, skip string conversion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pgx natively encodes [16]byte as Postgres UUID — no need to convert to string for bind args. --- dbtype/timerange.go | 4 ++-- dbtype/timerange_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dbtype/timerange.go b/dbtype/timerange.go index 343575c..1b996e5 100644 --- a/dbtype/timerange.go +++ b/dbtype/timerange.go @@ -21,10 +21,10 @@ type TimeRange struct { func UUIDv7Range(column string, since, until *time.Time) TimeRange { r := TimeRange{column: column} if since != nil { - r.floor = FormatUUID(FloorUUIDv7(*since)) + r.floor = FloorUUIDv7(*since) } if until != nil { - r.ceil = FormatUUID(CeilUUIDv7(*until)) + r.ceil = CeilUUIDv7(*until) } return r } diff --git a/dbtype/timerange_test.go b/dbtype/timerange_test.go index 0af00eb..5d11bdf 100644 --- a/dbtype/timerange_test.go +++ b/dbtype/timerange_test.go @@ -78,8 +78,8 @@ func TestTimeRange_UUIDv7_BoundValues(t *testing.T) { _, args, err := r.ToSql() require.NoError(t, err) - assert.Equal(t, dbtype.FormatUUID(dbtype.FloorUUIDv7(since)), args[0]) - assert.Equal(t, dbtype.FormatUUID(dbtype.CeilUUIDv7(until)), args[1]) + assert.Equal(t, dbtype.FloorUUIDv7(since), args[0]) + assert.Equal(t, dbtype.CeilUUIDv7(until), args[1]) } func TestTimeRange_Int64ID_BoundValues(t *testing.T) {