Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 124 additions & 7 deletions bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,27 @@ func asSliceForIn(i interface{}) (v reflect.Value, ok bool) {
// In expands slice values in args, returning the modified query string
// and a new arg list that can be executed by a database. The `query` should
// use the `?` bindVar. The return value uses the `?` bindVar.
//
// Single-column IN lists expand as usual:
//
// In("SELECT * FROM t WHERE id IN (?)", []int{1, 2, 3})
// // "SELECT * FROM t WHERE id IN (?, ?, ?)", []interface{}{1, 2, 3}
//
// Multi-column (tuple) IN lists are expanded when a slice element is itself
// a slice or array (other than []byte), matching issue #294:
//
// In("SELECT * FROM t WHERE (id, typ) IN (?)", [][]interface{}{{1, "a"}, {2, "b"}})
// // "SELECT * FROM t WHERE (id, typ) IN ((?, ?), (?, ?))", []interface{}{1, "a", 2, "b"}
//
// All tuple rows must have the same width.
func In(query string, args ...interface{}) (string, []interface{}, error) {
// argMeta stores reflect.Value and length for slices and
// the value itself for non-slice arguments
type argMeta struct {
v reflect.Value
i interface{}
length int
width int // >0 means each element is a tuple of this width
}

var flatArgsCount int
Expand Down Expand Up @@ -174,11 +188,21 @@ func In(query string, args ...interface{}) (string, []interface{}, error) {
meta[i].v = v

anySlices = true
flatArgsCount += meta[i].length

if meta[i].length == 0 {
return "", nil, errors.New("empty slice passed to 'in' query")
}

width, err := tupleWidth(v)
if err != nil {
return "", nil, err
}
meta[i].width = width
if width > 0 {
flatArgsCount += meta[i].length * width
} else {
flatArgsCount += meta[i].length
}
} else {
meta[i].i = arg
flatArgsCount++
Expand Down Expand Up @@ -219,15 +243,38 @@ func In(query string, args ...interface{}) (string, []interface{}, error) {
continue
}

// write everything up to and including our ? character
buf.WriteString(query[:offset+i+1])
if argMeta.width > 0 {
// Multi-column: replace "?" with "(?, ?), (?, ?), ..."
buf.WriteString(query[:offset+i])
for row := 0; row < argMeta.length; row++ {
if row > 0 {
buf.WriteString(", ")
}
buf.WriteByte('(')
for c := 0; c < argMeta.width; c++ {
if c > 0 {
buf.WriteString(", ")
}
buf.WriteByte('?')
}
buf.WriteByte(')')
}
var err error
newArgs, err = appendReflectTuples(newArgs, argMeta.v, argMeta.length, argMeta.width)
if err != nil {
return "", nil, err
}
} else {
// write everything up to and including our ? character
buf.WriteString(query[:offset+i+1])

for si := 1; si < argMeta.length; si++ {
buf.WriteString(", ?")
}

for si := 1; si < argMeta.length; si++ {
buf.WriteString(", ?")
newArgs = appendReflectSlice(newArgs, argMeta.v, argMeta.length)
}

newArgs = appendReflectSlice(newArgs, argMeta.v, argMeta.length)

// slice the query and reset the offset. this avoids some bookkeeping for
// the write after the loop
query = query[offset+i+1:]
Expand All @@ -243,6 +290,57 @@ func In(query string, args ...interface{}) (string, []interface{}, error) {
return buf.String(), newArgs, nil
}

// tupleWidth reports whether v is a slice of fixed-width tuples (slice/array
// elements). Returns 0 when elements are scalar values for a single-column IN.
func tupleWidth(v reflect.Value) (int, error) {
if v.Len() == 0 {
return 0, nil
}
first, ok := derefInValue(v.Index(0))
if !ok {
return 0, nil
}
switch first.Kind() {
case reflect.Array:
return first.Len(), nil
case reflect.Slice:
// []byte is a driver.Value / single bind value, not a tuple row.
if first.Type() == reflect.TypeOf([]byte{}) {
return 0, nil
}
w := first.Len()
if w == 0 {
return 0, errors.New("empty tuple passed to 'in' query")
}
// Validate remaining rows share the same width.
for i := 1; i < v.Len(); i++ {
el, ok := derefInValue(v.Index(i))
if !ok || (el.Kind() != reflect.Slice && el.Kind() != reflect.Array) {
return 0, errors.New("mixed tuple/non-tuple values in 'in' query")
}
if el.Type() == reflect.TypeOf([]byte{}) {
return 0, errors.New("mixed tuple/non-tuple values in 'in' query")
}
if el.Len() != w {
return 0, errors.New("tuple width mismatch in 'in' query")
}
}
return w, nil
default:
return 0, nil
}
}

func derefInValue(v reflect.Value) (reflect.Value, bool) {
for v.Kind() == reflect.Interface || v.Kind() == reflect.Ptr {
if v.IsNil() {
return reflect.Value{}, false
}
v = v.Elem()
}
return v, true
}

func appendReflectSlice(args []interface{}, v reflect.Value, vlen int) []interface{} {
switch val := v.Interface().(type) {
case []interface{}:
Expand All @@ -263,3 +361,22 @@ func appendReflectSlice(args []interface{}, v reflect.Value, vlen int) []interfa

return args
}

func appendReflectTuples(args []interface{}, v reflect.Value, rows, width int) ([]interface{}, error) {
for i := 0; i < rows; i++ {
el, ok := derefInValue(v.Index(i))
if !ok {
return nil, errors.New("nil tuple in 'in' query")
}
if el.Kind() != reflect.Slice && el.Kind() != reflect.Array {
return nil, errors.New("expected tuple row in 'in' query")
}
if el.Len() != width {
return nil, errors.New("tuple width mismatch in 'in' query")
}
for c := 0; c < width; c++ {
args = append(args, el.Index(c).Interface())
}
}
return args, nil
}
64 changes: 64 additions & 0 deletions sqlx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1604,6 +1604,70 @@ func TestIn(t *testing.T) {
})
}

func TestInMultiColumn(t *testing.T) {
q, args, err := In(
"SELECT * FROM t WHERE (id, typ) IN (?)",
[][]interface{}{{1, "a"}, {2, "b"}, {3, "c"}},
)
if err != nil {
t.Fatal(err)
}
wantQ := "SELECT * FROM t WHERE (id, typ) IN ((?, ?), (?, ?), (?, ?))"
if q != wantQ {
t.Fatalf("query=\n%q\nwant\n%q", q, wantQ)
}
if len(args) != 6 {
t.Fatalf("args=%v", args)
}
if args[0] != 1 || args[1] != "a" || args[5] != "c" {
t.Fatalf("args=%v", args)
}

// fixed-size array rows
q, args, err = In("WHERE (a, b) IN (?)", [][2]int{{1, 2}, {3, 4}})
if err != nil {
t.Fatal(err)
}
if q != "WHERE (a, b) IN ((?, ?), (?, ?))" {
t.Fatalf("q=%q", q)
}
if len(args) != 4 || args[0] != 1 || args[3] != 4 {
t.Fatalf("args=%v", args)
}

// single-column [][]byte still expands as scalar bind values, not tuples
q, args, err = In("WHERE b IN (?)", [][]byte{[]byte("x"), []byte("y")})
if err != nil {
t.Fatal(err)
}
if q != "WHERE b IN (?, ?)" {
t.Fatalf("[][]byte should be single-column, got %q", q)
}

// width mismatch
_, _, err = In("WHERE (a, b) IN (?)", [][]interface{}{{1, 2}, {3}})
if err == nil {
t.Fatal("expected width mismatch error")
}

// mixed with other bind vars
q, args, err = In(
"SELECT * FROM t WHERE x = ? AND (id, typ) IN (?) AND y = ?",
"pre",
[][]interface{}{{9, "z"}},
"post",
)
if err != nil {
t.Fatal(err)
}
if q != "SELECT * FROM t WHERE x = ? AND (id, typ) IN ((?, ?)) AND y = ?" {
t.Fatalf("q=%q", q)
}
if len(args) != 4 || args[0] != "pre" || args[3] != "post" {
t.Fatalf("args=%v", args)
}
}

func TestBindStruct(t *testing.T) {
var err error

Expand Down