Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions typeid/typeid-go/bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ var testTypeIDs = func() []typeid.TypeID {
var ids []typeid.TypeID
for _, prefix := range prefixPatterns {
for i := 0; i < 10; i++ {
ids = append(ids, typeid.Must(typeid.Generate(prefix)))
ids = append(ids, typeid.MustGenerate(prefix))
}
}
return ids
Expand Down Expand Up @@ -431,8 +431,8 @@ func BenchmarkTypicalUsage(b *testing.B) {
// Client pattern: few creates, many string conversions
b.Run("client", func(b *testing.B) {
// Pre-create TypeIDs
userID := typeid.Must(typeid.Generate("user"))
sessionID := typeid.Must(typeid.Generate("session"))
userID := typeid.MustGenerate("user")
sessionID := typeid.MustGenerate("session")

b.ReportAllocs()
b.ResetTimer()
Expand All @@ -457,11 +457,11 @@ func BenchmarkTypicalUsage(b *testing.B) {
var tid typeid.TypeID
for b.Loop() {
// Create new request ID
tid = typeid.Must(typeid.Generate("req"))
tid = typeid.MustGenerate("req")
s = tid.String()

// Create response ID
tid = typeid.Must(typeid.Generate("resp"))
tid = typeid.MustGenerate("resp")
s = tid.String()
}

Expand Down
10 changes: 5 additions & 5 deletions typeid/typeid-go/encoding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func TestJSONValid(t *testing.T) {
for _, td := range testdata {
t.Run(td.Name, func(t *testing.T) {
// Test MarshalText via JSON encoding
tid := typeid.Must(typeid.Parse(td.Tid))
tid := typeid.MustParse(td.Tid)
encoded, err := json.Marshal(tid)
assert.NoError(t, err)
assert.Equal(t, `"`+td.Tid+`"`, string(encoded))
Expand All @@ -47,7 +47,7 @@ func TestAppendTextValid(t *testing.T) {

for _, td := range testdata {
t.Run(td.Name, func(t *testing.T) {
tid := typeid.Must(typeid.Parse(td.Tid))
tid := typeid.MustParse(td.Tid)

// Test AppendText with nil slice (equivalent to MarshalText)
result, err := tid.AppendText(nil)
Expand Down Expand Up @@ -121,21 +121,21 @@ func TestJSONOmitZero(t *testing.T) {
},
{
name: "constructed zero ID",
typeID: typeid.Must(typeid.Parse("00000000000000000000000000")),
typeID: typeid.MustParse("00000000000000000000000000"),
expectedWithout: `{"id":"00000000000000000000000000"}`,
expectedWith: `{}`,
description: "constructed zero ID should omit with omitzero tag",
},
{
name: "prefixed zero ID",
typeID: typeid.Must(typeid.Parse("user_00000000000000000000000000")),
typeID: typeid.MustParse("user_00000000000000000000000000"),
expectedWithout: `{"id":"user_00000000000000000000000000"}`,
expectedWith: `{"id":"user_00000000000000000000000000"}`,
description: "prefixed zero ID should not omit because IsZero() returns false",
},
{
name: "non-zero ID",
typeID: typeid.Must(typeid.Parse("prefix_01h455vb4pex5vsknk084sn02q")),
typeID: typeid.MustParse("prefix_01h455vb4pex5vsknk084sn02q"),
expectedWithout: `{"id":"prefix_01h455vb4pex5vsknk084sn02q"}`,
expectedWith: `{"id":"prefix_01h455vb4pex5vsknk084sn02q"}`,
description: "non-zero ID should always be included",
Expand Down
12 changes: 7 additions & 5 deletions typeid/typeid-go/examples_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package typeid_test

import (
"database/sql"
"encoding/json"
"fmt"

Expand Down Expand Up @@ -47,7 +48,7 @@ func ExampleTypeID_MarshalText() {

// Create a product with TypeID
product := Product{
ID: typeid.Must(typeid.Parse("product_00041061050r3gg28a1c60t3gf")),
ID: typeid.MustParse("product_00041061050r3gg28a1c60t3gf"),
Name: "Widget",
Price: 29.99,
}
Expand Down Expand Up @@ -124,9 +125,10 @@ func ExampleTypeID_Scan() {
// Retrieved user user_00041061050r3gg28a1c60t3gf from database
}

// ExampleNullableID demonstrates using NullableID for nullable database columns
func ExampleNullableID() {
var managerID typeid.NullableID
// Example_nullableColumns demonstrates using sql.Null[TypeID] for nullable database columns.
// This is the recommended approach for handling nullable TypeID columns in Go applications.
func Example_nullableColumns() {
var managerID sql.Null[typeid.TypeID]

// Scan NULL value from database
err := managerID.Scan(nil)
Expand All @@ -141,7 +143,7 @@ func ExampleNullableID() {
panic(err)
}
fmt.Printf("Is valid: %v\n", managerID.Valid)
fmt.Printf("Manager: %s\n", managerID.TypeID.String())
fmt.Printf("Manager: %s\n", managerID.V.String())

// Output:
// Is valid: false
Expand Down
12 changes: 12 additions & 0 deletions typeid/typeid-go/shared_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package typeid

// MustParse returns a TypeID if the error is nil, otherwise panics.
// Used in tests to create a TypeID in a single line as follows:
// tid := MustParse("prefix_abc123")
func MustParse(s string) TypeID {
tid, err := Parse(s)
if err != nil {
panic(err)
}
return tid
}
39 changes: 1 addition & 38 deletions typeid/typeid-go/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"fmt"
)

// TODO: decide if we want nullable (or just use pointers)
// For nullable TypeID columns, use sql.Null[TypeID].

// Scan implements the sql.Scanner interface so the TypeIDs can be read from
// databases transparently. Currently database types that map to string are
Expand Down Expand Up @@ -35,40 +35,3 @@ func (tid *TypeID) Scan(src any) error {
func (tid TypeID) Value() (driver.Value, error) {
return tid.String(), nil
}

// NullableID is wrapper for nullable columns.
type NullableID struct {
TypeID TypeID
Valid bool
}

func (n NullableID) Value() (driver.Value, error) {
if !n.Valid {
return nil, nil // SQL NULL
}
return n.TypeID.Value() // Delegate to TypeID
}

func (n *NullableID) Scan(src any) error {
if src == nil {
n.TypeID, n.Valid = zeroID, false
return nil
}

// Empty string is invalid even for nullable columns - force explicit NULL usage
if str, ok := src.(string); ok && str == "" {
return &validationError{
Message: "empty string is invalid TypeID",
}
}

// Try to scan the TypeID, only set Valid=true if successful
err := n.TypeID.Scan(src)
if err != nil {
n.Valid = false
return err
}

n.Valid = true
return nil
}
57 changes: 28 additions & 29 deletions typeid/typeid-go/sql_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package typeid_test

import (
"database/sql"
_ "embed"
"errors"
"testing"
Expand Down Expand Up @@ -30,7 +31,7 @@ func TestScanValid(t *testing.T) {
err := scanned.Scan(td.Tid)
assert.NoError(t, err)

expected := typeid.Must(typeid.Parse(td.Tid))
expected := typeid.MustParse(td.Tid)
assert.Equal(t, expected, scanned)
assert.Equal(t, td.Tid, scanned.String())
})
Expand Down Expand Up @@ -113,70 +114,71 @@ func TestValue(t *testing.T) {

for _, td := range testdata {
t.Run(td.Name, func(t *testing.T) {
tid := typeid.Must(typeid.Parse(td.Tid))
tid := typeid.MustParse(td.Tid)
actual, err := tid.Value()
assert.NoError(t, err)
assert.Equal(t, td.Tid, actual)
})
}
}

func TestNullableIDScanValid(t *testing.T) {
// Test sql.Null[TypeID] to verify it works for nullable database columns
func TestSQLNullScanValid(t *testing.T) {
var testdata []ValidExample
err := yaml.Unmarshal(validSQLYML, &testdata)
require.NoError(t, err)

for _, td := range testdata {
t.Run(td.Name, func(t *testing.T) {
// Test NullableID.Scan with valid TypeID strings
var scanned typeid.NullableID
// Test sql.Null[TypeID].Scan with valid TypeID strings
var scanned sql.Null[typeid.TypeID]
err := scanned.Scan(td.Tid)
assert.NoError(t, err)

expected := typeid.Must(typeid.Parse(td.Tid))
assert.True(t, scanned.Valid, "NullableID should be valid for valid typeid")
assert.Equal(t, expected, scanned.TypeID)
assert.Equal(t, td.Tid, scanned.TypeID.String())
expected := typeid.MustParse(td.Tid)
assert.True(t, scanned.Valid, "sql.Null[TypeID] should be valid for valid typeid")
assert.Equal(t, expected, scanned.V)
assert.Equal(t, td.Tid, scanned.V.String())
})
}
}

func TestNullableIDScanSpecialCases(t *testing.T) {
func TestSQLNullScanSpecialCases(t *testing.T) {
testdata := []struct {
name string
input any
expected typeid.NullableID
expected sql.Null[typeid.TypeID]
expectError bool
}{
{"nil", nil, typeid.NullableID{Valid: false}, false},
{"empty string", "", typeid.NullableID{}, true},
{"nil", nil, sql.Null[typeid.TypeID]{Valid: false}, false},
{"empty string", "", sql.Null[typeid.TypeID]{}, true},
}

for _, td := range testdata {
t.Run(td.name, func(t *testing.T) {
var scanned typeid.NullableID
var scanned sql.Null[typeid.TypeID]
err := scanned.Scan(td.input)

if td.expectError {
assert.Error(t, err)
assert.Contains(t, err.Error(), "empty string is invalid TypeID")
assert.Contains(t, err.Error(), "cannot scan empty string into TypeID")
Copy link

Copilot AI Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test now expects "cannot scan empty string into TypeID", but TypeID.Scan still returns "empty string is invalid TypeID". Align the test or change the error message in TypeID.Scan so they match.

Copilot uses AI. Check for mistakes.
// Verify that scan errors are validation errors
assert.True(t, errors.Is(err, typeid.ErrValidation), "expected validation error")
} else {
assert.NoError(t, err)
assert.Equal(t, td.expected.Valid, scanned.Valid)
if td.expected.Valid {
assert.Equal(t, td.expected.TypeID, scanned.TypeID)
assert.Equal(t, td.expected.V, scanned.V)
}
}
})
}
}

func TestNullableIDValue(t *testing.T) {
func TestSQLNullValue(t *testing.T) {
// Test the invalid case (Valid: false)
t.Run("invalid", func(t *testing.T) {
invalid := typeid.NullableID{Valid: false}
invalid := sql.Null[typeid.TypeID]{Valid: false}
actual, err := invalid.Value()
assert.NoError(t, err)
assert.Equal(t, nil, actual)
Expand All @@ -189,32 +191,31 @@ func TestNullableIDValue(t *testing.T) {

for _, td := range testdata {
t.Run(td.Name, func(t *testing.T) {
tid := typeid.Must(typeid.Parse(td.Tid))
nullable := typeid.NullableID{TypeID: tid, Valid: true}
tid := typeid.MustParse(td.Tid)
nullable := sql.Null[typeid.TypeID]{V: tid, Valid: true}
actual, err := nullable.Value()
assert.NoError(t, err)
assert.Equal(t, td.Tid, actual)
})
}
}

func TestNullableIDScanInvalid(t *testing.T) {
func TestSQLNullScanInvalid(t *testing.T) {
var testdata []InvalidExample
err := yaml.Unmarshal(invalidSQLYML, &testdata)
require.NoError(t, err)

for _, td := range testdata {
t.Run(td.Name, func(t *testing.T) {
// Test NullableID.Scan with invalid TypeID strings
var scanned typeid.NullableID
// Test sql.Null[TypeID].Scan with invalid TypeID strings
var scanned sql.Null[typeid.TypeID]
err := scanned.Scan(td.Tid)
assert.Error(t, err, "NullableID.Scan should fail for invalid typeid: %s", td.Tid)
assert.False(t, scanned.Valid, "NullableID should not be valid after scan error")
assert.Error(t, err, "sql.Null[TypeID].Scan should fail for invalid typeid: %s", td.Tid)
})
}
}

func TestNullableIDScanUnsupportedType(t *testing.T) {
func TestSQLNullScanUnsupportedType(t *testing.T) {
testdata := []struct {
name string
input any
Expand All @@ -233,12 +234,10 @@ func TestNullableIDScanUnsupportedType(t *testing.T) {

for _, td := range testdata {
t.Run(td.name, func(t *testing.T) {
var scanned typeid.NullableID
var scanned sql.Null[typeid.TypeID]
err := scanned.Scan(td.input)
assert.Error(t, err)
assert.Contains(t, err.Error(), "unsupported scan type")
assert.False(t, scanned.Valid, "NullableID should not be valid after scan error")
// Verify that scan errors are validation errors
assert.True(t, errors.Is(err, typeid.ErrValidation), "expected validation error")
})
}
Expand Down
10 changes: 0 additions & 10 deletions typeid/typeid-go/typeid.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,3 @@ func (tid TypeID) HasSuffix() bool {
func (tid TypeID) IsZero() bool {
return tid.value == ""
}

// Must returns a TypeID if the error is nil, otherwise panics.
// Often used with Parse() to create a TypeID in a single line as follows:
// tid := Must(Parse("prefix_abc123"))
func Must(tid TypeID, err error) TypeID {
if err != nil {
panic(err)
}
return tid
}
Loading