diff --git a/callback.go b/callback.go index cda4537..702f234 100644 --- a/callback.go +++ b/callback.go @@ -260,6 +260,16 @@ func callbackArgGeneric(v *C.sqlite3_value) (reflect.Value, error) { } } +// callbackArgConvert returns conv as-is when the parameter type is the +// canonical type conv produces, and wraps it with a cast for named types +// (e.g. time.Duration), which reflect.Call would otherwise panic on. +func callbackArgConvert(conv callbackArgConverter, typ, canonical reflect.Type) callbackArgConverter { + if typ == canonical { + return conv + } + return callbackArgCast{conv, typ}.Run +} + func callbackArg(typ reflect.Type) (callbackArgConverter, error) { switch typ.Kind() { case reflect.Interface: @@ -271,18 +281,18 @@ func callbackArg(typ reflect.Type) (callbackArgConverter, error) { if typ.Elem().Kind() != reflect.Uint8 { return nil, errors.New("the only supported slice type is []byte") } - return callbackArgBytes, nil + return callbackArgConvert(callbackArgBytes, typ, reflect.TypeOf([]byte(nil))), nil case reflect.String: - return callbackArgString, nil + return callbackArgConvert(callbackArgString, typ, reflect.TypeOf("")), nil case reflect.Bool: - return callbackArgBool, nil + return callbackArgConvert(callbackArgBool, typ, reflect.TypeOf(false)), nil case reflect.Int64: - return callbackArgInt64, nil + return callbackArgConvert(callbackArgInt64, typ, reflect.TypeOf(int64(0))), nil case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Int, reflect.Uint: c := callbackArgCast{callbackArgInt64, typ} return c.Run, nil case reflect.Float64: - return callbackArgFloat64, nil + return callbackArgConvert(callbackArgFloat64, typ, reflect.TypeOf(float64(0))), nil case reflect.Float32: c := callbackArgCast{callbackArgFloat64, typ} return c.Run, nil diff --git a/sqlite3_test.go b/sqlite3_test.go index 6d6100d..d8a535b 100644 --- a/sqlite3_test.go +++ b/sqlite3_test.go @@ -1439,6 +1439,71 @@ func TestFunctionRegistration(t *testing.T) { } } +func TestFunctionRegistrationNamedTypes(t *testing.T) { + type NInt int64 + type NFloat float64 + type NString string + type NBlob []byte + type NBool bool + + dur := func(n int64) time.Duration { return time.Duration(n) } + nint := func(a, b NInt) NInt { return a + b } + nfloat := func(a, b NFloat) NFloat { return a + b } + nstring := func(s NString) NString { return s + "!" } + nblob := func(s string) NBlob { return NBlob(s) } + nbool := func(b NBool) NBool { return !b } + + sql.Register("sqlite3_FunctionRegistrationNamedTypes", &SQLiteDriver{ + ConnectHook: func(conn *SQLiteConn) error { + if err := conn.RegisterFunc("dur", dur, true); err != nil { + return err + } + if err := conn.RegisterFunc("nint", nint, true); err != nil { + return err + } + if err := conn.RegisterFunc("nfloat", nfloat, true); err != nil { + return err + } + if err := conn.RegisterFunc("nstring", nstring, true); err != nil { + return err + } + if err := conn.RegisterFunc("nblob", nblob, true); err != nil { + return err + } + return conn.RegisterFunc("nbool", nbool, true) + }, + }) + db, err := sql.Open("sqlite3_FunctionRegistrationNamedTypes", ":memory:") + if err != nil { + t.Fatal("Failed to open database:", err) + } + defer db.Close() + + ops := []struct { + query string + expected any + }{ + {"SELECT dur(42)", int64(42)}, + {"SELECT nint(1,2)", int64(3)}, + {"SELECT nfloat(1.5,1.5)", float64(3)}, + {`SELECT nstring('foo')`, "foo!"}, + {`SELECT nblob('xy')`, []byte("xy")}, + // An empty blob result is mapped to SQL NULL. + {`SELECT nblob('') IS NULL`, true}, + {"SELECT nbool(0)", true}, + } + + for _, op := range ops { + ret := reflect.New(reflect.TypeOf(op.expected)) + err = db.QueryRow(op.query).Scan(ret.Interface()) + if err != nil { + t.Errorf("Query %q failed: %s", op.query, err) + } else if !reflect.DeepEqual(ret.Elem().Interface(), op.expected) { + t.Errorf("Query %q returned wrong value: got %v (%T), want %v (%T)", op.query, ret.Elem().Interface(), ret.Elem().Interface(), op.expected, op.expected) + } + } +} + func TestFunctionArgStringContainingZero(t *testing.T) { sql.Register("sqlite3_FunctionArgZero", &SQLiteDriver{ ConnectHook: func(conn *SQLiteConn) error {