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 ### Breaking changes
- Removed `UUID`, `NewUUID`, and `ParseUUID`. - 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()`. - Removed `DBConfig.AutoMigrate`, `SQLOFunc`, and `JSONB.ToMap()`.
- Replaced `Column` / `ColumnList` intersection interfaces with dialect-neutral type aliases. - Replaced `Column` / `ColumnList` intersection interfaces with dialect-neutral type aliases.
- `Apply*` and `Expr*` helpers now accept `Column` / `ColumnList` / `Expression` aliases instead of `mysql.*` types. - `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 | | `StringKSUID` | `VARCHAR(27)`, `TEXT` | Base62 string |
| `BinaryKSUID` | `BINARY(20)`, `BYTEA` | Raw 20 bytes | | `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 ## Dialect notes
+1 -1
View File
@@ -23,7 +23,7 @@
// # Identifier types // # Identifier types
// //
// [StringKSUID] and [BinaryKSUID] wrap segmentio/ksuid with storage-specific // [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 // for text columns (VARCHAR, TEXT); choose BinaryKSUID for binary columns
// (BINARY(20), BYTEA). // (BINARY(20), BYTEA).
// //
+2 -2
View File
@@ -9,8 +9,8 @@ import (
) )
// BinaryKSUID wraps segmentio/ksuid for binary-column storage (BINARY(20), BYTEA). // 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 // SQL Scan/Value use the raw 20-byte encoding. GraphQL transit uses a string
// string JSON transport (see ksuid_gql.go). // scalar (base62 KSUID).
type BinaryKSUID struct { type BinaryKSUID struct {
ksuid.KSUID ksuid.KSUID
} }
+5 -12
View File
@@ -1,17 +1,13 @@
package dbx package dbx
import ( import (
"encoding/json"
"fmt" "fmt"
"io" "io"
"strconv"
"github.com/segmentio/ksuid" "github.com/segmentio/ksuid"
) )
type ksuidGQLTransport struct {
KSUIDStr string `json:"ksuid_str"`
}
func unmarshalKSUIDFromGQL(value any, unmarshal func([]byte) error) error { func unmarshalKSUIDFromGQL(value any, unmarshal func([]byte) error) error {
str, ok := value.(string) str, ok := value.(string)
if !ok { if !ok {
@@ -20,14 +16,11 @@ func unmarshalKSUIDFromGQL(value any, unmarshal func([]byte) error) error {
return unmarshal([]byte(str)) return unmarshal([]byte(str))
} }
// marshalKSUIDToGQL writes a JSON-encoded string scalar for gqlgen.
func marshalKSUIDToGQL(w io.Writer, id ksuid.KSUID) { func marshalKSUIDToGQL(w io.Writer, id ksuid.KSUID) {
transport := ksuidGQLTransport{KSUIDStr: id.String()} quoted := strconv.Quote(id.String())
data, err := json.Marshal(transport) if _, err := io.WriteString(w, quoted); err != nil {
if err != nil { panic(fmt.Errorf("GraphQL failed to write KSUID value: %w", err))
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)))
} }
} }
+2 -2
View File
@@ -9,8 +9,8 @@ import (
) )
// StringKSUID wraps segmentio/ksuid for text-column storage (VARCHAR, TEXT). // StringKSUID wraps segmentio/ksuid for text-column storage (VARCHAR, TEXT).
// SQL Scan/Value use the base62 string encoding. GraphQL transit uses the // SQL Scan/Value use the base62 string encoding. GraphQL transit uses a string
// shared string JSON transport (see ksuid_gql.go). // scalar (base62 KSUID).
type StringKSUID struct { type StringKSUID struct {
ksuid.KSUID ksuid.KSUID
} }
+24 -20
View File
@@ -3,6 +3,7 @@ package dbx
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"io"
"testing" "testing"
"github.com/segmentio/ksuid" "github.com/segmentio/ksuid"
@@ -50,16 +51,32 @@ func TestStringKSUID_Conversion(t *testing.T) {
func TestStringKSUID_GQL(t *testing.T) { func TestStringKSUID_GQL(t *testing.T) {
id := NewStringKSUID() 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 var buf bytes.Buffer
id.MarshalGQL(&buf) id.MarshalGQL(&buf)
var transport ksuidGQLTransport var wire string
require.NoError(t, json.Unmarshal(buf.Bytes(), &transport)) require.NoError(t, json.Unmarshal(buf.Bytes(), &wire))
assert.Equal(t, id.String(), transport.KSUIDStr) assert.Equal(t, id.String(), wire)
return wire
var parsed StringKSUID
require.NoError(t, parsed.UnmarshalGQL(id.String()))
assert.Equal(t, id, parsed)
} }
func TestBinaryKSUID_ScanValue(t *testing.T) { func TestBinaryKSUID_ScanValue(t *testing.T) {
@@ -100,19 +117,6 @@ func TestBinaryKSUID_Conversion(t *testing.T) {
assert.Equal(t, b, s.AsBinaryKSUID()) 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) { func TestParseStringKSUID(t *testing.T) {
id := ksuid.New() id := ksuid.New()