refactor: use GraphQL string scalar for KSUID transit

Marshal and unmarshal both use the base62 KSUID string, replacing the
legacy JSON object wrapper and aligning gqlgen scalar behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-29 18:03:39 -07:00
parent 29b855f2b9
commit 0f8e79a890
7 changed files with 36 additions and 39 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
### Breaking changes
- Removed `UUID`, `NewUUID`, and `ParseUUID`.
- Added `StringKSUID` and `BinaryKSUID` with distinct SQL encodings and shared GraphQL transit (`ksuid_str` JSON field, replacing `uuid_str`).
- Added `StringKSUID` and `BinaryKSUID` with distinct SQL encodings and shared GraphQL string scalar transit.
- Removed `DBConfig.AutoMigrate`, `SQLOFunc`, and `JSONB.ToMap()`.
- Replaced `Column` / `ColumnList` intersection interfaces with dialect-neutral type aliases.
- `Apply*` and `Expr*` helpers now accept `Column` / `ColumnList` / `Expression` aliases instead of `mysql.*` types.
+1 -1
View File
@@ -57,7 +57,7 @@ For Postgres, use `dbx.DialectPostgres` and blank-import `dbxp` instead of `dbxm
| `StringKSUID` | `VARCHAR(27)`, `TEXT` | Base62 string |
| `BinaryKSUID` | `BINARY(20)`, `BYTEA` | Raw 20 bytes |
Both types use identical GraphQL JSON transit (`ksuid_str` field). Pick the type that matches your column encoding — Scan rejects ambiguous payloads.
Both types use identical GraphQL string scalar transit. Pick the type that matches your column encoding — Scan rejects ambiguous payloads.
## Dialect notes
+1 -1
View File
@@ -23,7 +23,7 @@
// # Identifier types
//
// [StringKSUID] and [BinaryKSUID] wrap segmentio/ksuid with storage-specific
// SQL encoding. Both share identical GraphQL string transit. Choose StringKSUID
// SQL encoding. Both share identical GraphQL string scalar transit. Choose StringKSUID
// for text columns (VARCHAR, TEXT); choose BinaryKSUID for binary columns
// (BINARY(20), BYTEA).
//
+2 -2
View File
@@ -9,8 +9,8 @@ import (
)
// BinaryKSUID wraps segmentio/ksuid for binary-column storage (BINARY(20), BYTEA).
// SQL Scan/Value use the raw 20-byte encoding. GraphQL transit uses the shared
// string JSON transport (see ksuid_gql.go).
// SQL Scan/Value use the raw 20-byte encoding. GraphQL transit uses a string
// scalar (base62 KSUID).
type BinaryKSUID struct {
ksuid.KSUID
}
+5 -12
View File
@@ -1,17 +1,13 @@
package dbx
import (
"encoding/json"
"fmt"
"io"
"strconv"
"github.com/segmentio/ksuid"
)
type ksuidGQLTransport struct {
KSUIDStr string `json:"ksuid_str"`
}
func unmarshalKSUIDFromGQL(value any, unmarshal func([]byte) error) error {
str, ok := value.(string)
if !ok {
@@ -20,14 +16,11 @@ func unmarshalKSUIDFromGQL(value any, unmarshal func([]byte) error) error {
return unmarshal([]byte(str))
}
// marshalKSUIDToGQL writes a JSON-encoded string scalar for gqlgen.
func marshalKSUIDToGQL(w io.Writer, id ksuid.KSUID) {
transport := ksuidGQLTransport{KSUIDStr: id.String()}
data, err := json.Marshal(transport)
if err != nil {
panic(fmt.Errorf("GraphQL failed to JSON-marshal KSUID value: %s", err))
}
if _, err := w.Write(data); err != nil {
panic(fmt.Errorf("GraphQL failed to write KSUID value: %s", string(data)))
quoted := strconv.Quote(id.String())
if _, err := io.WriteString(w, quoted); err != nil {
panic(fmt.Errorf("GraphQL failed to write KSUID value: %w", err))
}
}
+2 -2
View File
@@ -9,8 +9,8 @@ import (
)
// StringKSUID wraps segmentio/ksuid for text-column storage (VARCHAR, TEXT).
// SQL Scan/Value use the base62 string encoding. GraphQL transit uses the
// shared string JSON transport (see ksuid_gql.go).
// SQL Scan/Value use the base62 string encoding. GraphQL transit uses a string
// scalar (base62 KSUID).
type StringKSUID struct {
ksuid.KSUID
}
+24 -20
View File
@@ -3,6 +3,7 @@ package dbx
import (
"bytes"
"encoding/json"
"io"
"testing"
"github.com/segmentio/ksuid"
@@ -50,16 +51,32 @@ func TestStringKSUID_Conversion(t *testing.T) {
func TestStringKSUID_GQL(t *testing.T) {
id := NewStringKSUID()
wire := assertKSUIDGQLMarshal(t, id)
var parsed StringKSUID
require.NoError(t, parsed.UnmarshalGQL(wire))
assert.Equal(t, id, parsed)
}
func TestBinaryKSUID_GQL(t *testing.T) {
id := NewBinaryKSUID()
wire := assertKSUIDGQLMarshal(t, id)
var parsed BinaryKSUID
require.NoError(t, parsed.UnmarshalGQL(wire))
assert.Equal(t, id, parsed)
}
func assertKSUIDGQLMarshal(t *testing.T, id interface{ String() string; MarshalGQL(w io.Writer) }) string {
t.Helper()
var buf bytes.Buffer
id.MarshalGQL(&buf)
var transport ksuidGQLTransport
require.NoError(t, json.Unmarshal(buf.Bytes(), &transport))
assert.Equal(t, id.String(), transport.KSUIDStr)
var parsed StringKSUID
require.NoError(t, parsed.UnmarshalGQL(id.String()))
assert.Equal(t, id, parsed)
var wire string
require.NoError(t, json.Unmarshal(buf.Bytes(), &wire))
assert.Equal(t, id.String(), wire)
return wire
}
func TestBinaryKSUID_ScanValue(t *testing.T) {
@@ -100,19 +117,6 @@ func TestBinaryKSUID_Conversion(t *testing.T) {
assert.Equal(t, b, s.AsBinaryKSUID())
}
func TestBinaryKSUID_GQL(t *testing.T) {
id := NewBinaryKSUID()
var buf bytes.Buffer
id.MarshalGQL(&buf)
var transport ksuidGQLTransport
require.NoError(t, json.Unmarshal(buf.Bytes(), &transport))
assert.Equal(t, id.String(), transport.KSUIDStr)
var parsed BinaryKSUID
require.NoError(t, parsed.UnmarshalGQL(id.String()))
assert.Equal(t, id, parsed)
}
func TestParseStringKSUID(t *testing.T) {
id := ksuid.New()