Skip to content

Commit

Permalink
fix JSONField not handling NULL UUIDs
Browse files Browse the repository at this point in the history
  • Loading branch information
bokwoon95 committed Aug 31, 2024
1 parent f076302 commit e7ca6e3
Showing 4 changed files with 78 additions and 42 deletions.
19 changes: 0 additions & 19 deletions fetch_exec_test.go
Original file line number Diff line number Diff line change
@@ -2,32 +2,13 @@ package sq

import (
"database/sql"
"io"
"log"
"testing"
"time"

"github.com/bokwoon95/sq/internal/testutil"
_ "github.com/mattn/go-sqlite3"
)

func init() {
// For tests, silence the logger (the output is too noisy). If you need to
// see what queries are being run, comment this out.

defaultLogger = NewLogger(io.Discard, "", log.LstdFlags, LoggerConfig{
ShowTimeTaken: true,
ShowCaller: true,
HideArgs: true,
})
verboseLogger = NewLogger(io.Discard, "", log.LstdFlags, LoggerConfig{
ShowTimeTaken: true,
ShowCaller: true,
ShowResults: 5,
InterpolateVerbose: true,
})
}

var ACTOR = New[struct {
TableStruct `sq:"actor"`
ACTOR_ID NumberField
75 changes: 71 additions & 4 deletions integration_test.go
Original file line number Diff line number Diff line change
@@ -69,7 +69,7 @@ func TestRow(t *testing.T) {
dsn: "file:/TestRow/sqlite?vfs=memdb&_foreign_keys=true",
teardown: "DROP TABLE IF EXISTS table00;",
setup: "CREATE TABLE table00 (" +
"\n uuid UUID PRIMARY KEY" +
"\n uuid UUID" +
"\n ,data JSON" +
"\n ,color TEXT" +
"\n ,direction TEXT" +
@@ -100,7 +100,7 @@ func TestRow(t *testing.T) {
"\nCREATE TYPE direction AS ENUM ('north', 'south', 'east', 'west');" +
"\nCREATE TYPE weekday AS ENUM ('Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday');" +
"\nCREATE TABLE table00 (" +
"\n uuid UUID PRIMARY KEY" +
"\n uuid UUID" +
"\n ,data JSONB" +
"\n ,color color" +
"\n ,direction direction" +
@@ -125,7 +125,7 @@ func TestRow(t *testing.T) {
dsn: *mysqlDSN,
teardown: "DROP TABLE IF EXISTS table00;",
setup: "CREATE TABLE table00 (" +
"\n uuid BINARY(16) PRIMARY KEY" +
"\n uuid BINARY(16)" +
"\n ,data JSON" +
"\n ,color VARCHAR(255)" +
"\n ,direction VARCHAR(255)" +
@@ -150,7 +150,7 @@ func TestRow(t *testing.T) {
dsn: *sqlserverDSN,
teardown: "DROP TABLE IF EXISTS table00;",
setup: "CREATE TABLE table00 (" +
"\n uuid BINARY(16) PRIMARY KEY" +
"\n uuid BINARY(16)" +
"\n ,data NVARCHAR(MAX)" +
"\n ,color NVARCHAR(255)" +
"\n ,direction NVARCHAR(255)" +
@@ -377,6 +377,73 @@ func TestRow(t *testing.T) {
if !exists {
t.Errorf(testutil.Callers()+" expected row with uuid = %q to exist, got false", table00Values[0].uuid.String())
}

// SQLServer driver *still* doesn't support NULL UUIDs 🙄, skip
// NULL testing for SQL Server.
// https://github.com/denisenkom/go-mssqldb/issues/196
if tt.dialect == "sqlserver" {
return
}

// Insert NULLs.
_, err = Exec(Log(db), InsertInto(TABLE00).
ColumnValues(func(col *Column) {
col.Set(TABLE00.UUID, nil)
col.Set(TABLE00.DATA, nil)
col.Set(TABLE00.COLOR, nil)
col.Set(TABLE00.DIRECTION, nil)
col.Set(TABLE00.WEEKDAY, nil)
col.Set(TABLE00.TEXT_ARRAY, nil)
col.Set(TABLE00.INT_ARRAY, nil)
col.Set(TABLE00.INT64_ARRAY, nil)
col.Set(TABLE00.INT32_ARRAY, nil)
col.Set(TABLE00.FLOAT64_ARRAY, nil)
col.Set(TABLE00.FLOAT32_ARRAY, nil)
col.Set(TABLE00.BOOL_ARRAY, nil)
col.Set(TABLE00.BYTES, nil)
col.Set(TABLE00.IS_ACTIVE, nil)
col.Set(TABLE00.PRICE, nil)
col.Set(TABLE00.SCORE, nil)
col.Set(TABLE00.NAME, nil)
col.Set(TABLE00.UPDATED_AT, nil)
}).
SetDialect(tt.dialect),
)
if err != nil {
t.Fatal(testutil.Callers(), err)
}

// Fetch NULLs.
_, err = FetchAll(VerboseLog(db), From(TABLE00).
Where(TABLE00.UUID.IsNull()).
OrderBy(TABLE00.UUID).
SetDialect(tt.dialect),
func(row *Row) Table00 {
var value Table00
row.UUIDField(&value.uuid, TABLE00.UUID)
row.JSONField(&value.data, TABLE00.DATA)
row.EnumField(&value.color, TABLE00.COLOR)
row.EnumField(&value.direction, TABLE00.DIRECTION)
row.EnumField(&value.weekday, TABLE00.WEEKDAY)
row.ArrayField(&value.textArray, TABLE00.TEXT_ARRAY)
row.ArrayField(&value.intArray, TABLE00.INT_ARRAY)
row.ArrayField(&value.int64Array, TABLE00.INT64_ARRAY)
row.ArrayField(&value.int32Array, TABLE00.INT32_ARRAY)
row.ArrayField(&value.float64Array, TABLE00.FLOAT64_ARRAY)
row.ArrayField(&value.float32Array, TABLE00.FLOAT32_ARRAY)
row.ArrayField(&value.boolArray, TABLE00.BOOL_ARRAY)
value.bytes = row.BytesField(TABLE00.BYTES)
value.isActive = row.BoolField(TABLE00.IS_ACTIVE)
value.price = row.Float64Field(TABLE00.PRICE)
value.score = row.Int64Field(TABLE00.SCORE)
value.name = row.StringField(TABLE00.NAME)
value.updatedAt = row.TimeField(TABLE00.UPDATED_AT)
return value
},
)
if err != nil {
t.Fatal(testutil.Callers(), err)
}
})
}
}
17 changes: 6 additions & 11 deletions row_column.go
Original file line number Diff line number Diff line change
@@ -18,16 +18,11 @@ import (

// Row represents the state of a row after a call to rows.Next().
type Row struct {
dialect string
sqlRows *sql.Rows
runningIndex int
fields []Field
scanDest []any

// TODO: call Values using the go-mysql driver and check what the driver
// returns for bool and time.Time (without calling parseTime). Then we need
// to accomodate those cases into []byte handling below.
// TODO: Then! we can finally take the new code for a spin.
dialect string
sqlRows *sql.Rows
runningIndex int
fields []Field
scanDest []any
queryIsStatic bool
columns []string
columnTypes []*sql.ColumnType
@@ -1078,7 +1073,7 @@ func (row *Row) uuid(destPtr any, field UUID, skip int) {
var uuid [16]byte
if len(scanDest.bytes) == 16 {
copy(uuid[:], scanDest.bytes)
} else {
} else if len(scanDest.bytes) > 0 {
uuid, err = googleuuid.ParseBytes(scanDest.bytes)
if err != nil {
panic(fmt.Errorf(callsite(skip+1)+"parsing %q as UUID string: %w", string(scanDest.bytes), err))
9 changes: 1 addition & 8 deletions sq.go
Original file line number Diff line number Diff line change
@@ -8,7 +8,6 @@ import (
"encoding/json"
"fmt"
"reflect"
"runtime"
"strings"
"sync"

@@ -301,10 +300,7 @@ func writeFields(ctx context.Context, dialect string, buf *bytes.Buffer, args *[
return nil
}

// mapperFunctionPanicked recovers from any panics *except* for runtime error
// panics. Runtime error panics like out-of-bounds index accesses or failed
// type assertions are not normal so we don't want to swallow the stack trace
// by recovering from it.
// mapperFunctionPanicked recovers from any panics.
//
// The function is called as such so that it shows up as
// "sq.mapperFunctionPanicked" in panic stack trace, giving the user a
@@ -313,9 +309,6 @@ func mapperFunctionPanicked(err *error) {
if r := recover(); r != nil {
switch r := r.(type) {
case error:
if runtimeErr, ok := r.(runtime.Error); ok {
panic(runtimeErr)
}
*err = r
default:
*err = fmt.Errorf(fmt.Sprint(r))

0 comments on commit e7ca6e3

Please sign in to comment.