Compare commits
2 Commits
main
...
81e724f72b
| Author | SHA1 | Date | |
|---|---|---|---|
| 81e724f72b | |||
| fa4efb38f7 |
@@ -16,6 +16,8 @@
|
|||||||
- `Query`, `MustQuery`, and `UpdateOne` helpers (+ Context variants).
|
- `Query`, `MustQuery`, and `UpdateOne` helpers (+ Context variants).
|
||||||
- `WithTxValue` for transactional functions that return a value.
|
- `WithTxValue` for transactional functions that return a value.
|
||||||
- `QueryCount`, `BuildQueryCountFn`, and `CountResult` for pagination counts.
|
- `QueryCount`, `BuildQueryCountFn`, and `CountResult` for pagination counts.
|
||||||
|
- `BinaryKSUID.BinExpr`, `ExprStringKSUIDs`, and flexible `Parse*Any` KSUID parsers.
|
||||||
|
- `MarshalUint64` and `UnmarshalUint64` for JS-safe GraphQL uint64 scalars.
|
||||||
- Context-aware variants for all query and mutation helpers.
|
- Context-aware variants for all query and mutation helpers.
|
||||||
- `Delete`, `DeleteAffected`, `WithTx`, `ContainsCol`, and `CurrentDialect()`.
|
- `Delete`, `DeleteAffected`, `WithTx`, `ContainsCol`, and `CurrentDialect()`.
|
||||||
- Package documentation (`doc.go`), expanded README, and subpackage docs for `dbxm` / `dbxp`.
|
- Package documentation (`doc.go`), expanded README, and subpackage docs for `dbxm` / `dbxp`.
|
||||||
|
|||||||
@@ -45,10 +45,11 @@ For Postgres, use `dbx.DialectPostgres` and blank-import `dbxp` instead of `dbxm
|
|||||||
| Query | `Fetch`, `MustFetch`, `FetchOne`, `MustFetchOne`, `Query`, `MustQuery` (+ `*Context` variants) |
|
| Query | `Fetch`, `MustFetch`, `FetchOne`, `MustFetchOne`, `Query`, `MustQuery` (+ `*Context` variants) |
|
||||||
| Mutations | `Insert`, `InsertReturning`, `Update`, `UpdateAffected`, `UpdateOne`, `UpdateReturning`, `Delete`, `DeleteAffected` (+ `*Context` variants) |
|
| Mutations | `Insert`, `InsertReturning`, `Update`, `UpdateAffected`, `UpdateOne`, `UpdateReturning`, `Delete`, `DeleteAffected` (+ `*Context` variants) |
|
||||||
| Transactions | `WithTx`, `WithTxValue` |
|
| Transactions | `WithTx`, `WithTxValue` |
|
||||||
| Columns | `NormalCols`, `ContainsCol`, `ExprValues`, `ExprStringers`, `QueryCount`, `BuildQueryCountFn` |
|
| Columns | `NormalCols`, `ContainsCol`, `ExprValues`, `ExprStringers`, `ExprStringKSUIDs`, `QueryCount`, `BuildQueryCountFn` |
|
||||||
| Partial update | `ApplyPtr`, `ApplyComplexPtr`, `ApplyInterfacePtr`, `ApplyVal` |
|
| Partial update | `ApplyPtr`, `ApplyComplexPtr`, `ApplyInterfacePtr`, `ApplyVal` |
|
||||||
| Pointers | `Ptr`, `Val`, `NowPtr`, `TrimPtr`, `TrimPtrToNil`, `IsZero` |
|
| Pointers | `Ptr`, `Val`, `NowPtr`, `TrimPtr`, `TrimPtrToNil`, `IsZero` |
|
||||||
| Types | `StringKSUID`, `BinaryKSUID`, `JSONB` |
|
| Types | `StringKSUID`, `BinaryKSUID`, `JSONB` |
|
||||||
|
| GraphQL | `MarshalUint64`, `UnmarshalUint64` |
|
||||||
|
|
||||||
## KSUID type selection
|
## KSUID type selection
|
||||||
|
|
||||||
|
|||||||
@@ -28,10 +28,16 @@
|
|||||||
// SQL encoding. Both implement [ApplyInterface] via Equal and IsZero for use
|
// SQL encoding. Both implement [ApplyInterface] via Equal and IsZero for use
|
||||||
// with [ApplyInterfacePtr]. Both share identical GraphQL string scalar transit.
|
// with [ApplyInterfacePtr]. Both share identical GraphQL string scalar transit.
|
||||||
// Choose StringKSUID for text columns (VARCHAR, TEXT); choose BinaryKSUID for
|
// Choose StringKSUID for text columns (VARCHAR, TEXT); choose BinaryKSUID for
|
||||||
// binary columns (BINARY(20), BYTEA).
|
// binary columns (BINARY(20), BYTEA). [ParseStringKSUIDAny] and
|
||||||
|
// [ParseBinaryKSUIDAny] accept flexible input for API boundaries.
|
||||||
//
|
//
|
||||||
// [JSONB] provides map-based JSON column scanning for Postgres JSONB and MySQL JSON.
|
// [JSONB] provides map-based JSON column scanning for Postgres JSONB and MySQL JSON.
|
||||||
//
|
//
|
||||||
|
// # GraphQL scalars
|
||||||
|
//
|
||||||
|
// [MarshalUint64] and [UnmarshalUint64] provide JS-safe uint64 serialization for
|
||||||
|
// gqlgen without importing gqlgen directly.
|
||||||
|
//
|
||||||
// # Pointer and string utilities
|
// # Pointer and string utilities
|
||||||
//
|
//
|
||||||
// [Ptr], [Val], [TrimPtr], [StringToFilter], and related helpers for nullable
|
// [Ptr], [Val], [TrimPtr], [StringToFilter], and related helpers for nullable
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package dbx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Uint64Marshaler wraps a uint64 for GraphQL JSON output as a string scalar.
|
||||||
|
type Uint64Marshaler uint64
|
||||||
|
|
||||||
|
// MarshalGQL writes the uint64 as a JSON string for gqlgen compatibility.
|
||||||
|
func (u Uint64Marshaler) MarshalGQL(w io.Writer) {
|
||||||
|
fmt.Fprintf(w, "\"%d\"", uint64(u))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalUint64 marshals a uint64 as a JSON string to avoid JavaScript
|
||||||
|
// precision loss for integers larger than 2^53-1.
|
||||||
|
func MarshalUint64(i uint64) Uint64Marshaler {
|
||||||
|
return Uint64Marshaler(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalUint64 unmarshals a uint64 from a JSON string or number.
|
||||||
|
func UnmarshalUint64(v any) (uint64, error) {
|
||||||
|
switch value := v.(type) {
|
||||||
|
case string:
|
||||||
|
if value == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
n, err := strconv.ParseUint(value, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("invalid uint64 string: %w", err)
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
case int:
|
||||||
|
if value < 0 {
|
||||||
|
return 0, fmt.Errorf("invalid negative uint64: %d", value)
|
||||||
|
}
|
||||||
|
return uint64(value), nil
|
||||||
|
case int64:
|
||||||
|
if value < 0 {
|
||||||
|
return 0, fmt.Errorf("invalid negative uint64: %d", value)
|
||||||
|
}
|
||||||
|
return uint64(value), nil
|
||||||
|
case float64:
|
||||||
|
if value < 0 || value != float64(uint64(value)) {
|
||||||
|
return 0, fmt.Errorf("invalid uint64 number: %v", value)
|
||||||
|
}
|
||||||
|
return uint64(value), nil
|
||||||
|
case uint64:
|
||||||
|
return value, nil
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("invalid uint64 type: %T", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package dbx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMarshalUint64(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
MarshalUint64(9007199254740993).MarshalGQL(&buf)
|
||||||
|
assert.Equal(t, `"9007199254740993"`, buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalUint64_String(t *testing.T) {
|
||||||
|
n, err := UnmarshalUint64("42")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, uint64(42), n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalUint64_Number(t *testing.T) {
|
||||||
|
n, err := UnmarshalUint64(float64(42))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, uint64(42), n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalUint64_RoundTripJSON(t *testing.T) {
|
||||||
|
const original uint64 = 9007199254740993
|
||||||
|
var buf bytes.Buffer
|
||||||
|
MarshalUint64(original).MarshalGQL(&buf)
|
||||||
|
|
||||||
|
var wire string
|
||||||
|
require.NoError(t, json.Unmarshal(buf.Bytes(), &wire))
|
||||||
|
|
||||||
|
got, err := UnmarshalUint64(wire)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, original, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalUint64_Invalid(t *testing.T) {
|
||||||
|
_, err := UnmarshalUint64(true)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
_, err = UnmarshalUint64(float64(-1))
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
@@ -24,3 +24,12 @@ func ExprStringers(values []fmt.Stringer) []Expression {
|
|||||||
}
|
}
|
||||||
return expressions
|
return expressions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExprStringKSUIDs converts StringKSUID pointers to Jet string expressions.
|
||||||
|
func ExprStringKSUIDs(ids []*StringKSUID) []Expression {
|
||||||
|
expressions := make([]Expression, len(ids))
|
||||||
|
for i, id := range ids {
|
||||||
|
expressions[i] = mysql.String(id.String())
|
||||||
|
}
|
||||||
|
return expressions
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/go-jet/jet/v2/mysql"
|
||||||
"github.com/segmentio/ksuid"
|
"github.com/segmentio/ksuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -43,6 +44,14 @@ func ParseBinaryKSUID(b []byte) (BinaryKSUID, error) {
|
|||||||
return BinaryKSUID{KSUID: id}, nil
|
return BinaryKSUID{KSUID: id}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BinExpr returns a Jet string expression for binary-encoded IN clauses.
|
||||||
|
func (b BinaryKSUID) BinExpr() mysql.StringExpression {
|
||||||
|
if b.IsNil() {
|
||||||
|
return mysql.StringExp(mysql.NULL)
|
||||||
|
}
|
||||||
|
return mysql.String(string(b.Bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
// AsStringKSUID returns a StringKSUID view of the same identifier.
|
// AsStringKSUID returns a StringKSUID view of the same identifier.
|
||||||
func (b BinaryKSUID) AsStringKSUID() StringKSUID {
|
func (b BinaryKSUID) AsStringKSUID() StringKSUID {
|
||||||
return StringKSUID{KSUID: b.KSUID}
|
return StringKSUID{KSUID: b.KSUID}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package dbx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/segmentio/ksuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseStringKSUIDAny parses a StringKSUID from a string, byte slice, ksuid.KSUID,
|
||||||
|
// or nil. Accepts base62 string (27 bytes), raw binary (20 bytes), or hex-encoded
|
||||||
|
// forms (40 or 54 bytes).
|
||||||
|
func ParseStringKSUIDAny(src any) (StringKSUID, error) {
|
||||||
|
id, err := parseKSUIDAny(src)
|
||||||
|
if err != nil {
|
||||||
|
return NilStringKSUID, err
|
||||||
|
}
|
||||||
|
return StringKSUID{KSUID: id}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseBinaryKSUIDAny parses a BinaryKSUID from the same accepted forms as
|
||||||
|
// [ParseStringKSUIDAny].
|
||||||
|
func ParseBinaryKSUIDAny(src any) (BinaryKSUID, error) {
|
||||||
|
id, err := parseKSUIDAny(src)
|
||||||
|
if err != nil {
|
||||||
|
return NilBinaryKSUID, err
|
||||||
|
}
|
||||||
|
return BinaryKSUID{KSUID: id}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseKSUIDAny(src any) (ksuid.KSUID, error) {
|
||||||
|
switch v := src.(type) {
|
||||||
|
case ksuid.KSUID:
|
||||||
|
return v, nil
|
||||||
|
case StringKSUID:
|
||||||
|
return v.KSUID, nil
|
||||||
|
case BinaryKSUID:
|
||||||
|
return v.KSUID, nil
|
||||||
|
case string:
|
||||||
|
return parseKSUIDBytes([]byte(v))
|
||||||
|
case []byte:
|
||||||
|
return parseKSUIDBytes(v)
|
||||||
|
case nil:
|
||||||
|
return ksuid.Nil, nil
|
||||||
|
default:
|
||||||
|
return ksuid.Nil, fmt.Errorf("cannot parse KSUID from type %T", src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseKSUIDBytes(src []byte) (ksuid.KSUID, error) {
|
||||||
|
if len(src) == 40 || len(src) == 54 {
|
||||||
|
decoded := make([]byte, hex.DecodedLen(len(src)))
|
||||||
|
if _, err := hex.Decode(decoded, src); err != nil {
|
||||||
|
return ksuid.Nil, err
|
||||||
|
}
|
||||||
|
src = decoded
|
||||||
|
}
|
||||||
|
|
||||||
|
switch len(src) {
|
||||||
|
case 0:
|
||||||
|
return ksuid.Nil, nil
|
||||||
|
case ksuidBinaryLength:
|
||||||
|
return ksuid.FromBytes(src)
|
||||||
|
case ksuidStringLength:
|
||||||
|
return ksuid.Parse(string(src))
|
||||||
|
default:
|
||||||
|
return ksuid.Nil, fmt.Errorf("cannot parse KSUID from byte slice of length %d", len(src))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package dbx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/segmentio/ksuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseStringKSUIDAny(t *testing.T) {
|
||||||
|
id := ksuid.New()
|
||||||
|
stringForm := id.String()
|
||||||
|
byteForm := id.Bytes()
|
||||||
|
hexForm := hex.EncodeToString(byteForm)
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
src any
|
||||||
|
}{
|
||||||
|
{"string", stringForm},
|
||||||
|
{"bytes string form", []byte(stringForm)},
|
||||||
|
{"bytes binary form", byteForm},
|
||||||
|
{"hex", []byte(hexForm)},
|
||||||
|
{"ksuid", id},
|
||||||
|
{"StringKSUID", StringKSUID{KSUID: id}},
|
||||||
|
{"nil", nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got, err := ParseStringKSUIDAny(tc.src)
|
||||||
|
require.NoError(t, err)
|
||||||
|
if tc.src == nil {
|
||||||
|
assert.True(t, got.IsZero())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(t, id.String(), got.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseBinaryKSUIDAny(t *testing.T) {
|
||||||
|
id := ksuid.New()
|
||||||
|
got, err := ParseBinaryKSUIDAny(id.Bytes())
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, id.Bytes(), got.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseStringKSUIDAny_InvalidType(t *testing.T) {
|
||||||
|
_, err := ParseStringKSUIDAny(123)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBinaryKSUID_BinExpr(t *testing.T) {
|
||||||
|
id := NewBinaryKSUID()
|
||||||
|
expr := id.BinExpr()
|
||||||
|
require.NotNil(t, expr)
|
||||||
|
|
||||||
|
nilExpr := NilBinaryKSUID.BinExpr()
|
||||||
|
require.NotNil(t, nilExpr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExprStringKSUIDs(t *testing.T) {
|
||||||
|
a := NewStringKSUID()
|
||||||
|
b := NewStringKSUID()
|
||||||
|
exprs := ExprStringKSUIDs([]*StringKSUID{&a, &b})
|
||||||
|
require.Len(t, exprs, 2)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user