diff --git a/callback.go b/callback.go index 293d26d..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 @@ -326,8 +336,7 @@ func callbackRetInteger(ctx *C.sqlite3_context, v reflect.Value) error { case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Int, reflect.Uint: v = v.Convert(reflect.TypeOf(int64(0))) case reflect.Bool: - b := v.Interface().(bool) - if b { + if v.Bool() { v = reflect.ValueOf(int64(1)) } else { v = reflect.ValueOf(int64(0)) @@ -336,7 +345,7 @@ func callbackRetInteger(ctx *C.sqlite3_context, v reflect.Value) error { return fmt.Errorf("cannot convert %s to INTEGER", v.Type()) } - C.sqlite3_result_int64(ctx, C.sqlite3_int64(v.Interface().(int64))) + C.sqlite3_result_int64(ctx, C.sqlite3_int64(v.Int())) return nil } @@ -349,7 +358,7 @@ func callbackRetFloat(ctx *C.sqlite3_context, v reflect.Value) error { return fmt.Errorf("cannot convert %s to FLOAT", v.Type()) } - C.sqlite3_result_double(ctx, C.double(v.Interface().(float64))) + C.sqlite3_result_double(ctx, C.double(v.Float())) return nil } @@ -357,11 +366,10 @@ func callbackRetBlob(ctx *C.sqlite3_context, v reflect.Value) error { if v.Type().Kind() != reflect.Slice || v.Type().Elem().Kind() != reflect.Uint8 { return fmt.Errorf("cannot convert %s to BLOB", v.Type()) } - i := v.Interface() - if i == nil || len(i.([]byte)) == 0 { + bs := v.Bytes() + if len(bs) == 0 { C.sqlite3_result_null(ctx) } else { - bs := i.([]byte) if i64 && len(bs) > math.MaxInt32 { C.sqlite3_result_error_toobig(ctx) return nil @@ -375,7 +383,7 @@ func callbackRetText(ctx *C.sqlite3_context, v reflect.Value) error { if v.Type().Kind() != reflect.String { return fmt.Errorf("cannot convert %s to TEXT", v.Type()) } - s := v.Interface().(string) + s := v.String() if i64 && len(s) > math.MaxInt32 { C.sqlite3_result_error_toobig(ctx) return 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 {