Merge pull request #1421 from mattn/fix-callback-named-types

Fix panic when registered functions return named types
This commit is contained in:
mattn
2026-07-13 02:37:48 +00:00
committed by GitHub
2 changed files with 86 additions and 13 deletions

View File

@@ -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

View File

@@ -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 {