refactor: ♻️ change resolvspec types to build in sqltypes
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
# sqltypes
|
||||
|
||||
Nullable SQL types for hand-written or generated Go models. Each type wraps a
|
||||
value with a `Valid` flag and implements `database/sql.Scanner`,
|
||||
`driver.Valuer`, `encoding/json`, `gopkg.in/yaml.v3`, and `encoding/xml`
|
||||
marshalling — so a single struct field can be scanned from a database row,
|
||||
round-tripped through JSON/YAML/XML, and written back to the database without
|
||||
any per-format glue code.
|
||||
|
||||
This package is what the `bun` and `gorm` writers emit when generating models
|
||||
with `--types sqltypes` (see [`pkg/writers/bun`](../writers/bun/README.md) and
|
||||
[`pkg/writers/gorm`](../writers/gorm/README.md)). It can also be imported
|
||||
directly in hand-written models.
|
||||
|
||||
## Import
|
||||
|
||||
```go
|
||||
import sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
```
|
||||
|
||||
## Scalar types
|
||||
|
||||
All scalar types are instantiations of the generic `SqlNull[T]`:
|
||||
|
||||
| Type | Underlying | Typical SQL type |
|
||||
|---|---|---|
|
||||
| `SqlInt16` | `int16` | `smallint` |
|
||||
| `SqlInt32` | `int32` | `integer` |
|
||||
| `SqlInt64` | `int64` | `bigint` |
|
||||
| `SqlFloat32` | `float32` | `real`, `float4` |
|
||||
| `SqlFloat64` | `float64` | `double precision`, `numeric`, `decimal`, `money` |
|
||||
| `SqlBool` | `bool` | `boolean` |
|
||||
| `SqlString` | `string` | `text`, `varchar`, `char`, `citext`, `inet`, `cidr`, `macaddr` |
|
||||
| `SqlByteArray` | `[]byte` | `bytea` (base64-encoded in JSON/YAML/XML) |
|
||||
| `SqlUUID` | `uuid.UUID` (`github.com/google/uuid`) | `uuid` |
|
||||
|
||||
You can also instantiate `SqlNull[T]` directly for any type not covered
|
||||
above, e.g. `SqlNull[MyEnum]`.
|
||||
|
||||
### Date/time types
|
||||
|
||||
Plain `time.Time` doesn't distinguish date-only, time-only, and timestamp
|
||||
semantics, and its zero value marshals to a confusing `0001-01-01T00:00:00Z`.
|
||||
These wrapper types fix both problems:
|
||||
|
||||
| Type | Format | Notes |
|
||||
|---|---|---|
|
||||
| `SqlTimeStamp` | `2006-01-02T15:04:05` | Full timestamp |
|
||||
| `SqlDate` | `2006-01-02` | Date only |
|
||||
| `SqlTime` | `15:04:05` | Time only |
|
||||
|
||||
Zero/pre-epoch values (`time.Time{}` or anything before `0002-01-01`) marshal
|
||||
to `null` and `Value()` returns `nil`, instead of leaking Go's zero-time
|
||||
sentinel into the database or API responses.
|
||||
|
||||
### JSON types
|
||||
|
||||
| Type | Underlying | Notes |
|
||||
|---|---|---|
|
||||
| `SqlJSONB` | `[]byte` | Raw JSON bytes; `MarshalYAML` decodes to native YAML mappings/sequences instead of an embedded JSON string |
|
||||
| `SqlJSON` | `= SqlJSONB` | Alias — PostgreSQL's `json` and `jsonb` share the same Go representation |
|
||||
|
||||
`SqlJSONB` has `AsMap()` / `AsSlice()` helpers for pulling out
|
||||
`map[string]any` / `[]any` without a separate `json.Unmarshal` call.
|
||||
|
||||
### Vector type (pgvector)
|
||||
|
||||
`SqlVector` wraps `[]float32` for the `vector` column type ([pgvector](https://github.com/pgvector/pgvector)),
|
||||
scanning/writing the `[1,2,3]` literal format pgvector uses over the wire.
|
||||
|
||||
## Array types
|
||||
|
||||
PostgreSQL array columns (`text[]`, `integer[]`, …) map to `SqlXxxArray`
|
||||
types, each wrapping `Val []T` + `Valid bool` and handling PostgreSQL's
|
||||
`{a,b,c}` array literal format on `Scan`/`Value`:
|
||||
|
||||
`SqlStringArray`, `SqlInt16Array`, `SqlInt32Array`, `SqlInt64Array`,
|
||||
`SqlFloat32Array`, `SqlFloat64Array`, `SqlBoolArray`, `SqlUUIDArray`.
|
||||
|
||||
## Constructing values
|
||||
|
||||
Every type has a `NewSqlXxx(v)` constructor that sets `Valid: true`:
|
||||
|
||||
```go
|
||||
name := sql_types.NewSqlString("Ada Lovelace")
|
||||
age := sql_types.NewSqlInt32(36)
|
||||
tags := sql_types.NewSqlStringArray([]string{"engineer", "mathematician"})
|
||||
```
|
||||
|
||||
The zero value of any type (`sql_types.SqlString{}`) is null/invalid — use it
|
||||
directly for a `NULL` field instead of a separate constructor.
|
||||
|
||||
Generic helpers:
|
||||
|
||||
```go
|
||||
sql_types.Null(v, valid) // SqlNull[T]{Val: v, Valid: valid}
|
||||
sql_types.NewSql[T](anyValue) // best-effort conversion from any Go value
|
||||
```
|
||||
|
||||
## Reading values back
|
||||
|
||||
Each scalar type has typed accessors that return the zero value instead of
|
||||
panicking when `Valid` is false:
|
||||
|
||||
```go
|
||||
n.Int64() // SqlInt16/32/64, SqlFloat32/64, SqlBool, SqlString → int64
|
||||
n.Float64() // → float64
|
||||
n.Bool() // → bool
|
||||
n.Time() // SqlNull[time.Time]-based types → time.Time
|
||||
n.UUID() // SqlUUID → uuid.UUID
|
||||
n.String() // fmt.Stringer — empty string when invalid
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
ID sql_types.SqlUUID `json:"id"`
|
||||
Name sql_types.SqlString `json:"name"`
|
||||
Tags sql_types.SqlStringArray `json:"tags"`
|
||||
Metadata sql_types.SqlJSONB `json:"metadata"`
|
||||
CreatedAt sql_types.SqlTimeStamp `json:"created_at"`
|
||||
}
|
||||
|
||||
u := User{
|
||||
ID: sql_types.NewSqlUUID(uuid.New()),
|
||||
Name: sql_types.NewSqlString("Ada Lovelace"),
|
||||
Tags: sql_types.NewSqlStringArray([]string{"engineer"}),
|
||||
CreatedAt: sql_types.SqlTimeStampNow(),
|
||||
}
|
||||
// Metadata left as the zero value → serializes as null, scans as NULL.
|
||||
```
|
||||
|
||||
Every type implements `sql.Scanner` and `driver.Valuer`, so these fields can
|
||||
be used directly as struct fields with `database/sql`, `bun`, or `gorm`
|
||||
without additional tags or hooks.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,485 @@
|
||||
package sqltypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func TestParsePostgresArrayElements(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want []string
|
||||
wantErr bool
|
||||
}{
|
||||
{"simple", "{a,b,c}", []string{"a", "b", "c"}, false},
|
||||
{"empty array", "{}", []string{}, false},
|
||||
{"null", "NULL", nil, false},
|
||||
{"lowercase null", "null", nil, false},
|
||||
{"empty string", "", nil, false},
|
||||
{"quoted with comma", `{a,"b,c",d}`, []string{"a", "b,c", "d"}, false},
|
||||
{"escaped quote", `{"a""b"}`, []string{`a"b`}, false},
|
||||
{"escaped backslash", `{"a\\b"}`, []string{`a\b`}, false},
|
||||
{"not an array", "abc", nil, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := parsePostgresArrayElements(tt.input)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(got) != len(tt.want) {
|
||||
t.Fatalf("expected %v, got %v", tt.want, got)
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tt.want[i] {
|
||||
t.Errorf("index %d: expected %q, got %q", i, tt.want[i], got[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatPostgresStringArray(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []string
|
||||
want string
|
||||
}{
|
||||
{"nil", nil, "NULL"},
|
||||
{"empty", []string{}, "{}"},
|
||||
{"simple", []string{"a", "b"}, "{a,b}"},
|
||||
{"needs quoting comma", []string{"a,b"}, `{"a,b"}`},
|
||||
{"needs quoting empty elem", []string{""}, `{""}`},
|
||||
{"needs quoting quote", []string{`a"b`}, `{"a""b"}`},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := formatPostgresStringArray(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("expected %q, got %q", tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlStringArray(t *testing.T) {
|
||||
t.Run("scan and value round-trip", func(t *testing.T) {
|
||||
var a SqlStringArray
|
||||
if err := a.Scan(`{a,"b,c",d}`); err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
want := []string{"a", "b,c", "d"}
|
||||
if len(a.Val) != len(want) {
|
||||
t.Fatalf("expected %v, got %v", want, a.Val)
|
||||
}
|
||||
for i := range want {
|
||||
if a.Val[i] != want[i] {
|
||||
t.Errorf("index %d: expected %q, got %q", i, want[i], a.Val[i])
|
||||
}
|
||||
}
|
||||
|
||||
val, err := a.Value()
|
||||
if err != nil {
|
||||
t.Fatalf("Value failed: %v", err)
|
||||
}
|
||||
var b SqlStringArray
|
||||
if err := b.Scan(val); err != nil {
|
||||
t.Fatalf("re-scan failed: %v", err)
|
||||
}
|
||||
for i := range want {
|
||||
if b.Val[i] != want[i] {
|
||||
t.Errorf("round-trip index %d: expected %q, got %q", i, want[i], b.Val[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("scan nil", func(t *testing.T) {
|
||||
var a SqlStringArray
|
||||
if err := a.Scan(nil); err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
if a.Valid {
|
||||
t.Error("expected invalid")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("value invalid", func(t *testing.T) {
|
||||
a := SqlStringArray{Valid: false}
|
||||
val, err := a.Value()
|
||||
if err != nil {
|
||||
t.Fatalf("Value failed: %v", err)
|
||||
}
|
||||
if val != nil {
|
||||
t.Errorf("expected nil, got %v", val)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("scan wrong type", func(t *testing.T) {
|
||||
var a SqlStringArray
|
||||
if err := a.Scan(42); err == nil {
|
||||
t.Error("expected error for unsupported scan type")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("json round-trip", func(t *testing.T) {
|
||||
a := NewSqlStringArray([]string{"x", "y", "z"})
|
||||
data, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
if string(data) != `["x","y","z"]` {
|
||||
t.Errorf("unexpected JSON: %s", data)
|
||||
}
|
||||
var a2 SqlStringArray
|
||||
if err := json.Unmarshal(data, &a2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if !a2.Valid || len(a2.Val) != 3 {
|
||||
t.Fatalf("expected 3 valid elements, got %v", a2)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("json null", func(t *testing.T) {
|
||||
var a SqlStringArray
|
||||
data, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
if string(data) != "null" {
|
||||
t.Errorf("expected null, got %s", data)
|
||||
}
|
||||
var a2 SqlStringArray
|
||||
if err := json.Unmarshal([]byte("null"), &a2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if a2.Valid {
|
||||
t.Error("expected invalid after unmarshaling null")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("json unmarshal invalid type errors", func(t *testing.T) {
|
||||
var a SqlStringArray
|
||||
if err := json.Unmarshal([]byte(`42`), &a); err == nil {
|
||||
t.Error("expected error unmarshaling non-array JSON")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSqlInt16Array(t *testing.T) {
|
||||
t.Run("scan and value", func(t *testing.T) {
|
||||
var a SqlInt16Array
|
||||
if err := a.Scan("{1,2,-3}"); err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
want := []int16{1, 2, -3}
|
||||
for i := range want {
|
||||
if a.Val[i] != want[i] {
|
||||
t.Errorf("index %d: expected %d, got %d", i, want[i], a.Val[i])
|
||||
}
|
||||
}
|
||||
val, err := a.Value()
|
||||
if err != nil {
|
||||
t.Fatalf("Value failed: %v", err)
|
||||
}
|
||||
if val != "{1,2,-3}" {
|
||||
t.Errorf("expected {1,2,-3}, got %v", val)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("scan invalid element", func(t *testing.T) {
|
||||
var a SqlInt16Array
|
||||
if err := a.Scan("{1,abc}"); err == nil {
|
||||
t.Error("expected error for non-numeric element")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("json round-trip", func(t *testing.T) {
|
||||
a := NewSqlInt16Array([]int16{5, 10, 15})
|
||||
data, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var a2 SqlInt16Array
|
||||
if err := json.Unmarshal(data, &a2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
for i, v := range a.Val {
|
||||
if a2.Val[i] != v {
|
||||
t.Errorf("index %d: expected %d, got %d", i, v, a2.Val[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSqlInt32Array(t *testing.T) {
|
||||
a := NewSqlInt32Array([]int32{100000, -200000})
|
||||
val, err := a.Value()
|
||||
if err != nil {
|
||||
t.Fatalf("Value failed: %v", err)
|
||||
}
|
||||
var b SqlInt32Array
|
||||
if err := b.Scan(val); err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
for i, v := range a.Val {
|
||||
if b.Val[i] != v {
|
||||
t.Errorf("index %d: expected %d, got %d", i, v, b.Val[i])
|
||||
}
|
||||
}
|
||||
|
||||
data, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var c SqlInt32Array
|
||||
if err := json.Unmarshal(data, &c); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
for i, v := range a.Val {
|
||||
if c.Val[i] != v {
|
||||
t.Errorf("index %d: expected %d, got %d", i, v, c.Val[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlInt64Array(t *testing.T) {
|
||||
a := NewSqlInt64Array([]int64{9223372036854775807, -9223372036854775808})
|
||||
val, err := a.Value()
|
||||
if err != nil {
|
||||
t.Fatalf("Value failed: %v", err)
|
||||
}
|
||||
var b SqlInt64Array
|
||||
if err := b.Scan(val); err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
for i, v := range a.Val {
|
||||
if b.Val[i] != v {
|
||||
t.Errorf("index %d: expected %d, got %d", i, v, b.Val[i])
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("null json", func(t *testing.T) {
|
||||
var n SqlInt64Array
|
||||
data, _ := json.Marshal(n)
|
||||
if string(data) != "null" {
|
||||
t.Errorf("expected null, got %s", data)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSqlFloat32Array(t *testing.T) {
|
||||
a := NewSqlFloat32Array([]float32{1.5, -2.25, 0})
|
||||
val, err := a.Value()
|
||||
if err != nil {
|
||||
t.Fatalf("Value failed: %v", err)
|
||||
}
|
||||
var b SqlFloat32Array
|
||||
if err := b.Scan(val); err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
for i, v := range a.Val {
|
||||
if b.Val[i] != v {
|
||||
t.Errorf("index %d: expected %v, got %v", i, v, b.Val[i])
|
||||
}
|
||||
}
|
||||
|
||||
data, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var c SqlFloat32Array
|
||||
if err := json.Unmarshal(data, &c); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
for i, v := range a.Val {
|
||||
if c.Val[i] != v {
|
||||
t.Errorf("index %d: expected %v, got %v", i, v, c.Val[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlFloat64Array(t *testing.T) {
|
||||
a := NewSqlFloat64Array([]float64{3.14159, -2.71828})
|
||||
val, err := a.Value()
|
||||
if err != nil {
|
||||
t.Fatalf("Value failed: %v", err)
|
||||
}
|
||||
var b SqlFloat64Array
|
||||
if err := b.Scan(val); err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
for i, v := range a.Val {
|
||||
if b.Val[i] != v {
|
||||
t.Errorf("index %d: expected %v, got %v", i, v, b.Val[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlBoolArray(t *testing.T) {
|
||||
t.Run("scan various truthy forms", func(t *testing.T) {
|
||||
var a SqlBoolArray
|
||||
if err := a.Scan("{t,f,true,false,1,0,yes}"); err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
want := []bool{true, false, true, false, true, false, true}
|
||||
for i := range want {
|
||||
if a.Val[i] != want[i] {
|
||||
t.Errorf("index %d: expected %v, got %v", i, want[i], a.Val[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("value formatting", func(t *testing.T) {
|
||||
a := NewSqlBoolArray([]bool{true, false})
|
||||
val, err := a.Value()
|
||||
if err != nil {
|
||||
t.Fatalf("Value failed: %v", err)
|
||||
}
|
||||
if val != "{t,f}" {
|
||||
t.Errorf("expected {t,f}, got %v", val)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("json round-trip", func(t *testing.T) {
|
||||
a := NewSqlBoolArray([]bool{true, false, true})
|
||||
data, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var a2 SqlBoolArray
|
||||
if err := json.Unmarshal(data, &a2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
for i, v := range a.Val {
|
||||
if a2.Val[i] != v {
|
||||
t.Errorf("index %d: expected %v, got %v", i, v, a2.Val[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSqlUUIDArray(t *testing.T) {
|
||||
u1, u2 := uuid.New(), uuid.New()
|
||||
|
||||
t.Run("scan and value round-trip", func(t *testing.T) {
|
||||
a := NewSqlUUIDArray([]uuid.UUID{u1, u2})
|
||||
val, err := a.Value()
|
||||
if err != nil {
|
||||
t.Fatalf("Value failed: %v", err)
|
||||
}
|
||||
var b SqlUUIDArray
|
||||
if err := b.Scan(val); err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
if b.Val[0] != u1 || b.Val[1] != u2 {
|
||||
t.Errorf("expected [%v %v], got %v", u1, u2, b.Val)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("scan invalid uuid element", func(t *testing.T) {
|
||||
var a SqlUUIDArray
|
||||
if err := a.Scan("{not-a-uuid}"); err == nil {
|
||||
t.Error("expected error for invalid uuid element")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("json round-trip", func(t *testing.T) {
|
||||
a := NewSqlUUIDArray([]uuid.UUID{u1, u2})
|
||||
data, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var a2 SqlUUIDArray
|
||||
if err := json.Unmarshal(data, &a2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if a2.Val[0] != u1 || a2.Val[1] != u2 {
|
||||
t.Errorf("expected [%v %v], got %v", u1, u2, a2.Val)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSqlVector(t *testing.T) {
|
||||
t.Run("scan and value round-trip", func(t *testing.T) {
|
||||
var v SqlVector
|
||||
if err := v.Scan("[1,2.5,-3]"); err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
want := []float32{1, 2.5, -3}
|
||||
for i := range want {
|
||||
if v.Val[i] != want[i] {
|
||||
t.Errorf("index %d: expected %v, got %v", i, want[i], v.Val[i])
|
||||
}
|
||||
}
|
||||
val, err := v.Value()
|
||||
if err != nil {
|
||||
t.Fatalf("Value failed: %v", err)
|
||||
}
|
||||
if val != "[1,2.5,-3]" {
|
||||
t.Errorf("expected [1,2.5,-3], got %v", val)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("scan empty vector", func(t *testing.T) {
|
||||
var v SqlVector
|
||||
if err := v.Scan("[]"); err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
if !v.Valid || len(v.Val) != 0 {
|
||||
t.Errorf("expected valid empty vector, got %v", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("scan invalid literal", func(t *testing.T) {
|
||||
var v SqlVector
|
||||
if err := v.Scan("not-a-vector"); err == nil {
|
||||
t.Error("expected error for invalid vector literal")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("json round-trip", func(t *testing.T) {
|
||||
v := NewSqlVector([]float32{0.1, 0.2, 0.3})
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var v2 SqlVector
|
||||
if err := json.Unmarshal(data, &v2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
for i, f := range v.Val {
|
||||
if v2.Val[i] != f {
|
||||
t.Errorf("index %d: expected %v, got %v", i, f, v2.Val[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("json null", func(t *testing.T) {
|
||||
var v SqlVector
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
if string(data) != "null" {
|
||||
t.Errorf("expected null, got %s", data)
|
||||
}
|
||||
var v2 SqlVector
|
||||
if err := json.Unmarshal([]byte("null"), &v2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if v2.Valid {
|
||||
t.Error("expected invalid after unmarshaling null")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,981 @@
|
||||
// Package sqltypes provides nullable SQL types with automatic casting and conversion methods.
|
||||
package sqltypes
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// tryParseDT attempts to parse a string into a time.Time using various formats.
|
||||
func tryParseDT(str string) (time.Time, error) {
|
||||
var lasterror error
|
||||
tryFormats := []string{
|
||||
time.RFC3339,
|
||||
"2006-01-02T15:04:05.000-0700",
|
||||
"2006-01-02T15:04:05.000",
|
||||
"06-01-02T15:04:05.000",
|
||||
"2006-01-02T15:04:05",
|
||||
"2006-01-02 15:04:05",
|
||||
"02/01/2006",
|
||||
"02-01-2006",
|
||||
"2006-01-02",
|
||||
"15:04:05.000",
|
||||
"15:04:05",
|
||||
"15:04",
|
||||
}
|
||||
for _, f := range tryFormats {
|
||||
tx, err := time.Parse(f, str)
|
||||
if err == nil {
|
||||
return tx, nil
|
||||
}
|
||||
lasterror = err
|
||||
}
|
||||
return time.Time{}, lasterror // Return zero time on failure
|
||||
}
|
||||
|
||||
// ToJSONDT formats a time.Time to RFC3339 string.
|
||||
func ToJSONDT(dt time.Time) string {
|
||||
return dt.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// SqlNull is a generic nullable type that behaves like sql.NullXXX with auto-casting.
|
||||
type SqlNull[T any] struct {
|
||||
Val T
|
||||
Valid bool
|
||||
}
|
||||
|
||||
// Scan implements sql.Scanner.
|
||||
func (n *SqlNull[T]) Scan(value any) error {
|
||||
if value == nil {
|
||||
n.Valid = false
|
||||
n.Val = *new(T)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if T is []byte, and decode base64 if applicable
|
||||
// Do this BEFORE trying sql.Null to ensure base64 is handled
|
||||
var zero T
|
||||
if _, ok := any(zero).([]byte); ok {
|
||||
// For []byte types, try to decode from base64
|
||||
var strVal string
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
strVal = v
|
||||
case []byte:
|
||||
strVal = string(v)
|
||||
default:
|
||||
strVal = fmt.Sprintf("%v", value)
|
||||
}
|
||||
// Try base64 decode
|
||||
if decoded, err := base64.StdEncoding.DecodeString(strVal); err == nil {
|
||||
n.Val = any(decoded).(T)
|
||||
n.Valid = true
|
||||
return nil
|
||||
}
|
||||
// Fallback to raw bytes
|
||||
n.Val = any([]byte(strVal)).(T)
|
||||
n.Valid = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try standard sql.Null[T] for other types.
|
||||
var sqlNull sql.Null[T]
|
||||
if err := sqlNull.Scan(value); err == nil {
|
||||
n.Val = sqlNull.V
|
||||
n.Valid = sqlNull.Valid
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fallback: parse from string/bytes.
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return n.FromString(v)
|
||||
case []byte:
|
||||
return n.FromString(string(v))
|
||||
case float32, float64:
|
||||
return n.FromString(fmt.Sprintf("%f", value))
|
||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
||||
return n.FromString(fmt.Sprintf("%d", value))
|
||||
default:
|
||||
return n.FromString(fmt.Sprintf("%v", value))
|
||||
}
|
||||
}
|
||||
func (n *SqlNull[T]) FromString(s string) error {
|
||||
s = strings.TrimSpace(s)
|
||||
n.Valid = false
|
||||
n.Val = *new(T)
|
||||
|
||||
if s == "" || strings.EqualFold(s, "null") {
|
||||
return nil
|
||||
}
|
||||
|
||||
var zero T
|
||||
switch any(zero).(type) {
|
||||
case int, int8, int16, int32, int64:
|
||||
if i, err := strconv.ParseInt(s, 10, 64); err == nil {
|
||||
reflect.ValueOf(&n.Val).Elem().SetInt(i)
|
||||
n.Valid = true
|
||||
} else if f, err := strconv.ParseFloat(s, 64); err == nil {
|
||||
reflect.ValueOf(&n.Val).Elem().SetInt(int64(f))
|
||||
n.Valid = true
|
||||
}
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
if u, err := strconv.ParseUint(s, 10, 64); err == nil {
|
||||
reflect.ValueOf(&n.Val).Elem().SetUint(u)
|
||||
n.Valid = true
|
||||
} else if f, err := strconv.ParseFloat(s, 64); err == nil && f >= 0 {
|
||||
reflect.ValueOf(&n.Val).Elem().SetUint(uint64(f))
|
||||
n.Valid = true
|
||||
}
|
||||
case float32, float64:
|
||||
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
||||
reflect.ValueOf(&n.Val).Elem().SetFloat(f)
|
||||
n.Valid = true
|
||||
}
|
||||
case bool:
|
||||
if b, err := strconv.ParseBool(s); err == nil {
|
||||
n.Val = any(b).(T)
|
||||
n.Valid = true
|
||||
}
|
||||
case time.Time:
|
||||
if t, err := tryParseDT(s); err == nil && !t.IsZero() {
|
||||
n.Val = any(t).(T)
|
||||
n.Valid = true
|
||||
}
|
||||
case uuid.UUID:
|
||||
if u, err := uuid.Parse(s); err == nil {
|
||||
n.Val = any(u).(T)
|
||||
n.Valid = true
|
||||
}
|
||||
case []byte:
|
||||
n.Val = any([]byte(s)).(T)
|
||||
n.Valid = true
|
||||
case string:
|
||||
n.Val = any(s).(T)
|
||||
n.Valid = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements driver.Valuer.
|
||||
func (n SqlNull[T]) Value() (driver.Value, error) {
|
||||
if !n.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Check if the type implements fmt.Stringer (e.g., uuid.UUID, custom types)
|
||||
// Convert to string for driver compatibility
|
||||
if stringer, ok := any(n.Val).(fmt.Stringer); ok {
|
||||
return stringer.String(), nil
|
||||
}
|
||||
|
||||
return any(n.Val), nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (n SqlNull[T]) MarshalJSON() ([]byte, error) {
|
||||
if !n.Valid {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
|
||||
// Check if T is []byte, and encode to base64
|
||||
if _, ok := any(n.Val).([]byte); ok {
|
||||
// Encode []byte as base64
|
||||
encoded := base64.StdEncoding.EncodeToString(any(n.Val).([]byte))
|
||||
return json.Marshal(encoded)
|
||||
}
|
||||
|
||||
return json.Marshal(n.Val)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (n *SqlNull[T]) UnmarshalJSON(b []byte) error {
|
||||
if len(b) == 0 || string(b) == "null" || strings.TrimSpace(string(b)) == "" {
|
||||
n.Valid = false
|
||||
n.Val = *new(T)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if T is []byte, and decode from base64
|
||||
var val T
|
||||
if _, ok := any(val).([]byte); ok {
|
||||
// Unmarshal as string first (JSON representation)
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err == nil {
|
||||
// Decode from base64
|
||||
if decoded, err := base64.StdEncoding.DecodeString(s); err == nil {
|
||||
n.Val = any(decoded).(T)
|
||||
n.Valid = true
|
||||
return nil
|
||||
}
|
||||
// Fallback to raw string as bytes
|
||||
n.Val = any([]byte(s)).(T)
|
||||
n.Valid = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(b, &val); err == nil {
|
||||
n.Val = val
|
||||
n.Valid = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fallback: unmarshal as string and parse.
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err == nil {
|
||||
return n.FromString(s)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot unmarshal %s into SqlNull[%T]", b, n.Val)
|
||||
}
|
||||
|
||||
// MarshalYAML implements yaml.Marshaler.
|
||||
func (n SqlNull[T]) MarshalYAML() (any, error) {
|
||||
if !n.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Check if T is []byte, and encode to base64 (mirrors MarshalJSON).
|
||||
if b, ok := any(n.Val).([]byte); ok {
|
||||
return base64.StdEncoding.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
return n.Val, nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshaler.
|
||||
func (n *SqlNull[T]) UnmarshalYAML(value *yaml.Node) error {
|
||||
if value == nil || value.Tag == "!!null" {
|
||||
n.Valid = false
|
||||
n.Val = *new(T)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if T is []byte, and decode from base64.
|
||||
var zero T
|
||||
if _, ok := any(zero).([]byte); ok {
|
||||
var s string
|
||||
if err := value.Decode(&s); err == nil {
|
||||
if decoded, err := base64.StdEncoding.DecodeString(s); err == nil {
|
||||
n.Val = any(decoded).(T)
|
||||
n.Valid = true
|
||||
return nil
|
||||
}
|
||||
n.Val = any([]byte(s)).(T)
|
||||
n.Valid = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var val T
|
||||
if err := value.Decode(&val); err == nil {
|
||||
n.Val = val
|
||||
n.Valid = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fallback: decode as string and parse.
|
||||
var s string
|
||||
if err := value.Decode(&s); err == nil {
|
||||
return n.FromString(s)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot unmarshal %q into SqlNull[%T]", value.Value, n.Val)
|
||||
}
|
||||
|
||||
// MarshalXML implements xml.Marshaler.
|
||||
func (n SqlNull[T]) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
if !n.Valid {
|
||||
return e.EncodeElement("", start)
|
||||
}
|
||||
|
||||
// Check if T is []byte, and encode to base64 (mirrors MarshalJSON).
|
||||
if b, ok := any(n.Val).([]byte); ok {
|
||||
return e.EncodeElement(base64.StdEncoding.EncodeToString(b), start)
|
||||
}
|
||||
|
||||
return e.EncodeElement(n.Val, start)
|
||||
}
|
||||
|
||||
// UnmarshalXML implements xml.Unmarshaler.
|
||||
//
|
||||
// XML has no native null representation, so an empty element unmarshals to
|
||||
// an invalid (null) value rather than a zero-value-but-valid one.
|
||||
func (n *SqlNull[T]) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
var s string
|
||||
if err := d.DecodeElement(&s, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == "" {
|
||||
n.Valid = false
|
||||
n.Val = *new(T)
|
||||
return nil
|
||||
}
|
||||
|
||||
var zero T
|
||||
if _, ok := any(zero).([]byte); ok {
|
||||
if decoded, err := base64.StdEncoding.DecodeString(s); err == nil {
|
||||
n.Val = any(decoded).(T)
|
||||
n.Valid = true
|
||||
return nil
|
||||
}
|
||||
n.Val = any([]byte(s)).(T)
|
||||
n.Valid = true
|
||||
return nil
|
||||
}
|
||||
|
||||
return n.FromString(s)
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer.
|
||||
func (n SqlNull[T]) String() string {
|
||||
if !n.Valid {
|
||||
return ""
|
||||
}
|
||||
// Check if the type implements fmt.Stringer for better string representation
|
||||
if stringer, ok := any(n.Val).(fmt.Stringer); ok {
|
||||
return stringer.String()
|
||||
}
|
||||
return fmt.Sprintf("%v", n.Val)
|
||||
}
|
||||
|
||||
// Int64 converts to int64 or 0 if invalid.
|
||||
func (n SqlNull[T]) Int64() int64 {
|
||||
if !n.Valid {
|
||||
return 0
|
||||
}
|
||||
v := reflect.ValueOf(any(n.Val))
|
||||
switch v.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return v.Int()
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return int64(v.Uint())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return int64(v.Float())
|
||||
case reflect.String:
|
||||
i, _ := strconv.ParseInt(v.String(), 10, 64)
|
||||
return i
|
||||
case reflect.Bool:
|
||||
if v.Bool() {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Float64 converts to float64 or 0.0 if invalid.
|
||||
func (n SqlNull[T]) Float64() float64 {
|
||||
if !n.Valid {
|
||||
return 0.0
|
||||
}
|
||||
v := reflect.ValueOf(any(n.Val))
|
||||
switch v.Kind() {
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return v.Float()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return float64(v.Int())
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return float64(v.Uint())
|
||||
case reflect.String:
|
||||
f, _ := strconv.ParseFloat(v.String(), 64)
|
||||
return f
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// Bool converts to bool or false if invalid.
|
||||
func (n SqlNull[T]) Bool() bool {
|
||||
if !n.Valid {
|
||||
return false
|
||||
}
|
||||
v := reflect.ValueOf(any(n.Val))
|
||||
if v.Kind() == reflect.Bool {
|
||||
return v.Bool()
|
||||
}
|
||||
s := strings.ToLower(strings.TrimSpace(fmt.Sprint(n.Val)))
|
||||
return s == "true" || s == "t" || s == "1" || s == "yes" || s == "on"
|
||||
}
|
||||
|
||||
// Time converts to time.Time or zero if invalid.
|
||||
func (n SqlNull[T]) Time() time.Time {
|
||||
if !n.Valid {
|
||||
return time.Time{}
|
||||
}
|
||||
if t, ok := any(n.Val).(time.Time); ok {
|
||||
return t
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// UUID converts to uuid.UUID or Nil if invalid.
|
||||
func (n SqlNull[T]) UUID() uuid.UUID {
|
||||
if !n.Valid {
|
||||
return uuid.Nil
|
||||
}
|
||||
if u, ok := any(n.Val).(uuid.UUID); ok {
|
||||
return u
|
||||
}
|
||||
return uuid.Nil
|
||||
}
|
||||
|
||||
// Type aliases for common types.
|
||||
type (
|
||||
SqlInt16 = SqlNull[int16]
|
||||
SqlInt32 = SqlNull[int32]
|
||||
SqlInt64 = SqlNull[int64]
|
||||
SqlFloat32 = SqlNull[float32]
|
||||
SqlFloat64 = SqlNull[float64]
|
||||
SqlBool = SqlNull[bool]
|
||||
SqlString = SqlNull[string]
|
||||
SqlByteArray = SqlNull[[]byte]
|
||||
SqlUUID = SqlNull[uuid.UUID]
|
||||
)
|
||||
|
||||
// SqlTimeStamp - Timestamp with custom formatting (YYYY-MM-DDTHH:MM:SS).
|
||||
type SqlTimeStamp struct{ SqlNull[time.Time] }
|
||||
|
||||
func (t SqlTimeStamp) MarshalJSON() ([]byte, error) {
|
||||
if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0002, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
return fmt.Appendf(nil, `"%s"`, t.Val.Format("2006-01-02T15:04:05")), nil
|
||||
}
|
||||
|
||||
func (t *SqlTimeStamp) UnmarshalJSON(b []byte) error {
|
||||
if err := t.SqlNull.UnmarshalJSON(b); err != nil {
|
||||
return err
|
||||
}
|
||||
if t.Valid && (t.Val.IsZero() || t.Val.Format("2006-01-02T15:04:05") == "0001-01-01T00:00:00") {
|
||||
t.Valid = false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t SqlTimeStamp) Value() (driver.Value, error) {
|
||||
if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0002, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
||||
return nil, nil
|
||||
}
|
||||
return t.Val.Format("2006-01-02T15:04:05"), nil
|
||||
}
|
||||
|
||||
func (t SqlTimeStamp) MarshalYAML() (any, error) {
|
||||
if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0002, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
||||
return nil, nil
|
||||
}
|
||||
return t.Val.Format("2006-01-02T15:04:05"), nil
|
||||
}
|
||||
|
||||
func (t *SqlTimeStamp) UnmarshalYAML(value *yaml.Node) error {
|
||||
if err := t.SqlNull.UnmarshalYAML(value); err != nil {
|
||||
return err
|
||||
}
|
||||
if t.Valid && (t.Val.IsZero() || t.Val.Format("2006-01-02T15:04:05") == "0001-01-01T00:00:00") {
|
||||
t.Valid = false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t SqlTimeStamp) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0002, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
||||
return e.EncodeElement("", start)
|
||||
}
|
||||
return e.EncodeElement(t.Val.Format("2006-01-02T15:04:05"), start)
|
||||
}
|
||||
|
||||
func (t *SqlTimeStamp) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
var s string
|
||||
if err := d.DecodeElement(&s, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == "" {
|
||||
t.Valid = false
|
||||
t.Val = time.Time{}
|
||||
return nil
|
||||
}
|
||||
tm, err := tryParseDT(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.Val = tm
|
||||
t.Valid = !tm.IsZero() && tm.Format("2006-01-02T15:04:05") != "0001-01-01T00:00:00"
|
||||
return nil
|
||||
}
|
||||
|
||||
func SqlTimeStampNow() SqlTimeStamp {
|
||||
return SqlTimeStamp{SqlNull: SqlNull[time.Time]{Val: time.Now(), Valid: true}}
|
||||
}
|
||||
|
||||
// SqlDate - Date only (YYYY-MM-DD).
|
||||
type SqlDate struct{ SqlNull[time.Time] }
|
||||
|
||||
func (d SqlDate) MarshalJSON() ([]byte, error) {
|
||||
if !d.Valid || d.Val.IsZero() {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
s := d.Val.Format("2006-01-02")
|
||||
if strings.HasPrefix(s, "0001-01-01") {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
return fmt.Appendf(nil, `"%s"`, s), nil
|
||||
}
|
||||
|
||||
func (d *SqlDate) UnmarshalJSON(b []byte) error {
|
||||
if err := d.SqlNull.UnmarshalJSON(b); err != nil {
|
||||
return err
|
||||
}
|
||||
if d.Valid && d.Val.Format("2006-01-02") <= "0001-01-01" {
|
||||
d.Valid = false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d SqlDate) Value() (driver.Value, error) {
|
||||
if !d.Valid || d.Val.IsZero() {
|
||||
return nil, nil
|
||||
}
|
||||
s := d.Val.Format("2006-01-02")
|
||||
if s <= "0001-01-01" {
|
||||
return nil, nil
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (d SqlDate) String() string {
|
||||
if !d.Valid {
|
||||
return ""
|
||||
}
|
||||
s := d.Val.Format("2006-01-02")
|
||||
if strings.HasPrefix(s, "0001-01-01") || strings.HasPrefix(s, "1800-12-31") {
|
||||
return ""
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (d SqlDate) MarshalYAML() (any, error) {
|
||||
if !d.Valid || d.Val.IsZero() {
|
||||
return nil, nil
|
||||
}
|
||||
s := d.Val.Format("2006-01-02")
|
||||
if strings.HasPrefix(s, "0001-01-01") {
|
||||
return nil, nil
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (d *SqlDate) UnmarshalYAML(value *yaml.Node) error {
|
||||
if err := d.SqlNull.UnmarshalYAML(value); err != nil {
|
||||
return err
|
||||
}
|
||||
if d.Valid && d.Val.Format("2006-01-02") <= "0001-01-01" {
|
||||
d.Valid = false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d SqlDate) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
if !d.Valid || d.Val.IsZero() {
|
||||
return e.EncodeElement("", start)
|
||||
}
|
||||
s := d.Val.Format("2006-01-02")
|
||||
if strings.HasPrefix(s, "0001-01-01") {
|
||||
return e.EncodeElement("", start)
|
||||
}
|
||||
return e.EncodeElement(s, start)
|
||||
}
|
||||
|
||||
func (d *SqlDate) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) error {
|
||||
var s string
|
||||
if err := dec.DecodeElement(&s, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == "" {
|
||||
d.Valid = false
|
||||
d.Val = time.Time{}
|
||||
return nil
|
||||
}
|
||||
tm, err := tryParseDT(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.Val = tm
|
||||
d.Valid = !tm.IsZero() && tm.Format("2006-01-02") > "0001-01-01"
|
||||
return nil
|
||||
}
|
||||
|
||||
func SqlDateNow() SqlDate {
|
||||
return SqlDate{SqlNull: SqlNull[time.Time]{Val: time.Now(), Valid: true}}
|
||||
}
|
||||
|
||||
// SqlTime - Time only (HH:MM:SS).
|
||||
type SqlTime struct{ SqlNull[time.Time] }
|
||||
|
||||
func (t SqlTime) MarshalJSON() ([]byte, error) {
|
||||
if !t.Valid || t.Val.IsZero() {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
s := t.Val.Format("15:04:05")
|
||||
if s == "00:00:00" {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
return fmt.Appendf(nil, `"%s"`, s), nil
|
||||
}
|
||||
|
||||
func (t *SqlTime) UnmarshalJSON(b []byte) error {
|
||||
if err := t.SqlNull.UnmarshalJSON(b); err != nil {
|
||||
return err
|
||||
}
|
||||
if t.Valid && t.Val.Format("15:04:05") == "00:00:00" {
|
||||
t.Valid = false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t SqlTime) Value() (driver.Value, error) {
|
||||
if !t.Valid || t.Val.IsZero() {
|
||||
return nil, nil
|
||||
}
|
||||
return t.Val.Format("15:04:05"), nil
|
||||
}
|
||||
|
||||
func (t SqlTime) String() string {
|
||||
if !t.Valid {
|
||||
return ""
|
||||
}
|
||||
return t.Val.Format("15:04:05")
|
||||
}
|
||||
|
||||
func (t SqlTime) MarshalYAML() (any, error) {
|
||||
if !t.Valid || t.Val.IsZero() {
|
||||
return nil, nil
|
||||
}
|
||||
s := t.Val.Format("15:04:05")
|
||||
if s == "00:00:00" {
|
||||
return nil, nil
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (t *SqlTime) UnmarshalYAML(value *yaml.Node) error {
|
||||
if err := t.SqlNull.UnmarshalYAML(value); err != nil {
|
||||
return err
|
||||
}
|
||||
if t.Valid && t.Val.Format("15:04:05") == "00:00:00" {
|
||||
t.Valid = false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t SqlTime) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
if !t.Valid || t.Val.IsZero() {
|
||||
return e.EncodeElement("", start)
|
||||
}
|
||||
s := t.Val.Format("15:04:05")
|
||||
if s == "00:00:00" {
|
||||
return e.EncodeElement("", start)
|
||||
}
|
||||
return e.EncodeElement(s, start)
|
||||
}
|
||||
|
||||
func (t *SqlTime) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
var s string
|
||||
if err := d.DecodeElement(&s, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == "" {
|
||||
t.Valid = false
|
||||
t.Val = time.Time{}
|
||||
return nil
|
||||
}
|
||||
tm, err := tryParseDT(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.Val = tm
|
||||
t.Valid = !tm.IsZero() && tm.Format("15:04:05") != "00:00:00"
|
||||
return nil
|
||||
}
|
||||
|
||||
func SqlTimeNow() SqlTime {
|
||||
return SqlTime{SqlNull: SqlNull[time.Time]{Val: time.Now(), Valid: true}}
|
||||
}
|
||||
|
||||
// SqlJSONB - Nullable JSONB as []byte.
|
||||
type SqlJSONB []byte
|
||||
|
||||
// SqlJSON - Nullable JSON as []byte. PostgreSQL's json and jsonb types share
|
||||
// the same textual representation and Go marshalling behavior, differing only
|
||||
// in server-side storage, so SqlJSON is an alias of SqlJSONB.
|
||||
type SqlJSON = SqlJSONB
|
||||
|
||||
// Scan implements sql.Scanner.
|
||||
func (n *SqlJSONB) Scan(value any) error {
|
||||
if value == nil {
|
||||
*n = nil
|
||||
return nil
|
||||
}
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
*n = []byte(v)
|
||||
case []byte:
|
||||
*n = v
|
||||
default:
|
||||
dat, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal value to JSON: %v", err)
|
||||
}
|
||||
*n = dat
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements driver.Valuer.
|
||||
func (n SqlJSONB) Value() (driver.Value, error) {
|
||||
if len(n) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var js any
|
||||
if err := json.Unmarshal(n, &js); err != nil {
|
||||
return nil, fmt.Errorf("invalid JSON: %v", err)
|
||||
}
|
||||
return string(n), nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (n SqlJSONB) MarshalJSON() ([]byte, error) {
|
||||
if len(n) == 0 {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
var obj any
|
||||
if err := json.Unmarshal(n, &obj); err != nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (n *SqlJSONB) UnmarshalJSON(b []byte) error {
|
||||
s := strings.TrimSpace(string(b))
|
||||
if s == "null" || s == "" || (!strings.HasPrefix(s, "{") && !strings.HasPrefix(s, "[")) {
|
||||
*n = nil
|
||||
return nil
|
||||
}
|
||||
*n = b
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalYAML implements yaml.Marshaler. The underlying JSON is decoded into
|
||||
// a generic value first so it renders as native YAML mappings/sequences
|
||||
// rather than an embedded JSON string.
|
||||
func (n SqlJSONB) MarshalYAML() (any, error) {
|
||||
if len(n) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var v any
|
||||
if err := json.Unmarshal(n, &v); err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshaler.
|
||||
func (n *SqlJSONB) UnmarshalYAML(value *yaml.Node) error {
|
||||
if value == nil || value.Tag == "!!null" {
|
||||
*n = nil
|
||||
return nil
|
||||
}
|
||||
var v any
|
||||
if err := value.Decode(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*n = b
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalXML implements xml.Marshaler. JSON has no clean structural mapping
|
||||
// to XML, so the raw JSON text is emitted as the element's text content.
|
||||
func (n SqlJSONB) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
if len(n) == 0 {
|
||||
return e.EncodeElement("", start)
|
||||
}
|
||||
var obj any
|
||||
if err := json.Unmarshal(n, &obj); err != nil {
|
||||
return e.EncodeElement("", start)
|
||||
}
|
||||
return e.EncodeElement(string(n), start)
|
||||
}
|
||||
|
||||
// UnmarshalXML implements xml.Unmarshaler, reading back the raw JSON text
|
||||
// written by MarshalXML.
|
||||
func (n *SqlJSONB) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
var s string
|
||||
if err := d.DecodeElement(&s, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
*n = nil
|
||||
return nil
|
||||
}
|
||||
*n = []byte(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n SqlJSONB) AsMap() (map[string]any, error) {
|
||||
if len(n) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
js := make(map[string]any)
|
||||
if err := json.Unmarshal(n, &js); err != nil {
|
||||
return nil, fmt.Errorf("invalid JSON: %v", err)
|
||||
}
|
||||
return js, nil
|
||||
}
|
||||
|
||||
func (n SqlJSONB) AsSlice() ([]any, error) {
|
||||
if len(n) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
js := make([]any, 0)
|
||||
if err := json.Unmarshal(n, &js); err != nil {
|
||||
return nil, fmt.Errorf("invalid JSON: %v", err)
|
||||
}
|
||||
return js, nil
|
||||
}
|
||||
|
||||
// TryIfInt64 tries to parse any value to int64 with default.
|
||||
func TryIfInt64(v any, def int64) int64 {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
i, err := strconv.ParseInt(val, 10, 64)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return i
|
||||
case int:
|
||||
return int64(val)
|
||||
case int8:
|
||||
return int64(val)
|
||||
case int16:
|
||||
return int64(val)
|
||||
case int32:
|
||||
return int64(val)
|
||||
case int64:
|
||||
return val
|
||||
case uint:
|
||||
return int64(val)
|
||||
case uint8:
|
||||
return int64(val)
|
||||
case uint16:
|
||||
return int64(val)
|
||||
case uint32:
|
||||
return int64(val)
|
||||
case uint64:
|
||||
return int64(val)
|
||||
case float32:
|
||||
return int64(val)
|
||||
case float64:
|
||||
return int64(val)
|
||||
case []byte:
|
||||
i, err := strconv.ParseInt(string(val), 10, 64)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return i
|
||||
default:
|
||||
return def
|
||||
}
|
||||
}
|
||||
|
||||
// Constructor helpers - clean and fast value creation
|
||||
func Null[T any](v T, valid bool) SqlNull[T] {
|
||||
return SqlNull[T]{Val: v, Valid: valid}
|
||||
}
|
||||
|
||||
func NewSql[T any](value any) SqlNull[T] {
|
||||
n := SqlNull[T]{}
|
||||
|
||||
if value == nil {
|
||||
return n
|
||||
}
|
||||
|
||||
// Fast path: exact match
|
||||
if v, ok := value.(T); ok {
|
||||
n.Val = v
|
||||
n.Valid = true
|
||||
return n
|
||||
}
|
||||
|
||||
// Try from another SqlNull
|
||||
if sn, ok := value.(SqlNull[T]); ok {
|
||||
return sn
|
||||
}
|
||||
|
||||
// Convert via string
|
||||
_ = n.FromString(fmt.Sprintf("%v", value))
|
||||
return n
|
||||
}
|
||||
|
||||
func NewSqlInt16(v int16) SqlInt16 {
|
||||
return SqlInt16{Val: v, Valid: true}
|
||||
}
|
||||
|
||||
func NewSqlInt32(v int32) SqlInt32 {
|
||||
return SqlInt32{Val: v, Valid: true}
|
||||
}
|
||||
|
||||
func NewSqlInt64(v int64) SqlInt64 {
|
||||
return SqlInt64{Val: v, Valid: true}
|
||||
}
|
||||
|
||||
func NewSqlFloat32(v float32) SqlFloat32 {
|
||||
return SqlFloat32{Val: v, Valid: true}
|
||||
}
|
||||
|
||||
func NewSqlFloat64(v float64) SqlFloat64 {
|
||||
return SqlFloat64{Val: v, Valid: true}
|
||||
}
|
||||
|
||||
func NewSqlBool(v bool) SqlBool {
|
||||
return SqlBool{Val: v, Valid: true}
|
||||
}
|
||||
|
||||
func NewSqlString(v string) SqlString {
|
||||
return SqlString{Val: v, Valid: true}
|
||||
}
|
||||
|
||||
func NewSqlByteArray(v []byte) SqlByteArray {
|
||||
return SqlByteArray{Val: v, Valid: true}
|
||||
}
|
||||
|
||||
func NewSqlUUID(v uuid.UUID) SqlUUID {
|
||||
return SqlUUID{Val: v, Valid: true}
|
||||
}
|
||||
|
||||
func NewSqlTimeStamp(v time.Time) SqlTimeStamp {
|
||||
return SqlTimeStamp{SqlNull: SqlNull[time.Time]{Val: v, Valid: true}}
|
||||
}
|
||||
|
||||
func NewSqlDate(v time.Time) SqlDate {
|
||||
return SqlDate{SqlNull: SqlNull[time.Time]{Val: v, Valid: true}}
|
||||
}
|
||||
|
||||
func NewSqlTime(v time.Time) SqlTime {
|
||||
return SqlTime{SqlNull: SqlNull[time.Time]{Val: v, Valid: true}}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package sqltypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestSqlNull_FromString_UnsignedInt guards against a regression where
|
||||
// FromString called reflect.Value.SetInt on unsigned-kind fields, which
|
||||
// panics since SetInt only accepts Int/Int8/.../Int64 kinds.
|
||||
func TestSqlNull_FromString_UnsignedInt(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected uint32
|
||||
}{
|
||||
{"simple", "123", 123},
|
||||
{"zero", "0", 0},
|
||||
{"large", "4000000000", 4000000000},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var n SqlNull[uint32]
|
||||
if err := n.FromString(tt.input); err != nil {
|
||||
t.Fatalf("FromString failed: %v", err)
|
||||
}
|
||||
if !n.Valid {
|
||||
t.Fatalf("expected valid=true")
|
||||
}
|
||||
if n.Val != tt.expected {
|
||||
t.Errorf("expected %d, got %d", tt.expected, n.Val)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSqlNull_FromString_Int64Precision guards against a regression where
|
||||
// FromString unconditionally re-parsed the string as float64 after a
|
||||
// successful ParseInt, corrupting large int64 values due to float rounding
|
||||
// (e.g. math.MaxInt64 became negative).
|
||||
func TestSqlNull_FromString_Int64Precision(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected int64
|
||||
}{
|
||||
{"max int64", "9223372036854775807", 9223372036854775807},
|
||||
{"min int64", "-9223372036854775808", -9223372036854775808},
|
||||
{"large safe value", "1234567890123456", 1234567890123456},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var n SqlNull[int64]
|
||||
if err := n.FromString(tt.input); err != nil {
|
||||
t.Fatalf("FromString failed: %v", err)
|
||||
}
|
||||
if !n.Valid {
|
||||
t.Fatalf("expected valid=true")
|
||||
}
|
||||
if n.Val != tt.expected {
|
||||
t.Errorf("expected %d, got %d", tt.expected, n.Val)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSqlNull_FromString_FloatFallback verifies that non-integral strings
|
||||
// still parse via the float fallback path for integer-kind fields.
|
||||
func TestSqlNull_FromString_FloatFallback(t *testing.T) {
|
||||
var n SqlNull[int32]
|
||||
if err := n.FromString("42.9"); err != nil {
|
||||
t.Fatalf("FromString failed: %v", err)
|
||||
}
|
||||
if !n.Valid || n.Val != 42 {
|
||||
t.Errorf("expected valid int32=42, got valid=%v val=%d", n.Valid, n.Val)
|
||||
}
|
||||
|
||||
var u SqlNull[uint32]
|
||||
if err := u.FromString("42.9"); err != nil {
|
||||
t.Fatalf("FromString failed: %v", err)
|
||||
}
|
||||
if !u.Valid || u.Val != 42 {
|
||||
t.Errorf("expected valid uint32=42, got valid=%v val=%d", u.Valid, u.Val)
|
||||
}
|
||||
|
||||
var neg SqlNull[uint32]
|
||||
if err := neg.FromString("-1.5"); err != nil {
|
||||
t.Fatalf("FromString failed: %v", err)
|
||||
}
|
||||
if neg.Valid {
|
||||
t.Errorf("expected invalid for negative value into unsigned type, got %v", neg.Val)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSqlNull_UnsignedTypes_ScanAndJSON exercises Scan and JSON round-trip
|
||||
// for every unsigned alias to make sure none of them panic.
|
||||
func TestSqlNull_UnsignedTypes_ScanAndJSON(t *testing.T) {
|
||||
t.Run("uint8 scan from string", func(t *testing.T) {
|
||||
var n SqlNull[uint8]
|
||||
if err := n.Scan("200"); err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
if !n.Valid || n.Val != 200 {
|
||||
t.Errorf("expected valid uint8=200, got valid=%v val=%d", n.Valid, n.Val)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("uint64 json round-trip", func(t *testing.T) {
|
||||
n := Null(uint64(18446744073709551615), true)
|
||||
data, err := json.Marshal(n)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var n2 SqlNull[uint64]
|
||||
if err := json.Unmarshal(data, &n2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if n2.Val != n.Val {
|
||||
t.Errorf("expected %d, got %d", n.Val, n2.Val)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("uint scan from numeric string fallback", func(t *testing.T) {
|
||||
var n SqlNull[uint]
|
||||
if err := n.Scan([]byte("77")); err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
if !n.Valid || n.Val != 77 {
|
||||
t.Errorf("expected valid uint=77, got valid=%v val=%d", n.Valid, n.Val)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,958 @@
|
||||
package sqltypes
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// TestNewSqlInt16 tests NewSqlInt16 type
|
||||
func TestNewSqlInt16(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input interface{}
|
||||
expected SqlInt16
|
||||
}{
|
||||
{"int", 42, Null(int16(42), true)},
|
||||
{"int32", int32(100), NewSqlInt16(100)},
|
||||
{"int64", int64(200), NewSqlInt16(200)},
|
||||
{"string", "123", NewSqlInt16(123)},
|
||||
{"nil", nil, Null(int16(0), false)},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var n SqlInt16
|
||||
if err := n.Scan(tt.input); err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
if n != tt.expected {
|
||||
t.Errorf("expected %v, got %v", tt.expected, n)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSqlInt16_Value(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input SqlInt16
|
||||
expected driver.Value
|
||||
}{
|
||||
{"zero", Null(int16(0), false), nil},
|
||||
{"positive", NewSqlInt16(42), int16(42)},
|
||||
{"negative", NewSqlInt16(-10), int16(-10)},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
val, err := tt.input.Value()
|
||||
if err != nil {
|
||||
t.Fatalf("Value failed: %v", err)
|
||||
}
|
||||
if val != tt.expected {
|
||||
t.Errorf("expected %v, got %v", tt.expected, val)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSqlInt16_JSON(t *testing.T) {
|
||||
n := NewSqlInt16(42)
|
||||
|
||||
// Marshal
|
||||
data, err := json.Marshal(n)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
expected := "42"
|
||||
if string(data) != expected {
|
||||
t.Errorf("expected %s, got %s", expected, string(data))
|
||||
}
|
||||
|
||||
// Unmarshal
|
||||
var n2 SqlInt16
|
||||
if err := json.Unmarshal([]byte("123"), &n2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if n2.Int64() != 123 {
|
||||
t.Errorf("expected 123, got %d", n2.Int64())
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewSqlInt64 tests NewSqlInt64 type
|
||||
func TestNewSqlInt64(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input interface{}
|
||||
expected SqlInt64
|
||||
}{
|
||||
{"int", 42, NewSqlInt64(42)},
|
||||
{"int32", int32(100), NewSqlInt64(100)},
|
||||
{"int64", int64(9223372036854775807), NewSqlInt64(9223372036854775807)},
|
||||
{"uint32", uint32(100), NewSqlInt64(100)},
|
||||
{"uint64", uint64(200), NewSqlInt64(200)},
|
||||
{"nil", nil, SqlInt64{}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var n SqlInt64
|
||||
if err := n.Scan(tt.input); err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
if n != tt.expected {
|
||||
t.Errorf("expected %v, got %v", tt.expected, n)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSqlFloat64 tests SqlFloat64 type
|
||||
func TestSqlFloat64(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input interface{}
|
||||
expected float64
|
||||
valid bool
|
||||
}{
|
||||
{"float64", float64(3.14), 3.14, true},
|
||||
{"float32", float32(2.5), 2.5, true},
|
||||
{"int", 42, 42.0, true},
|
||||
{"int64", int64(100), 100.0, true},
|
||||
{"nil", nil, 0, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var n SqlFloat64
|
||||
if err := n.Scan(tt.input); err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
if n.Valid != tt.valid {
|
||||
t.Errorf("expected valid=%v, got valid=%v", tt.valid, n.Valid)
|
||||
}
|
||||
if tt.valid && n.Float64() != tt.expected {
|
||||
t.Errorf("expected %v, got %v", tt.expected, n.Float64())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSqlTimeStamp tests SqlTimeStamp type
|
||||
func TestSqlTimeStamp(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input interface{}
|
||||
}{
|
||||
{"time.Time", now},
|
||||
{"string RFC3339", now.Format(time.RFC3339)},
|
||||
{"string date", "2024-01-15"},
|
||||
{"string datetime", "2024-01-15T10:30:00"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var ts SqlTimeStamp
|
||||
if err := ts.Scan(tt.input); err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
if ts.Time().IsZero() {
|
||||
t.Error("expected non-zero time")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlTimeStamp_JSON(t *testing.T) {
|
||||
now := time.Date(2024, 1, 15, 10, 30, 45, 0, time.UTC)
|
||||
ts := NewSqlTimeStamp(now)
|
||||
|
||||
// Marshal
|
||||
data, err := json.Marshal(ts)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
expected := `"2024-01-15T10:30:45"`
|
||||
if string(data) != expected {
|
||||
t.Errorf("expected %s, got %s", expected, string(data))
|
||||
}
|
||||
|
||||
// Unmarshal
|
||||
var ts2 SqlTimeStamp
|
||||
if err := json.Unmarshal([]byte(`"2024-01-15T10:30:45"`), &ts2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if ts2.Time().Year() != 2024 {
|
||||
t.Errorf("expected year 2024, got %d", ts2.Time().Year())
|
||||
}
|
||||
|
||||
// Test null
|
||||
var ts3 SqlTimeStamp
|
||||
if err := json.Unmarshal([]byte("null"), &ts3); err != nil {
|
||||
t.Fatalf("Unmarshal null failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSqlDate tests SqlDate type
|
||||
func TestSqlDate(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input interface{}
|
||||
}{
|
||||
{"time.Time", now},
|
||||
{"string date", "2024-01-15"},
|
||||
{"string UK format", "15/01/2024"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var d SqlDate
|
||||
if err := d.Scan(tt.input); err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
if d.String() == "0" {
|
||||
t.Error("expected non-zero date")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlDate_JSON(t *testing.T) {
|
||||
date := NewSqlDate(time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC))
|
||||
|
||||
// Marshal
|
||||
data, err := json.Marshal(date)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
expected := `"2024-01-15"`
|
||||
if string(data) != expected {
|
||||
t.Errorf("expected %s, got %s", expected, string(data))
|
||||
}
|
||||
|
||||
// Unmarshal
|
||||
var d2 SqlDate
|
||||
if err := json.Unmarshal([]byte(`"2024-01-15"`), &d2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSqlTime tests SqlTime type
|
||||
func TestSqlTime(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input interface{}
|
||||
expected string
|
||||
}{
|
||||
{"time.Time", now, now.Format("15:04:05")},
|
||||
{"string time", "10:30:45", "10:30:45"},
|
||||
{"string short time", "10:30", "10:30:00"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var tm SqlTime
|
||||
if err := tm.Scan(tt.input); err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
if tm.String() != tt.expected {
|
||||
t.Errorf("expected %s, got %s", tt.expected, tm.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSqlJSONB tests SqlJSONB type
|
||||
func TestSqlJSONB_Scan(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input interface{}
|
||||
expected string
|
||||
}{
|
||||
{"string JSON object", `{"key":"value"}`, `{"key":"value"}`},
|
||||
{"string JSON array", `[1,2,3]`, `[1,2,3]`},
|
||||
{"bytes", []byte(`{"test":true}`), `{"test":true}`},
|
||||
{"nil", nil, ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var j SqlJSONB
|
||||
if err := j.Scan(tt.input); err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
if tt.expected == "" && j == nil {
|
||||
return // nil case
|
||||
}
|
||||
if string(j) != tt.expected {
|
||||
t.Errorf("expected %s, got %s", tt.expected, string(j))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlJSONB_Value(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input SqlJSONB
|
||||
expected string
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid object", SqlJSONB(`{"key":"value"}`), `{"key":"value"}`, false},
|
||||
{"valid array", SqlJSONB(`[1,2,3]`), `[1,2,3]`, false},
|
||||
{"empty", SqlJSONB{}, "", false},
|
||||
{"nil", nil, "", false},
|
||||
{"invalid JSON", SqlJSONB(`{invalid`), "", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
val, err := tt.input.Value()
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("Value failed: %v", err)
|
||||
}
|
||||
if tt.expected == "" && val == nil {
|
||||
return // nil case
|
||||
}
|
||||
if val.(string) != tt.expected {
|
||||
t.Errorf("expected %s, got %s", tt.expected, val)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlJSONB_JSON(t *testing.T) {
|
||||
// Marshal
|
||||
j := SqlJSONB(`{"name":"test","count":42}`)
|
||||
data, err := json.Marshal(j)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
t.Fatalf("Unmarshal result failed: %v", err)
|
||||
}
|
||||
if result["name"] != "test" {
|
||||
t.Errorf("expected name=test, got %v", result["name"])
|
||||
}
|
||||
|
||||
// Unmarshal
|
||||
var j2 SqlJSONB
|
||||
if err := json.Unmarshal([]byte(`{"key":"value"}`), &j2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if string(j2) != `{"key":"value"}` {
|
||||
t.Errorf("expected {\"key\":\"value\"}, got %s", string(j2))
|
||||
}
|
||||
|
||||
// Test null
|
||||
var j3 SqlJSONB
|
||||
if err := json.Unmarshal([]byte("null"), &j3); err != nil {
|
||||
t.Fatalf("Unmarshal null failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlJSONB_AsMap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input SqlJSONB
|
||||
wantErr bool
|
||||
wantNil bool
|
||||
}{
|
||||
{"valid object", SqlJSONB(`{"name":"test","age":30}`), false, false},
|
||||
{"empty", SqlJSONB{}, false, true},
|
||||
{"nil", nil, false, true},
|
||||
{"invalid JSON", SqlJSONB(`{invalid`), true, false},
|
||||
{"array not object", SqlJSONB(`[1,2,3]`), true, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m, err := tt.input.AsMap()
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("AsMap failed: %v", err)
|
||||
}
|
||||
if tt.wantNil {
|
||||
if m != nil {
|
||||
t.Errorf("expected nil, got %v", m)
|
||||
}
|
||||
return
|
||||
}
|
||||
if m == nil {
|
||||
t.Error("expected non-nil map")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlJSONB_AsSlice(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input SqlJSONB
|
||||
wantErr bool
|
||||
wantNil bool
|
||||
}{
|
||||
{"valid array", SqlJSONB(`[1,2,3]`), false, false},
|
||||
{"empty", SqlJSONB{}, false, true},
|
||||
{"nil", nil, false, true},
|
||||
{"invalid JSON", SqlJSONB(`[invalid`), true, false},
|
||||
{"object not array", SqlJSONB(`{"key":"value"}`), true, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s, err := tt.input.AsSlice()
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("AsSlice failed: %v", err)
|
||||
}
|
||||
if tt.wantNil {
|
||||
if s != nil {
|
||||
t.Errorf("expected nil, got %v", s)
|
||||
}
|
||||
return
|
||||
}
|
||||
if s == nil {
|
||||
t.Error("expected non-nil slice")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSqlUUID tests SqlUUID type
|
||||
func TestSqlUUID_Scan(t *testing.T) {
|
||||
testUUID := uuid.New()
|
||||
testUUIDStr := testUUID.String()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input interface{}
|
||||
expected string
|
||||
valid bool
|
||||
}{
|
||||
{"string UUID", testUUIDStr, testUUIDStr, true},
|
||||
{"bytes UUID", []byte(testUUIDStr), testUUIDStr, true},
|
||||
{"nil", nil, "", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var u SqlUUID
|
||||
if err := u.Scan(tt.input); err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
if u.Valid != tt.valid {
|
||||
t.Errorf("expected valid=%v, got valid=%v", tt.valid, u.Valid)
|
||||
}
|
||||
if tt.valid && u.String() != tt.expected {
|
||||
t.Errorf("expected %s, got %s", tt.expected, u.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlUUID_Value(t *testing.T) {
|
||||
testUUID := uuid.New()
|
||||
u := NewSqlUUID(testUUID)
|
||||
|
||||
val, err := u.Value()
|
||||
if err != nil {
|
||||
t.Fatalf("Value failed: %v", err)
|
||||
}
|
||||
// Value() should return a string for driver compatibility
|
||||
if val != testUUID.String() {
|
||||
t.Errorf("expected %s, got %s", testUUID.String(), val)
|
||||
}
|
||||
|
||||
// Test invalid UUID
|
||||
u2 := SqlUUID{Valid: false}
|
||||
val2, err := u2.Value()
|
||||
if err != nil {
|
||||
t.Fatalf("Value failed: %v", err)
|
||||
}
|
||||
if val2 != nil {
|
||||
t.Errorf("expected nil, got %v", val2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlUUID_JSON(t *testing.T) {
|
||||
testUUID := uuid.New()
|
||||
u := NewSqlUUID(testUUID)
|
||||
|
||||
// Marshal
|
||||
data, err := json.Marshal(u)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
expected := `"` + testUUID.String() + `"`
|
||||
if string(data) != expected {
|
||||
t.Errorf("expected %s, got %s", expected, string(data))
|
||||
}
|
||||
|
||||
// Unmarshal
|
||||
var u2 SqlUUID
|
||||
if err := json.Unmarshal([]byte(`"`+testUUID.String()+`"`), &u2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if u2.String() != testUUID.String() {
|
||||
t.Errorf("expected %s, got %s", testUUID.String(), u2.String())
|
||||
}
|
||||
|
||||
// Test null
|
||||
var u3 SqlUUID
|
||||
if err := json.Unmarshal([]byte("null"), &u3); err != nil {
|
||||
t.Fatalf("Unmarshal null failed: %v", err)
|
||||
}
|
||||
if u3.Valid {
|
||||
t.Error("expected invalid UUID")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTryIfInt64 tests the TryIfInt64 helper function
|
||||
func TestTryIfInt64(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input interface{}
|
||||
def int64
|
||||
expected int64
|
||||
}{
|
||||
{"string valid", "123", 0, 123},
|
||||
{"string invalid", "abc", 99, 99},
|
||||
{"int", 42, 0, 42},
|
||||
{"int32", int32(100), 0, 100},
|
||||
{"int64", int64(200), 0, 200},
|
||||
{"uint32", uint32(50), 0, 50},
|
||||
{"uint64", uint64(75), 0, 75},
|
||||
{"float32", float32(3.14), 0, 3},
|
||||
{"float64", float64(2.71), 0, 2},
|
||||
{"bytes", []byte("456"), 0, 456},
|
||||
{"unknown type", struct{}{}, 999, 999},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := TryIfInt64(tt.input, tt.def)
|
||||
if result != tt.expected {
|
||||
t.Errorf("expected %d, got %d", tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSqlString tests SqlString without base64 (plain text)
|
||||
func TestSqlString_Scan(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input interface{}
|
||||
expected string
|
||||
valid bool
|
||||
}{
|
||||
{
|
||||
name: "plain string",
|
||||
input: "hello world",
|
||||
expected: "hello world",
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "plain text",
|
||||
input: "plain text",
|
||||
expected: "plain text",
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "bytes as string",
|
||||
input: []byte("raw bytes"),
|
||||
expected: "raw bytes",
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "nil value",
|
||||
input: nil,
|
||||
expected: "",
|
||||
valid: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var s SqlString
|
||||
if err := s.Scan(tt.input); err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
if s.Valid != tt.valid {
|
||||
t.Errorf("expected valid=%v, got valid=%v", tt.valid, s.Valid)
|
||||
}
|
||||
if tt.valid && s.String() != tt.expected {
|
||||
t.Errorf("expected %q, got %q", tt.expected, s.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlString_JSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
inputValue string
|
||||
expectedJSON string
|
||||
expectedDecode string
|
||||
}{
|
||||
{
|
||||
name: "simple string",
|
||||
inputValue: "hello world",
|
||||
expectedJSON: `"hello world"`, // plain text, not base64
|
||||
expectedDecode: "hello world",
|
||||
},
|
||||
{
|
||||
name: "special characters",
|
||||
inputValue: "test@#$%",
|
||||
expectedJSON: `"test@#$%"`, // plain text, not base64
|
||||
expectedDecode: "test@#$%",
|
||||
},
|
||||
{
|
||||
name: "unicode string",
|
||||
inputValue: "Hello 世界",
|
||||
expectedJSON: `"Hello 世界"`, // plain text, not base64
|
||||
expectedDecode: "Hello 世界",
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
inputValue: "",
|
||||
expectedJSON: `""`,
|
||||
expectedDecode: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Test MarshalJSON
|
||||
s := NewSqlString(tt.inputValue)
|
||||
data, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
if string(data) != tt.expectedJSON {
|
||||
t.Errorf("Marshal: expected %s, got %s", tt.expectedJSON, string(data))
|
||||
}
|
||||
|
||||
// Test UnmarshalJSON
|
||||
var s2 SqlString
|
||||
if err := json.Unmarshal(data, &s2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if !s2.Valid {
|
||||
t.Error("expected valid=true after unmarshal")
|
||||
}
|
||||
if s2.String() != tt.expectedDecode {
|
||||
t.Errorf("Unmarshal: expected %q, got %q", tt.expectedDecode, s2.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlString_JSON_Null(t *testing.T) {
|
||||
// Test null handling
|
||||
var s SqlString
|
||||
if err := json.Unmarshal([]byte("null"), &s); err != nil {
|
||||
t.Fatalf("Unmarshal null failed: %v", err)
|
||||
}
|
||||
if s.Valid {
|
||||
t.Error("expected invalid after unmarshaling null")
|
||||
}
|
||||
|
||||
// Test marshal null
|
||||
data, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
if string(data) != "null" {
|
||||
t.Errorf("expected null, got %s", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
// TestSqlByteArray_Base64 tests SqlByteArray with base64 encoding/decoding
|
||||
func TestSqlByteArray_Base64_Scan(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input interface{}
|
||||
expected []byte
|
||||
valid bool
|
||||
}{
|
||||
{
|
||||
name: "base64 encoded bytes from SQL",
|
||||
input: "aGVsbG8gd29ybGQ=", // "hello world" in base64
|
||||
expected: []byte("hello world"),
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "plain bytes fallback",
|
||||
input: "plain text",
|
||||
expected: []byte("plain text"),
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "bytes base64 encoded",
|
||||
input: []byte("SGVsbG8gR29waGVy"), // "Hello Gopher" in base64
|
||||
expected: []byte("Hello Gopher"),
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "bytes plain fallback",
|
||||
input: []byte("raw bytes"),
|
||||
expected: []byte("raw bytes"),
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "binary data",
|
||||
input: "AQIDBA==", // []byte{1, 2, 3, 4} in base64
|
||||
expected: []byte{1, 2, 3, 4},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "nil value",
|
||||
input: nil,
|
||||
expected: nil,
|
||||
valid: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var b SqlByteArray
|
||||
if err := b.Scan(tt.input); err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
if b.Valid != tt.valid {
|
||||
t.Errorf("expected valid=%v, got valid=%v", tt.valid, b.Valid)
|
||||
}
|
||||
if tt.valid {
|
||||
if string(b.Val) != string(tt.expected) {
|
||||
t.Errorf("expected %q, got %q", tt.expected, b.Val)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlByteArray_Base64_JSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
inputValue []byte
|
||||
expectedJSON string
|
||||
expectedDecode []byte
|
||||
}{
|
||||
{
|
||||
name: "text bytes",
|
||||
inputValue: []byte("hello world"),
|
||||
expectedJSON: `"aGVsbG8gd29ybGQ="`, // base64 encoded
|
||||
expectedDecode: []byte("hello world"),
|
||||
},
|
||||
{
|
||||
name: "binary data",
|
||||
inputValue: []byte{0x01, 0x02, 0x03, 0x04, 0xFF},
|
||||
expectedJSON: `"AQIDBP8="`, // base64 encoded
|
||||
expectedDecode: []byte{0x01, 0x02, 0x03, 0x04, 0xFF},
|
||||
},
|
||||
{
|
||||
name: "empty bytes",
|
||||
inputValue: []byte{},
|
||||
expectedJSON: `""`, // base64 of empty bytes
|
||||
expectedDecode: []byte{},
|
||||
},
|
||||
{
|
||||
name: "unicode bytes",
|
||||
inputValue: []byte("Hello 世界"),
|
||||
expectedJSON: `"SGVsbG8g5LiW55WM"`, // base64 encoded
|
||||
expectedDecode: []byte("Hello 世界"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Test MarshalJSON
|
||||
b := NewSqlByteArray(tt.inputValue)
|
||||
data, err := json.Marshal(b)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
if string(data) != tt.expectedJSON {
|
||||
t.Errorf("Marshal: expected %s, got %s", tt.expectedJSON, string(data))
|
||||
}
|
||||
|
||||
// Test UnmarshalJSON
|
||||
var b2 SqlByteArray
|
||||
if err := json.Unmarshal(data, &b2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if !b2.Valid {
|
||||
t.Error("expected valid=true after unmarshal")
|
||||
}
|
||||
if string(b2.Val) != string(tt.expectedDecode) {
|
||||
t.Errorf("Unmarshal: expected %v, got %v", tt.expectedDecode, b2.Val)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlByteArray_Base64_JSON_Null(t *testing.T) {
|
||||
// Test null handling
|
||||
var b SqlByteArray
|
||||
if err := json.Unmarshal([]byte("null"), &b); err != nil {
|
||||
t.Fatalf("Unmarshal null failed: %v", err)
|
||||
}
|
||||
if b.Valid {
|
||||
t.Error("expected invalid after unmarshaling null")
|
||||
}
|
||||
|
||||
// Test marshal null
|
||||
data, err := json.Marshal(b)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
if string(data) != "null" {
|
||||
t.Errorf("expected null, got %s", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlByteArray_Value(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input SqlByteArray
|
||||
expected interface{}
|
||||
}{
|
||||
{
|
||||
name: "valid bytes",
|
||||
input: NewSqlByteArray([]byte("test data")),
|
||||
expected: []byte("test data"),
|
||||
},
|
||||
{
|
||||
name: "empty bytes",
|
||||
input: NewSqlByteArray([]byte{}),
|
||||
expected: []byte{},
|
||||
},
|
||||
{
|
||||
name: "invalid",
|
||||
input: SqlByteArray{Valid: false},
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
val, err := tt.input.Value()
|
||||
if err != nil {
|
||||
t.Fatalf("Value failed: %v", err)
|
||||
}
|
||||
if tt.expected == nil && val != nil {
|
||||
t.Errorf("expected nil, got %v", val)
|
||||
}
|
||||
if tt.expected != nil && val == nil {
|
||||
t.Errorf("expected %v, got nil", tt.expected)
|
||||
}
|
||||
if tt.expected != nil && val != nil {
|
||||
if string(val.([]byte)) != string(tt.expected.([]byte)) {
|
||||
t.Errorf("expected %v, got %v", tt.expected, val)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSqlString_RoundTrip tests complete round-trip: Go -> JSON -> Go -> SQL -> Go
|
||||
func TestSqlString_RoundTrip(t *testing.T) {
|
||||
original := "Test String with Special Chars: @#$%^&*()"
|
||||
|
||||
// Go -> JSON
|
||||
s1 := NewSqlString(original)
|
||||
jsonData, err := json.Marshal(s1)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
|
||||
// JSON -> Go
|
||||
var s2 SqlString
|
||||
if err := json.Unmarshal(jsonData, &s2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
// Go -> SQL (Value)
|
||||
_, err = s2.Value()
|
||||
if err != nil {
|
||||
t.Fatalf("Value failed: %v", err)
|
||||
}
|
||||
|
||||
// SQL -> Go (Scan plain text)
|
||||
var s3 SqlString
|
||||
// Simulate SQL driver returning plain text value
|
||||
if err := s3.Scan(original); err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify round-trip
|
||||
if s3.String() != original {
|
||||
t.Errorf("Round-trip failed: expected %q, got %q", original, s3.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestSqlByteArray_Base64_RoundTrip tests complete round-trip: Go -> JSON -> Go -> SQL -> Go
|
||||
func TestSqlByteArray_Base64_RoundTrip(t *testing.T) {
|
||||
original := []byte{0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0xFF, 0xFE} // "Hello " + binary data
|
||||
|
||||
// Go -> JSON
|
||||
b1 := NewSqlByteArray(original)
|
||||
jsonData, err := json.Marshal(b1)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
|
||||
// JSON -> Go
|
||||
var b2 SqlByteArray
|
||||
if err := json.Unmarshal(jsonData, &b2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
// Go -> SQL (Value)
|
||||
_, err = b2.Value()
|
||||
if err != nil {
|
||||
t.Fatalf("Value failed: %v", err)
|
||||
}
|
||||
|
||||
// SQL -> Go (Scan with base64)
|
||||
var b3 SqlByteArray
|
||||
// Simulate SQL driver returning base64 encoded value
|
||||
if err := b3.Scan("SGVsbG8g//4="); err != nil {
|
||||
t.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify round-trip
|
||||
if string(b3.Val) != string(original) {
|
||||
t.Errorf("Round-trip failed: expected %v, got %v", original, b3.Val)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,678 @@
|
||||
package sqltypes
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// ── SqlNull: YAML ────────────────────────────────────────────────────────────
|
||||
|
||||
func TestSqlNull_YAML_Int(t *testing.T) {
|
||||
n := NewSqlInt32(42)
|
||||
data, err := yaml.Marshal(n)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
if string(data) != "42\n" {
|
||||
t.Errorf("expected \"42\\n\", got %q", string(data))
|
||||
}
|
||||
|
||||
var n2 SqlInt32
|
||||
if err := yaml.Unmarshal(data, &n2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if n2.Int64() != 42 {
|
||||
t.Errorf("expected 42, got %d", n2.Int64())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlNull_YAML_Null(t *testing.T) {
|
||||
var n SqlInt32
|
||||
data, err := yaml.Marshal(n)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
if string(data) != "null\n" {
|
||||
t.Errorf("expected \"null\\n\", got %q", string(data))
|
||||
}
|
||||
|
||||
var n2 SqlInt32
|
||||
if err := yaml.Unmarshal([]byte("null\n"), &n2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if n2.Valid {
|
||||
t.Error("expected invalid after unmarshaling null")
|
||||
}
|
||||
|
||||
// ~ is also a YAML null.
|
||||
var n3 SqlInt32
|
||||
if err := yaml.Unmarshal([]byte("~\n"), &n3); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if n3.Valid {
|
||||
t.Error("expected invalid after unmarshaling ~")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlNull_YAML_String(t *testing.T) {
|
||||
s := NewSqlString("hello world")
|
||||
data, err := yaml.Marshal(s)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var s2 SqlString
|
||||
if err := yaml.Unmarshal(data, &s2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if s2.String() != "hello world" {
|
||||
t.Errorf("expected %q, got %q", "hello world", s2.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlNull_YAML_UUID(t *testing.T) {
|
||||
id := uuid.New()
|
||||
u := NewSqlUUID(id)
|
||||
data, err := yaml.Marshal(u)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
if string(data) != id.String()+"\n" {
|
||||
t.Errorf("expected %q, got %q", id.String()+"\n", string(data))
|
||||
}
|
||||
var u2 SqlUUID
|
||||
if err := yaml.Unmarshal(data, &u2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if u2.UUID() != id {
|
||||
t.Errorf("expected %v, got %v", id, u2.UUID())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlNull_YAML_ByteArray_Base64(t *testing.T) {
|
||||
orig := []byte{0x01, 0x02, 0xFF}
|
||||
b := NewSqlByteArray(orig)
|
||||
data, err := yaml.Marshal(b)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var b2 SqlByteArray
|
||||
if err := yaml.Unmarshal(data, &b2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if string(b2.Val) != string(orig) {
|
||||
t.Errorf("expected %v, got %v", orig, b2.Val)
|
||||
}
|
||||
}
|
||||
|
||||
// ── SqlNull: XML ─────────────────────────────────────────────────────────────
|
||||
|
||||
type xmlIntWrapper struct {
|
||||
XMLName xml.Name `xml:"root"`
|
||||
Value SqlInt32 `xml:"value"`
|
||||
}
|
||||
|
||||
func TestSqlNull_XML_Int(t *testing.T) {
|
||||
w := xmlIntWrapper{Value: NewSqlInt32(99)}
|
||||
data, err := xml.Marshal(w)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var w2 xmlIntWrapper
|
||||
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if w2.Value.Int64() != 99 {
|
||||
t.Errorf("expected 99, got %d", w2.Value.Int64())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlNull_XML_Null(t *testing.T) {
|
||||
w := xmlIntWrapper{}
|
||||
data, err := xml.Marshal(w)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var w2 xmlIntWrapper
|
||||
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if w2.Value.Valid {
|
||||
t.Errorf("expected invalid, got %v", w2.Value)
|
||||
}
|
||||
}
|
||||
|
||||
type xmlUUIDWrapper struct {
|
||||
XMLName xml.Name `xml:"root"`
|
||||
ID SqlUUID `xml:"id"`
|
||||
}
|
||||
|
||||
func TestSqlNull_XML_UUID(t *testing.T) {
|
||||
id := uuid.New()
|
||||
w := xmlUUIDWrapper{ID: NewSqlUUID(id)}
|
||||
data, err := xml.Marshal(w)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var w2 xmlUUIDWrapper
|
||||
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if w2.ID.UUID() != id {
|
||||
t.Errorf("expected %v, got %v", id, w2.ID.UUID())
|
||||
}
|
||||
}
|
||||
|
||||
type xmlByteWrapper struct {
|
||||
XMLName xml.Name `xml:"root"`
|
||||
Data SqlByteArray `xml:"data"`
|
||||
}
|
||||
|
||||
func TestSqlNull_XML_ByteArray_Base64(t *testing.T) {
|
||||
orig := []byte{0xDE, 0xAD, 0xBE, 0xEF}
|
||||
w := xmlByteWrapper{Data: NewSqlByteArray(orig)}
|
||||
data, err := xml.Marshal(w)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var w2 xmlByteWrapper
|
||||
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if string(w2.Data.Val) != string(orig) {
|
||||
t.Errorf("expected %v, got %v", orig, w2.Data.Val)
|
||||
}
|
||||
}
|
||||
|
||||
// ── SqlTimeStamp / SqlDate / SqlTime: YAML + XML ────────────────────────────
|
||||
|
||||
func TestSqlTimeStamp_YAML(t *testing.T) {
|
||||
ts := NewSqlTimeStamp(time.Date(2024, 6, 15, 9, 30, 0, 0, time.UTC))
|
||||
data, err := yaml.Marshal(ts)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
if string(data) != "2024-06-15T09:30:00\n" {
|
||||
t.Errorf("unexpected YAML: %q", string(data))
|
||||
}
|
||||
var ts2 SqlTimeStamp
|
||||
if err := yaml.Unmarshal(data, &ts2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if ts2.Time().Format("2006-01-02T15:04:05") != "2024-06-15T09:30:00" {
|
||||
t.Errorf("expected 2024-06-15T09:30:00, got %v", ts2.Time())
|
||||
}
|
||||
|
||||
var zero SqlTimeStamp
|
||||
zdata, err := yaml.Marshal(zero)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
if string(zdata) != "null\n" {
|
||||
t.Errorf("expected null, got %q", zdata)
|
||||
}
|
||||
}
|
||||
|
||||
type xmlTimeStampWrapper struct {
|
||||
XMLName xml.Name `xml:"root"`
|
||||
At SqlTimeStamp `xml:"at"`
|
||||
}
|
||||
|
||||
func TestSqlTimeStamp_XML(t *testing.T) {
|
||||
w := xmlTimeStampWrapper{At: NewSqlTimeStamp(time.Date(2024, 6, 15, 9, 30, 0, 0, time.UTC))}
|
||||
data, err := xml.Marshal(w)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var w2 xmlTimeStampWrapper
|
||||
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if w2.At.Time().Format("2006-01-02T15:04:05") != "2024-06-15T09:30:00" {
|
||||
t.Errorf("expected 2024-06-15T09:30:00, got %v", w2.At.Time())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlDate_YAML(t *testing.T) {
|
||||
d := NewSqlDate(time.Date(2024, 6, 15, 0, 0, 0, 0, time.UTC))
|
||||
data, err := yaml.Marshal(d)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
if string(data) != "\"2024-06-15\"\n" {
|
||||
t.Errorf("unexpected YAML: %q", string(data))
|
||||
}
|
||||
var d2 SqlDate
|
||||
if err := yaml.Unmarshal(data, &d2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if d2.String() != "2024-06-15" {
|
||||
t.Errorf("expected 2024-06-15, got %q", d2.String())
|
||||
}
|
||||
}
|
||||
|
||||
type xmlDateWrapper struct {
|
||||
XMLName xml.Name `xml:"root"`
|
||||
Day SqlDate `xml:"day"`
|
||||
}
|
||||
|
||||
func TestSqlDate_XML(t *testing.T) {
|
||||
w := xmlDateWrapper{Day: NewSqlDate(time.Date(2024, 6, 15, 0, 0, 0, 0, time.UTC))}
|
||||
data, err := xml.Marshal(w)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var w2 xmlDateWrapper
|
||||
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if w2.Day.String() != "2024-06-15" {
|
||||
t.Errorf("expected 2024-06-15, got %q", w2.Day.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlTime_YAML(t *testing.T) {
|
||||
tm := NewSqlTime(time.Date(0, 1, 1, 14, 5, 9, 0, time.UTC))
|
||||
data, err := yaml.Marshal(tm)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
if string(data) != "\"14:05:09\"\n" {
|
||||
t.Errorf("unexpected YAML: %q", string(data))
|
||||
}
|
||||
var tm2 SqlTime
|
||||
if err := yaml.Unmarshal(data, &tm2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if tm2.String() != "14:05:09" {
|
||||
t.Errorf("expected 14:05:09, got %q", tm2.String())
|
||||
}
|
||||
}
|
||||
|
||||
type xmlTimeWrapper struct {
|
||||
XMLName xml.Name `xml:"root"`
|
||||
At SqlTime `xml:"at"`
|
||||
}
|
||||
|
||||
func TestSqlTime_XML(t *testing.T) {
|
||||
w := xmlTimeWrapper{At: NewSqlTime(time.Date(0, 1, 1, 14, 5, 9, 0, time.UTC))}
|
||||
data, err := xml.Marshal(w)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var w2 xmlTimeWrapper
|
||||
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if w2.At.String() != "14:05:09" {
|
||||
t.Errorf("expected 14:05:09, got %q", w2.At.String())
|
||||
}
|
||||
}
|
||||
|
||||
// ── SqlJSONB: YAML + XML ─────────────────────────────────────────────────────
|
||||
|
||||
func TestSqlJSONB_YAML(t *testing.T) {
|
||||
j := SqlJSONB(`{"name":"test","count":42}`)
|
||||
data, err := yaml.Marshal(j)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
|
||||
var decoded map[string]any
|
||||
if err := yaml.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatalf("failed decoding produced YAML: %v", err)
|
||||
}
|
||||
if decoded["name"] != "test" {
|
||||
t.Errorf("expected name=test, got %v", decoded["name"])
|
||||
}
|
||||
|
||||
var j2 SqlJSONB
|
||||
if err := yaml.Unmarshal(data, &j2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
m, err := j2.AsMap()
|
||||
if err != nil {
|
||||
t.Fatalf("AsMap failed: %v", err)
|
||||
}
|
||||
if m["name"] != "test" {
|
||||
t.Errorf("expected name=test, got %v", m["name"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlJSONB_YAML_Null(t *testing.T) {
|
||||
var j SqlJSONB
|
||||
data, err := yaml.Marshal(j)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
if string(data) != "null\n" {
|
||||
t.Errorf("expected null, got %q", data)
|
||||
}
|
||||
var j2 SqlJSONB
|
||||
if err := yaml.Unmarshal([]byte("null\n"), &j2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if j2 != nil {
|
||||
t.Errorf("expected nil, got %v", j2)
|
||||
}
|
||||
}
|
||||
|
||||
type xmlJSONBWrapper struct {
|
||||
XMLName xml.Name `xml:"root"`
|
||||
Meta SqlJSONB `xml:"meta"`
|
||||
}
|
||||
|
||||
func TestSqlJSONB_XML(t *testing.T) {
|
||||
w := xmlJSONBWrapper{Meta: SqlJSONB(`{"key":"value"}`)}
|
||||
data, err := xml.Marshal(w)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var w2 xmlJSONBWrapper
|
||||
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
m, err := w2.Meta.AsMap()
|
||||
if err != nil {
|
||||
t.Fatalf("AsMap failed: %v", err)
|
||||
}
|
||||
if m["key"] != "value" {
|
||||
t.Errorf("expected key=value, got %v", m)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Array types: YAML ────────────────────────────────────────────────────────
|
||||
|
||||
func TestSqlStringArray_YAML(t *testing.T) {
|
||||
a := NewSqlStringArray([]string{"a", "b", "c"})
|
||||
data, err := yaml.Marshal(a)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var a2 SqlStringArray
|
||||
if err := yaml.Unmarshal(data, &a2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if len(a2.Val) != 3 || a2.Val[1] != "b" {
|
||||
t.Errorf("unexpected value %v", a2.Val)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlStringArray_YAML_Null(t *testing.T) {
|
||||
var a SqlStringArray
|
||||
data, err := yaml.Marshal(a)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
if string(data) != "null\n" {
|
||||
t.Errorf("expected null, got %q", data)
|
||||
}
|
||||
var a2 SqlStringArray
|
||||
if err := yaml.Unmarshal([]byte("null\n"), &a2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if a2.Valid {
|
||||
t.Error("expected invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlInt32Array_YAML(t *testing.T) {
|
||||
a := NewSqlInt32Array([]int32{1, 2, 3})
|
||||
data, err := yaml.Marshal(a)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var a2 SqlInt32Array
|
||||
if err := yaml.Unmarshal(data, &a2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
for i, v := range a.Val {
|
||||
if a2.Val[i] != v {
|
||||
t.Errorf("index %d: expected %d, got %d", i, v, a2.Val[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlUUIDArray_YAML(t *testing.T) {
|
||||
ids := []uuid.UUID{uuid.New(), uuid.New()}
|
||||
a := NewSqlUUIDArray(ids)
|
||||
data, err := yaml.Marshal(a)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var a2 SqlUUIDArray
|
||||
if err := yaml.Unmarshal(data, &a2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
for i, v := range ids {
|
||||
if a2.Val[i] != v {
|
||||
t.Errorf("index %d: expected %v, got %v", i, v, a2.Val[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlVector_YAML(t *testing.T) {
|
||||
v := NewSqlVector([]float32{0.1, 0.2, 0.3})
|
||||
data, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var v2 SqlVector
|
||||
if err := yaml.Unmarshal(data, &v2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
for i, f := range v.Val {
|
||||
if v2.Val[i] != f {
|
||||
t.Errorf("index %d: expected %v, got %v", i, f, v2.Val[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Array types: XML ─────────────────────────────────────────────────────────
|
||||
|
||||
type xmlStringArrayWrapper struct {
|
||||
XMLName xml.Name `xml:"root"`
|
||||
Tags SqlStringArray `xml:"tags"`
|
||||
}
|
||||
|
||||
func TestSqlStringArray_XML(t *testing.T) {
|
||||
w := xmlStringArrayWrapper{Tags: NewSqlStringArray([]string{"x", "y", "z"})}
|
||||
data, err := xml.Marshal(w)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var w2 xmlStringArrayWrapper
|
||||
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
want := []string{"x", "y", "z"}
|
||||
if len(w2.Tags.Val) != len(want) {
|
||||
t.Fatalf("expected %v, got %v", want, w2.Tags.Val)
|
||||
}
|
||||
for i := range want {
|
||||
if w2.Tags.Val[i] != want[i] {
|
||||
t.Errorf("index %d: expected %q, got %q", i, want[i], w2.Tags.Val[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlStringArray_XML_Empty(t *testing.T) {
|
||||
w := xmlStringArrayWrapper{}
|
||||
data, err := xml.Marshal(w)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var w2 xmlStringArrayWrapper
|
||||
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
if len(w2.Tags.Val) != 0 {
|
||||
t.Errorf("expected empty slice, got %v", w2.Tags.Val)
|
||||
}
|
||||
}
|
||||
|
||||
type xmlInt64ArrayWrapper struct {
|
||||
XMLName xml.Name `xml:"root"`
|
||||
Scores SqlInt64Array `xml:"scores"`
|
||||
}
|
||||
|
||||
func TestSqlInt64Array_XML(t *testing.T) {
|
||||
w := xmlInt64ArrayWrapper{Scores: NewSqlInt64Array([]int64{10, 20, 30})}
|
||||
data, err := xml.Marshal(w)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var w2 xmlInt64ArrayWrapper
|
||||
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
want := []int64{10, 20, 30}
|
||||
for i := range want {
|
||||
if w2.Scores.Val[i] != want[i] {
|
||||
t.Errorf("index %d: expected %d, got %d", i, want[i], w2.Scores.Val[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type xmlUUIDArrayWrapper struct {
|
||||
XMLName xml.Name `xml:"root"`
|
||||
IDs SqlUUIDArray `xml:"ids"`
|
||||
}
|
||||
|
||||
func TestSqlUUIDArray_XML(t *testing.T) {
|
||||
ids := []uuid.UUID{uuid.New(), uuid.New()}
|
||||
w := xmlUUIDArrayWrapper{IDs: NewSqlUUIDArray(ids)}
|
||||
data, err := xml.Marshal(w)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var w2 xmlUUIDArrayWrapper
|
||||
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
for i, id := range ids {
|
||||
if w2.IDs.Val[i] != id {
|
||||
t.Errorf("index %d: expected %v, got %v", i, id, w2.IDs.Val[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type xmlVectorWrapper struct {
|
||||
XMLName xml.Name `xml:"root"`
|
||||
Embedding SqlVector `xml:"embedding"`
|
||||
}
|
||||
|
||||
func TestSqlVector_XML(t *testing.T) {
|
||||
w := xmlVectorWrapper{Embedding: NewSqlVector([]float32{1.5, -2.5})}
|
||||
data, err := xml.Marshal(w)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
var w2 xmlVectorWrapper
|
||||
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
want := []float32{1.5, -2.5}
|
||||
for i := range want {
|
||||
if w2.Embedding.Val[i] != want[i] {
|
||||
t.Errorf("index %d: expected %v, got %v", i, want[i], w2.Embedding.Val[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Combined struct round-trip ───────────────────────────────────────────────
|
||||
|
||||
type yamlXMLRecord struct {
|
||||
XMLName xml.Name `xml:"record" yaml:"-" json:"-"`
|
||||
ID SqlUUID `xml:"id" yaml:"id"`
|
||||
Name SqlString `xml:"name" yaml:"name"`
|
||||
Age SqlInt32 `xml:"age" yaml:"age"`
|
||||
Active SqlBool `xml:"active" yaml:"active"`
|
||||
Created SqlTimeStamp `xml:"created" yaml:"created"`
|
||||
Tags SqlStringArray `xml:"tags" yaml:"tags"`
|
||||
}
|
||||
|
||||
func TestCombinedRecord_YAML_RoundTrip(t *testing.T) {
|
||||
id := uuid.New()
|
||||
original := yamlXMLRecord{
|
||||
ID: NewSqlUUID(id),
|
||||
Name: NewSqlString("Ada"),
|
||||
Age: NewSqlInt32(36),
|
||||
Active: NewSqlBool(true),
|
||||
Created: NewSqlTimeStamp(time.Date(2024, 3, 10, 12, 30, 0, 0, time.UTC)),
|
||||
Tags: NewSqlStringArray([]string{"engineer", "mathematician"}),
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(original)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
|
||||
var decoded yamlXMLRecord
|
||||
if err := yaml.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
if decoded.ID.UUID() != id {
|
||||
t.Errorf("ID: expected %v, got %v", id, decoded.ID.UUID())
|
||||
}
|
||||
if decoded.Name.String() != "Ada" {
|
||||
t.Errorf("Name: expected Ada, got %q", decoded.Name.String())
|
||||
}
|
||||
if decoded.Age.Int64() != 36 {
|
||||
t.Errorf("Age: expected 36, got %d", decoded.Age.Int64())
|
||||
}
|
||||
if !decoded.Active.Bool() {
|
||||
t.Error("Active: expected true")
|
||||
}
|
||||
if decoded.Created.Time().Format("2006-01-02T15:04:05") != "2024-03-10T12:30:00" {
|
||||
t.Errorf("Created: expected 2024-03-10T12:30:00, got %v", decoded.Created.Time())
|
||||
}
|
||||
if len(decoded.Tags.Val) != 2 || decoded.Tags.Val[0] != "engineer" {
|
||||
t.Errorf("Tags: unexpected value %v", decoded.Tags.Val)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCombinedRecord_XML_RoundTrip(t *testing.T) {
|
||||
id := uuid.New()
|
||||
original := yamlXMLRecord{
|
||||
ID: NewSqlUUID(id),
|
||||
Name: NewSqlString("Ada"),
|
||||
Age: NewSqlInt32(36),
|
||||
Active: NewSqlBool(true),
|
||||
Created: NewSqlTimeStamp(time.Date(2024, 3, 10, 12, 30, 0, 0, time.UTC)),
|
||||
Tags: NewSqlStringArray([]string{"engineer", "mathematician"}),
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(original)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
|
||||
var decoded yamlXMLRecord
|
||||
if err := xml.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
if decoded.ID.UUID() != id {
|
||||
t.Errorf("ID: expected %v, got %v", id, decoded.ID.UUID())
|
||||
}
|
||||
if decoded.Name.String() != "Ada" {
|
||||
t.Errorf("Name: expected Ada, got %q", decoded.Name.String())
|
||||
}
|
||||
if decoded.Age.Int64() != 36 {
|
||||
t.Errorf("Age: expected 36, got %d", decoded.Age.Int64())
|
||||
}
|
||||
if !decoded.Active.Bool() {
|
||||
t.Error("Active: expected true")
|
||||
}
|
||||
if decoded.Created.Time().Format("2006-01-02T15:04:05") != "2024-03-10T12:30:00" {
|
||||
t.Errorf("Created: expected 2024-03-10T12:30:00, got %v", decoded.Created.Time())
|
||||
}
|
||||
if len(decoded.Tags.Val) != 2 || decoded.Tags.Val[0] != "engineer" {
|
||||
t.Errorf("Tags: unexpected value %v", decoded.Tags.Val)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package sqltypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// record mimics a typical DB model composed of sqltypes fields, exercising
|
||||
// marshalling/unmarshalling of the whole set together as encoding/json would
|
||||
// when used on a real struct (not just the individual types in isolation).
|
||||
type record struct {
|
||||
ID SqlUUID `json:"id"`
|
||||
Name SqlString `json:"name"`
|
||||
Bio SqlString `json:"bio"`
|
||||
Age SqlInt32 `json:"age"`
|
||||
Score SqlFloat64 `json:"score"`
|
||||
Active SqlBool `json:"active"`
|
||||
CreatedAt SqlTimeStamp `json:"created_at"`
|
||||
BirthDate SqlDate `json:"birth_date"`
|
||||
Avatar SqlByteArray `json:"avatar"`
|
||||
Tags SqlStringArray `json:"tags"`
|
||||
Scores SqlInt32Array `json:"scores"`
|
||||
Metadata SqlJSONB `json:"metadata"`
|
||||
Embedding SqlVector `json:"embedding"`
|
||||
Extras []uuid.UUID `json:"-"`
|
||||
}
|
||||
|
||||
func TestStruct_JSON_RoundTrip_AllFieldsPresent(t *testing.T) {
|
||||
id := uuid.New()
|
||||
createdAt := time.Date(2024, 3, 10, 12, 30, 0, 0, time.UTC)
|
||||
birthDate := time.Date(1990, 5, 20, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
original := record{
|
||||
ID: NewSqlUUID(id),
|
||||
Name: NewSqlString("Ada Lovelace"),
|
||||
Bio: SqlString{}, // intentionally null
|
||||
Age: NewSqlInt32(36),
|
||||
Score: NewSqlFloat64(98.6),
|
||||
Active: NewSqlBool(true),
|
||||
CreatedAt: NewSqlTimeStamp(createdAt),
|
||||
BirthDate: NewSqlDate(birthDate),
|
||||
Avatar: NewSqlByteArray([]byte{0xDE, 0xAD, 0xBE, 0xEF}),
|
||||
Tags: NewSqlStringArray([]string{"engineer", "mathematician"}),
|
||||
Scores: NewSqlInt32Array([]int32{10, 20, 30}),
|
||||
Metadata: SqlJSONB(`{"role":"admin"}`),
|
||||
Embedding: NewSqlVector([]float32{0.1, 0.2, 0.3}),
|
||||
}
|
||||
|
||||
data, err := json.Marshal(original)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
|
||||
var decoded record
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
if decoded.ID.UUID() != id {
|
||||
t.Errorf("ID: expected %v, got %v", id, decoded.ID.UUID())
|
||||
}
|
||||
if decoded.Name.String() != "Ada Lovelace" {
|
||||
t.Errorf("Name: expected Ada Lovelace, got %q", decoded.Name.String())
|
||||
}
|
||||
if decoded.Bio.Valid {
|
||||
t.Errorf("Bio: expected invalid/null, got %v", decoded.Bio)
|
||||
}
|
||||
if decoded.Age.Int64() != 36 {
|
||||
t.Errorf("Age: expected 36, got %d", decoded.Age.Int64())
|
||||
}
|
||||
if decoded.Score.Float64() != 98.6 {
|
||||
t.Errorf("Score: expected 98.6, got %v", decoded.Score.Float64())
|
||||
}
|
||||
if !decoded.Active.Bool() {
|
||||
t.Errorf("Active: expected true")
|
||||
}
|
||||
if decoded.CreatedAt.Time().Format("2006-01-02T15:04:05") != createdAt.Format("2006-01-02T15:04:05") {
|
||||
t.Errorf("CreatedAt: expected %v, got %v", createdAt, decoded.CreatedAt.Time())
|
||||
}
|
||||
if decoded.BirthDate.String() != "1990-05-20" {
|
||||
t.Errorf("BirthDate: expected 1990-05-20, got %q", decoded.BirthDate.String())
|
||||
}
|
||||
if string(decoded.Avatar.Val) != string(original.Avatar.Val) {
|
||||
t.Errorf("Avatar: expected %v, got %v", original.Avatar.Val, decoded.Avatar.Val)
|
||||
}
|
||||
if len(decoded.Tags.Val) != 2 || decoded.Tags.Val[0] != "engineer" {
|
||||
t.Errorf("Tags: unexpected value %v", decoded.Tags.Val)
|
||||
}
|
||||
if len(decoded.Scores.Val) != 3 || decoded.Scores.Val[2] != 30 {
|
||||
t.Errorf("Scores: unexpected value %v", decoded.Scores.Val)
|
||||
}
|
||||
m, err := decoded.Metadata.AsMap()
|
||||
if err != nil || m["role"] != "admin" {
|
||||
t.Errorf("Metadata: expected role=admin, got %v (err=%v)", m, err)
|
||||
}
|
||||
if len(decoded.Embedding.Val) != 3 || decoded.Embedding.Val[1] != 0.2 {
|
||||
t.Errorf("Embedding: unexpected value %v", decoded.Embedding.Val)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStruct_JSON_RoundTrip_AllNull(t *testing.T) {
|
||||
var original record
|
||||
|
||||
data, err := json.Marshal(original)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
|
||||
var decoded record
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
if decoded.ID.Valid || decoded.Name.Valid || decoded.Age.Valid || decoded.Score.Valid ||
|
||||
decoded.Active.Valid || decoded.CreatedAt.Valid || decoded.BirthDate.Valid ||
|
||||
decoded.Avatar.Valid || decoded.Tags.Valid || decoded.Scores.Valid || decoded.Embedding.Valid {
|
||||
t.Errorf("expected all fields invalid/null after round-trip, got %+v", decoded)
|
||||
}
|
||||
if decoded.Metadata != nil {
|
||||
t.Errorf("expected nil Metadata, got %v", decoded.Metadata)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStruct_JSON_UnmarshalFromRawJSON(t *testing.T) {
|
||||
raw := `{
|
||||
"id": "3e843d4e-6b3c-4f2e-9a1e-6f0b2f3c9d10",
|
||||
"name": "Grace Hopper",
|
||||
"bio": null,
|
||||
"age": 85,
|
||||
"score": 100,
|
||||
"active": false,
|
||||
"created_at": "2023-12-01T08:00:00",
|
||||
"birth_date": "1906-12-09",
|
||||
"avatar": "AQIDBA==",
|
||||
"tags": ["navy", "compiler"],
|
||||
"scores": [1, 2, 3],
|
||||
"metadata": {"key": "value"},
|
||||
"embedding": [1.0, 2.0]
|
||||
}`
|
||||
|
||||
var decoded record
|
||||
if err := json.Unmarshal([]byte(raw), &decoded); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
if decoded.Name.String() != "Grace Hopper" {
|
||||
t.Errorf("expected Grace Hopper, got %q", decoded.Name.String())
|
||||
}
|
||||
if decoded.Age.Int64() != 85 {
|
||||
t.Errorf("expected age 85, got %d", decoded.Age.Int64())
|
||||
}
|
||||
if decoded.Active.Valid && decoded.Active.Bool() {
|
||||
t.Errorf("expected active=false")
|
||||
}
|
||||
if string(decoded.Avatar.Val) != string([]byte{1, 2, 3, 4}) {
|
||||
t.Errorf("expected avatar bytes [1 2 3 4], got %v", decoded.Avatar.Val)
|
||||
}
|
||||
if len(decoded.Tags.Val) != 2 || decoded.Tags.Val[1] != "compiler" {
|
||||
t.Errorf("unexpected tags %v", decoded.Tags.Val)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package sqltypes
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// TestUUIDWithRealDatabase tests that SqlUUID works with actual database operations
|
||||
func TestUUIDWithRealDatabase(t *testing.T) {
|
||||
// Open an in-memory SQLite database
|
||||
db, err := sql.Open("sqlite", ":memory:")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Create a test table with UUID column
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE test_users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id TEXT,
|
||||
name TEXT
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create table: %v", err)
|
||||
}
|
||||
|
||||
// Test 1: Insert with UUID
|
||||
testUUID1 := uuid.New()
|
||||
sqlUUID1 := NewSqlUUID(testUUID1)
|
||||
|
||||
_, err = db.Exec("INSERT INTO test_users (id, user_id, name) VALUES (?, ?, ?)",
|
||||
1, sqlUUID1, "Alice")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to insert record: %v", err)
|
||||
}
|
||||
|
||||
// Test 2: Update with UUID
|
||||
testUUID2 := uuid.New()
|
||||
sqlUUID2 := NewSqlUUID(testUUID2)
|
||||
|
||||
_, err = db.Exec("UPDATE test_users SET user_id = ? WHERE id = ?",
|
||||
sqlUUID2, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to update record: %v", err)
|
||||
}
|
||||
|
||||
// Test 3: Read back and verify
|
||||
var retrievedID string
|
||||
var name string
|
||||
err = db.QueryRow("SELECT user_id, name FROM test_users WHERE id = ?", 1).Scan(&retrievedID, &name)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to query record: %v", err)
|
||||
}
|
||||
|
||||
if retrievedID != testUUID2.String() {
|
||||
t.Errorf("Expected UUID %s, got %s", testUUID2.String(), retrievedID)
|
||||
}
|
||||
|
||||
if name != "Alice" {
|
||||
t.Errorf("Expected name 'Alice', got '%s'", name)
|
||||
}
|
||||
|
||||
// Test 4: Insert with NULL UUID
|
||||
nullUUID := SqlUUID{Valid: false}
|
||||
_, err = db.Exec("INSERT INTO test_users (id, user_id, name) VALUES (?, ?, ?)",
|
||||
2, nullUUID, "Bob")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to insert record with NULL UUID: %v", err)
|
||||
}
|
||||
|
||||
// Test 5: Read NULL UUID back
|
||||
var retrievedNullID sql.NullString
|
||||
err = db.QueryRow("SELECT user_id FROM test_users WHERE id = ?", 2).Scan(&retrievedNullID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to query NULL UUID record: %v", err)
|
||||
}
|
||||
|
||||
if retrievedNullID.Valid {
|
||||
t.Errorf("Expected NULL UUID, got %s", retrievedNullID.String)
|
||||
}
|
||||
|
||||
t.Logf("All database operations with UUID succeeded!")
|
||||
}
|
||||
|
||||
// TestUUIDValueReturnsString verifies that Value() returns string, not uuid.UUID
|
||||
func TestUUIDValueReturnsString(t *testing.T) {
|
||||
testUUID := uuid.New()
|
||||
sqlUUID := NewSqlUUID(testUUID)
|
||||
|
||||
val, err := sqlUUID.Value()
|
||||
if err != nil {
|
||||
t.Fatalf("Value() failed: %v", err)
|
||||
}
|
||||
|
||||
// The value should be a string, not a uuid.UUID
|
||||
strVal, ok := val.(string)
|
||||
if !ok {
|
||||
t.Fatalf("Expected Value() to return string, got %T", val)
|
||||
}
|
||||
|
||||
if strVal != testUUID.String() {
|
||||
t.Errorf("Expected %s, got %s", testUUID.String(), strVal)
|
||||
}
|
||||
|
||||
t.Logf("✓ Value() correctly returns string: %s", strVal)
|
||||
}
|
||||
|
||||
// CustomStringableType is a custom type that implements fmt.Stringer
|
||||
type CustomStringableType string
|
||||
|
||||
func (c CustomStringableType) String() string {
|
||||
return "custom:" + string(c)
|
||||
}
|
||||
|
||||
// TestCustomStringableType verifies that any type implementing fmt.Stringer works
|
||||
func TestCustomStringableType(t *testing.T) {
|
||||
customVal := CustomStringableType("test-value")
|
||||
sqlCustom := SqlNull[CustomStringableType]{
|
||||
Val: customVal,
|
||||
Valid: true,
|
||||
}
|
||||
|
||||
val, err := sqlCustom.Value()
|
||||
if err != nil {
|
||||
t.Fatalf("Value() failed: %v", err)
|
||||
}
|
||||
|
||||
// Should return the result of String() method
|
||||
strVal, ok := val.(string)
|
||||
if !ok {
|
||||
t.Fatalf("Expected Value() to return string, got %T", val)
|
||||
}
|
||||
|
||||
expected := "custom:test-value"
|
||||
if strVal != expected {
|
||||
t.Errorf("Expected %s, got %s", expected, strVal)
|
||||
}
|
||||
|
||||
t.Logf("✓ Custom Stringer type correctly converted to string: %s", strVal)
|
||||
}
|
||||
|
||||
// TestStringMethodUsesStringer verifies that String() method also uses fmt.Stringer
|
||||
func TestStringMethodUsesStringer(t *testing.T) {
|
||||
// Test with UUID
|
||||
testUUID := uuid.New()
|
||||
sqlUUID := NewSqlUUID(testUUID)
|
||||
|
||||
strResult := sqlUUID.String()
|
||||
if strResult != testUUID.String() {
|
||||
t.Errorf("Expected UUID String() to return %s, got %s", testUUID.String(), strResult)
|
||||
}
|
||||
t.Logf("✓ UUID String() method: %s", strResult)
|
||||
|
||||
// Test with custom Stringer type
|
||||
customVal := CustomStringableType("test-value")
|
||||
sqlCustom := SqlNull[CustomStringableType]{
|
||||
Val: customVal,
|
||||
Valid: true,
|
||||
}
|
||||
|
||||
customStr := sqlCustom.String()
|
||||
expected := "custom:test-value"
|
||||
if customStr != expected {
|
||||
t.Errorf("Expected custom String() to return %s, got %s", expected, customStr)
|
||||
}
|
||||
t.Logf("✓ Custom Stringer String() method: %s", customStr)
|
||||
|
||||
// Test with regular type (should use fmt.Sprintf)
|
||||
sqlInt := NewSqlInt64(42)
|
||||
intStr := sqlInt.String()
|
||||
if intStr != "42" {
|
||||
t.Errorf("Expected int String() to return '42', got '%s'", intStr)
|
||||
}
|
||||
t.Logf("✓ Regular type String() method: %s", intStr)
|
||||
}
|
||||
Reference in New Issue
Block a user