feat: add StringKSUID and BinaryKSUID types

Replace the UUID wrapper with storage-specific KSUID types that share
GraphQL string transit but use distinct SQL Scan/Value encodings.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-29 17:47:41 -07:00
parent 984b52d198
commit a303e025b2
6 changed files with 330 additions and 66 deletions
+76
View File
@@ -0,0 +1,76 @@
package dbx
import (
"database/sql/driver"
"fmt"
"io"
"github.com/segmentio/ksuid"
)
// 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).
type BinaryKSUID struct {
ksuid.KSUID
}
// NewBinaryKSUID generates a new BinaryKSUID.
func NewBinaryKSUID() BinaryKSUID {
return BinaryKSUID{KSUID: ksuid.New()}
}
// ParseBinaryKSUID parses a BinaryKSUID from its raw 20-byte form.
func ParseBinaryKSUID(b []byte) (BinaryKSUID, error) {
id, err := ksuid.FromBytes(b)
if err != nil {
return BinaryKSUID{}, err
}
return BinaryKSUID{KSUID: id}, nil
}
// AsStringKSUID returns a StringKSUID view of the same identifier.
func (b BinaryKSUID) AsStringKSUID() StringKSUID {
return StringKSUID{KSUID: b.KSUID}
}
// Scan implements sql.Scanner for binary-backed KSUID columns.
func (b *BinaryKSUID) Scan(src any) error {
switch v := src.(type) {
case nil:
*b = BinaryKSUID{}
return nil
case []byte:
if len(v) == ksuidStringLength {
return fmt.Errorf("BinaryKSUID.Scan: string-encoded KSUID (%d bytes); use StringKSUID", len(v))
}
id, err := ksuid.FromBytes(v)
if err != nil {
return fmt.Errorf("BinaryKSUID.Scan: %w", err)
}
*b = BinaryKSUID{KSUID: id}
return nil
case string:
return fmt.Errorf("BinaryKSUID.Scan: string value %q; use StringKSUID or store raw bytes", v)
default:
return fmt.Errorf("BinaryKSUID.Scan: unable to scan type %T", v)
}
}
// Value implements driver.Valuer for binary-backed KSUID columns.
func (b BinaryKSUID) Value() (driver.Value, error) {
if b.IsNil() {
return nil, nil
}
return b.Bytes(), nil
}
// UnmarshalGQL implements the graphql.Unmarshaler interface.
func (b *BinaryKSUID) UnmarshalGQL(value any) error {
return unmarshalKSUIDFromGQL(value, b.UnmarshalText)
}
// MarshalGQL implements the graphql.Marshaler interface.
func (b BinaryKSUID) MarshalGQL(w io.Writer) {
marshalKSUIDToGQL(w, b.KSUID)
}