Skip to content

Commit 531f756

Browse files
authored
Dispatcher to handle anon func returned in closure (#27)
1 parent a630a1c commit 531f756

File tree

3 files changed

+87
-7
lines changed

3 files changed

+87
-7
lines changed

querysql/gomssqldispatcher.go

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@ import (
44
"database/sql"
55
"fmt"
66
"reflect"
7+
"regexp"
78
"runtime"
89
"strconv"
910
"strings"
1011
)
1112

1213
type funcInfo struct {
13-
name string
14-
numArgs int
15-
argType []reflect.Type
16-
valueOf reflect.Value
14+
name string
15+
numArgs int
16+
isClosure bool
17+
argType []reflect.Type
18+
valueOf reflect.Value
1719
}
1820

1921
func GoMSSQLDispatcher(fs []interface{}) RowsGoDispatcher {
@@ -30,13 +32,21 @@ func GoMSSQLDispatcher(fs []interface{}) RowsGoDispatcher {
3032
}
3133

3234
fInfo.valueOf = reflect.ValueOf(f)
33-
getFunctionName := func(fullName string) string {
35+
getFunctionName := func(fullName string) (string, bool) {
3436
paths := strings.Split(fullName, "/")
3537
lastPath := paths[len(paths)-1]
3638
parts := strings.Split(lastPath, ".")
37-
return parts[len(parts)-1]
39+
fName := parts[len(parts)-1]
40+
matched, err := regexp.Match(`func\d+`, []byte(fName))
41+
if err != nil {
42+
panic(err.Error())
43+
}
44+
if matched {
45+
return parts[len(parts)-2], true // It is a closure
46+
}
47+
return fName, false
3848
}
39-
fInfo.name = getFunctionName(runtime.FuncForPC(fInfo.valueOf.Pointer()).Name())
49+
fInfo.name, fInfo.isClosure = getFunctionName(runtime.FuncForPC(fInfo.valueOf.Pointer()).Name())
4050

4151
if knownFuncs == "" {
4252
knownFuncs = fmt.Sprintf("'%s'", fInfo.name)
@@ -51,6 +61,9 @@ func GoMSSQLDispatcher(fs []interface{}) RowsGoDispatcher {
5161
for i := 0; i < fInfo.numArgs; i++ {
5262
fInfo.argType[i] = funcType.In(i)
5363
}
64+
if _, in := funcMap[fInfo.name]; in {
65+
panic(fmt.Sprintf("Function already in dispatcher %s (closure==%v)", fInfo.name, fInfo.isClosure))
66+
}
5467
funcMap[fInfo.name] = fInfo
5568
}
5669

querysql/querysql_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,3 +714,64 @@ values (42.00);
714714
assert.NoError(t, err)
715715
assert.Equal(t, "42.00", m.String())
716716
}
717+
718+
func TestAnonDispatcherFunc(t *testing.T) {
719+
qry := `
720+
if OBJECT_ID('dbo.MyUsers', 'U') is not null drop table MyUsers
721+
create table MyUsers (
722+
ID INT IDENTITY(1,1) PRIMARY KEY,
723+
Username NVARCHAR(50)
724+
);
725+
insert into MyUsers (Username) values ('JohnDoe');
726+
727+
-- logging
728+
select _log='info', Y = 'one';
729+
730+
-- dispatcher
731+
select _function='TestFunction', component = 'abc', val=1, time=1.23;
732+
733+
select _function='ReturnAnonFunc', label = 'myLabel', time=1.23;
734+
`
735+
736+
var hook LogHook
737+
logger := logrus.StandardLogger()
738+
logger.Hooks.Add(&hook)
739+
ctx := querysql.WithLogger(context.Background(), querysql.LogrusMSSQLLogger(logger, logrus.InfoLevel))
740+
ctx = querysql.WithDispatcher(ctx, querysql.GoMSSQLDispatcher([]interface{}{
741+
testhelper.TestFunction,
742+
testhelper.ReturnAnonFunc("myComponent"),
743+
}))
744+
testhelper.ResetTestFunctionsCalled()
745+
746+
_, err := querysql.ExecContext(ctx, sqldb, qry, "world")
747+
assert.NoError(t, err)
748+
749+
assert.True(t, testhelper.TestFunctionsCalled["ReturnAnonFunc.myComponent"])
750+
751+
// Check that we have exhausted the logging select before we do the call that gets ErrNoMoreSets
752+
assert.Equal(t, []logrus.Fields{
753+
{"Y": "one"},
754+
}, hook.lines)
755+
756+
assert.True(t, testhelper.TestFunctionsCalled["TestFunction"])
757+
}
758+
759+
func TestDispatcherPanicsWithTwoAnonFuncs(t *testing.T) {
760+
var mustNotBeTrue bool
761+
var hook LogHook
762+
logger := logrus.StandardLogger()
763+
logger.Hooks.Add(&hook)
764+
defer func() {
765+
r := recover()
766+
assert.NotNil(t, r) // nil if a panic didn't happen, not nil if a panic happened
767+
assert.False(t, mustNotBeTrue)
768+
}()
769+
770+
ctx := querysql.WithLogger(context.Background(), querysql.LogrusMSSQLLogger(logger, logrus.InfoLevel))
771+
ctx = querysql.WithDispatcher(ctx, querysql.GoMSSQLDispatcher([]interface{}{
772+
testhelper.ReturnAnonFunc("myComponent"),
773+
testhelper.ReturnAnonFunc("myComponent2"), // This should cause a panic
774+
}))
775+
// Nothing here gets executed because we expect the WithDispatcher to have panicked
776+
mustNotBeTrue = true
777+
}

querysql/testhelper/helper.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ func OtherTestFunction(time float64, money float64) {
2323
TestFunctionsCalled[getFunctionName()] = true
2424
}
2525

26+
func ReturnAnonFunc(component string) func(string, float64) {
27+
return func(label string, time float64) {
28+
TestFunctionsCalled[fmt.Sprintf("ReturnAnonFunc.%s", component)] = true
29+
}
30+
}
31+
2632
func ResetTestFunctionsCalled() {
2733
for k, _ := range TestFunctionsCalled {
2834
TestFunctionsCalled[k] = false

0 commit comments

Comments
 (0)