Skip to content

Commit 00c6e74

Browse files
committed
allow users to specify bind type (#520)
This is a thread-safe implementation of #520, which adds a single function `sqlx.BindDriver(driverName string, bindType int)` that allows users of sqlx to specify the bind type for any driver. This allows rebinding, and therefore named queries, to work in a lot more cases: * a new driver is released but not yet catalogued in SQLX * users customize a driver and give it a new name To do this, a registry had to be created so that it could be updated at runtime. This represents a synchronization problem, as it would be written to and read from after compile time. I tried two approaches: * use `sync.Map` * use `atomic.Value` and load/store the registry Details within, but `sync.Map` ended up being 5x slower, and the `atomic.Value` approach was ~2.5x slower: ``` goos: linux goarch: amd64 pkg: github.com/jmoiron/sqlx original: BenchmarkBindSpeed/old-4 100000000 11.0 ns/op sync.Map: BenchmarkBindSpeed/new-4 24575726 50.8 ns/op atomic.Value BenchmarkBindSpeed/new-4 42535839 27.5 ns/op ``` Despite the slower showing, using `atomic.Value` in this way has a correctness tradeoff. Loads and Stores are guaranteed to be atomic, but using Load+Store means that in cases of simultaneous writes, a write could get lost. This would be a _very_ difficult bug to find, so I've opted for `sync.Map` despite the worse performance. I think this is an acceptable trade-off as this is really unlikely to be in any hot loops. If this performance degredation does become a problem, another option would be to hardcode the original registry in a switch as in the original implementation, and only fallback on the custom registry. I don't know of a use case where people would want to _change_ the bindtype of a driver whose bindtype is already known, but the flexibility seems worth it as the performance lost doesn't seem like it's important.
1 parent 0794cb1 commit 00c6e74

File tree

2 files changed

+107
-12
lines changed

2 files changed

+107
-12
lines changed

bind.go

+28-12
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"reflect"
88
"strconv"
99
"strings"
10+
"sync"
1011

1112
"github.com/jmoiron/sqlx/reflectx"
1213
)
@@ -20,21 +21,36 @@ const (
2021
AT
2122
)
2223

24+
var defaultBinds = map[int][]string{
25+
DOLLAR: []string{"postgres", "pgx", "pq-timeouts", "cloudsqlpostgres", "ql"},
26+
QUESTION: []string{"mysql", "sqlite3"},
27+
NAMED: []string{"oci8", "ora", "goracle"},
28+
AT: []string{"sqlserver"},
29+
}
30+
31+
var binds sync.Map
32+
33+
func init() {
34+
for bind, drivers := range defaultBinds {
35+
for _, driver := range drivers {
36+
BindDriver(driver, bind)
37+
}
38+
}
39+
40+
}
41+
2342
// BindType returns the bindtype for a given database given a drivername.
2443
func BindType(driverName string) int {
25-
switch driverName {
26-
case "postgres", "pgx", "pq-timeouts", "cloudsqlpostgres", "ql":
27-
return DOLLAR
28-
case "mysql":
29-
return QUESTION
30-
case "sqlite3":
31-
return QUESTION
32-
case "oci8", "ora", "goracle", "godror":
33-
return NAMED
34-
case "sqlserver":
35-
return AT
44+
itype, ok := binds.Load(driverName)
45+
if !ok {
46+
return UNKNOWN
3647
}
37-
return UNKNOWN
48+
return itype.(int)
49+
}
50+
51+
// BindDriver sets the BindType for driverName to bindType.
52+
func BindDriver(driverName string, bindType int) {
53+
binds.Store(driverName, bindType)
3854
}
3955

4056
// FIXME: this should be able to be tolerant of escaped ?'s in queries without

bind_test.go

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package sqlx
2+
3+
import (
4+
"math/rand"
5+
"testing"
6+
)
7+
8+
func oldBindType(driverName string) int {
9+
switch driverName {
10+
case "postgres", "pgx", "pq-timeouts", "cloudsqlpostgres", "ql":
11+
return DOLLAR
12+
case "mysql":
13+
return QUESTION
14+
case "sqlite3":
15+
return QUESTION
16+
case "oci8", "ora", "goracle", "godror":
17+
return NAMED
18+
case "sqlserver":
19+
return AT
20+
}
21+
return UNKNOWN
22+
}
23+
24+
/*
25+
sync.Map implementation:
26+
27+
goos: linux
28+
goarch: amd64
29+
pkg: github.com/jmoiron/sqlx
30+
BenchmarkBindSpeed/old-4 100000000 11.0 ns/op
31+
BenchmarkBindSpeed/new-4 24575726 50.8 ns/op
32+
33+
34+
async.Value map implementation:
35+
36+
goos: linux
37+
goarch: amd64
38+
pkg: github.com/jmoiron/sqlx
39+
BenchmarkBindSpeed/old-4 100000000 11.0 ns/op
40+
BenchmarkBindSpeed/new-4 42535839 27.5 ns/op
41+
*/
42+
43+
func BenchmarkBindSpeed(b *testing.B) {
44+
testDrivers := []string{
45+
"postgres", "pgx", "mysql", "sqlite3", "ora", "sqlserver",
46+
}
47+
48+
b.Run("old", func(b *testing.B) {
49+
b.StopTimer()
50+
var seq []int
51+
for i := 0; i < b.N; i++ {
52+
seq = append(seq, rand.Intn(len(testDrivers)))
53+
}
54+
b.StartTimer()
55+
for i := 0; i < b.N; i++ {
56+
s := oldBindType(testDrivers[seq[i]])
57+
if s == UNKNOWN {
58+
b.Error("unknown driver")
59+
}
60+
}
61+
62+
})
63+
64+
b.Run("new", func(b *testing.B) {
65+
b.StopTimer()
66+
var seq []int
67+
for i := 0; i < b.N; i++ {
68+
seq = append(seq, rand.Intn(len(testDrivers)))
69+
}
70+
b.StartTimer()
71+
for i := 0; i < b.N; i++ {
72+
s := BindType(testDrivers[seq[i]])
73+
if s == UNKNOWN {
74+
b.Error("unknown driver")
75+
}
76+
}
77+
78+
})
79+
}

0 commit comments

Comments
 (0)