From fc6dd134821b20304eaa14bdcce36518b9f699cd Mon Sep 17 00:00:00 2001 From: Jeff Linse Date: Fri, 9 May 2025 19:18:57 -0700 Subject: [PATCH] Add support for scanning Postgres tuples --- typeid/typeid-go/sql.go | 34 ++++++++++++++++++++++++++++++---- typeid/typeid-go/sql_test.go | 12 +++++++++--- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/typeid/typeid-go/sql.go b/typeid/typeid-go/sql.go index be901538..07c47fff 100644 --- a/typeid/typeid-go/sql.go +++ b/typeid/typeid-go/sql.go @@ -3,8 +3,11 @@ package typeid import ( "database/sql/driver" "fmt" + "strings" ) +const minTupleStringLength = 40 // (x,00000000-0000-0000-0000-000000000000) + // Scan implements the sql.Scanner interface so the TypeIDs can be read from // databases transparently. Currently database types that map to string are // supported. @@ -17,10 +20,33 @@ func (tid *TypeID[P]) Scan(src any) error { return nil } return tid.UnmarshalText([]byte(obj)) - // TODO: add support for []byte - // we don't just want to store the full string as a byte array. Instead - // we should encode using the UUID bytes. We could add support for - // Binary Marshalling and Unmarshalling at the same time. + case []byte: + if len(obj) == 0 { + return nil + } + + // typeid-sql can store TypeIDs as tuples of the form (prefix,uuid). + if len(obj) < minTupleStringLength || obj[0] != '(' || obj[len(obj)-1] != ')' { + // TODO: add support for []byte + // we don't just want to store the full string as a byte array. Instead + // we should encode using the UUID bytes. We could add support for + // Binary Marshalling and Unmarshalling at the same time. + return fmt.Errorf("unsupported format for scan type %T", obj) + } + + obj = obj[1 : len(obj)-1] + parts := strings.Split(string(obj), ",") + if len(parts) != 2 { + return fmt.Errorf("invalid TypeID format: %s", obj) + } + + parsedID, err := fromUUID[TypeID[P]](parts[0], parts[1]) + if err != nil { + return fmt.Errorf("invalid UUID: %s: %w", parts[1], err) + } + + *tid = parsedID + return nil default: return fmt.Errorf("unsupported scan type %T", obj) } diff --git a/typeid/typeid-go/sql_test.go b/typeid/typeid-go/sql_test.go index 221cc4ba..185abc22 100644 --- a/typeid/typeid-go/sql_test.go +++ b/typeid/typeid-go/sql_test.go @@ -8,18 +8,23 @@ import ( ) func TestScan(t *testing.T) { + t.Parallel() + testdata := []struct { name string input any expected typeid.AnyID }{ - {"valid", "prefix_00041061050r3gg28a1c60t3gf", typeid.Must(typeid.FromString("prefix_00041061050r3gg28a1c60t3gf"))}, + {"valid_text", "prefix_01jtvs4hppfp8azhhy9x703dc1", typeid.Must(typeid.FromString("prefix_01jtvs4hppfp8azhhy9x703dc1"))}, + {"valid_tuple", []byte("(prefix,0196b792-46d6-7d90-afc6-3e4f4e01b581)"), typeid.Must(typeid.FromString("prefix_01jtvs4hppfp8azhhy9x703dc1"))}, {"nil", nil, typeid.AnyID{}}, {"empty string", "", typeid.AnyID{}}, } for _, td := range testdata { t.Run(td.name, func(t *testing.T) { + t.Parallel() + var scanned typeid.AnyID err := scanned.Scan(td.input) assert.NoError(t, err) @@ -31,8 +36,9 @@ func TestScan(t *testing.T) { } func TestValuer(t *testing.T) { - expected := "prefix_00041061050r3gg28a1c60t3gf" - tid := typeid.Must(typeid.FromString("prefix_00041061050r3gg28a1c60t3gf")) + t.Parallel() + expected := "prefix_01jtvs4hppfp8azhhy9x703dc1" + tid := typeid.Must(typeid.FromString("prefix_01jtvs4hppfp8azhhy9x703dc1")) actual, err := tid.Value() assert.NoError(t, err) assert.Equal(t, expected, actual)