Skip to content
Merged
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
21 changes: 17 additions & 4 deletions bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,20 @@ func asSliceForIn(i interface{}) (v reflect.Value, ok bool) {
// placeholder style. In always returns a query that still uses `?`; call
// db.Rebind on the result before Query/Exec so the placeholders match the
// driver.
//
// An empty or nil slice expands to the SQL literal NULL and binds no
// argument, so `x IN (?)` becomes `x IN (NULL)`, which matches no row on any
// database. Note that `x NOT IN (NULL)` also matches no row (SQL three-valued
// logic): if an empty list must mean "every row", build that branch of the
// query yourself.
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
empty bool // a slice of length 0, rewritten to the literal NULL
}

var flatArgsCount int
Expand Down Expand Up @@ -180,10 +187,7 @@ func In(query string, args ...interface{}) (string, []interface{}, error) {

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

if meta[i].length == 0 {
return "", nil, errors.New("empty slice passed to 'in' query")
}
meta[i].empty = meta[i].length == 0
} else {
meta[i].i = arg
flatArgsCount++
Expand Down Expand Up @@ -215,6 +219,15 @@ func In(query string, args ...interface{}) (string, []interface{}, error) {
argMeta := meta[arg]
arg++

// an empty slice becomes the literal NULL, which binds nothing
if argMeta.empty {
buf.WriteString(query[:offset+i])
buf.WriteString("NULL")
query = query[offset+i+1:]
offset = 0
continue
}

// not a slice, continue.
// our questionmark will either be written before the next expansion
// of a slice or after the loop when writing the rest of the query
Expand Down
91 changes: 91 additions & 0 deletions in_empty_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package sqlx

import (
"database/sql/driver"
"reflect"
"strings"
"testing"
)

type emptySliceValuer struct{}

func (emptySliceValuer) Value() (driver.Value, error) { return []int{}, nil }

// An empty slice expands to NULL and binds nothing, after IN and NOT IN alike
// (upstream #945).
func TestInEmptySlice(t *testing.T) {
tests := []struct {
name string
query string
args []interface{}
wantQ string
wantArgs []interface{}
wantErr string
}{
{"empty", "SELECT * FROM t WHERE x IN (?)", []interface{}{[]int{}}, "SELECT * FROM t WHERE x IN (NULL)", []interface{}{}, ""},
{"nil slice", "SELECT * FROM t WHERE x IN (?)", []interface{}{[]int(nil)}, "SELECT * FROM t WHERE x IN (NULL)", []interface{}{}, ""},
{"scalar before", "SELECT * FROM t WHERE a = ? AND x IN (?)", []interface{}{1, []int{}}, "SELECT * FROM t WHERE a = ? AND x IN (NULL)", []interface{}{1}, ""},
{"scalar after", "SELECT * FROM t WHERE x IN (?) AND a = ?", []interface{}{[]int{}, 1}, "SELECT * FROM t WHERE x IN (NULL) AND a = ?", []interface{}{1}, ""},
{"empty then full", "SELECT * FROM t WHERE x IN (?) AND y IN (?)", []interface{}{[]int{}, []int{1, 2}}, "SELECT * FROM t WHERE x IN (NULL) AND y IN (?, ?)", []interface{}{1, 2}, ""},
{"full then empty", "SELECT * FROM t WHERE x IN (?) AND y IN (?)", []interface{}{[]int{1, 2}, []int{}}, "SELECT * FROM t WHERE x IN (?, ?) AND y IN (NULL)", []interface{}{1, 2}, ""},
{"two empty", "SELECT * FROM t WHERE x IN (?) AND y IN (?)", []interface{}{[]int{}, []string{}}, "SELECT * FROM t WHERE x IN (NULL) AND y IN (NULL)", []interface{}{}, ""},
{"no space", "SELECT * FROM t WHERE x IN(?)", []interface{}{[]int{}}, "SELECT * FROM t WHERE x IN(NULL)", []interface{}{}, ""},
{"inner spaces", "SELECT * FROM t WHERE x IN ( ? )", []interface{}{[]int{}}, "SELECT * FROM t WHERE x IN ( NULL )", []interface{}{}, ""},
{"empty interface slice", "SELECT * FROM t WHERE x IN (?)", []interface{}{[]interface{}{}}, "SELECT * FROM t WHERE x IN (NULL)", []interface{}{}, ""},
{"empty bytes are a value", "SELECT * FROM t WHERE x IN (?)", []interface{}{[]byte{}}, "SELECT * FROM t WHERE x IN (?)", []interface{}{[]byte{}}, ""},
{"valuer returning an empty slice", "SELECT * FROM t WHERE x IN (?)", []interface{}{emptySliceValuer{}}, "SELECT * FROM t WHERE x IN (NULL)", []interface{}{}, ""},
{"too few args", "SELECT * FROM t WHERE x IN (?) AND y = ?", []interface{}{[]int{}}, "", nil, "number of bindVars exceeds arguments"},
{"too many args", "SELECT * FROM t WHERE x IN (?)", []interface{}{[]int{}, 1}, "", nil, "number of bindVars less than number arguments"},
{"not in", "SELECT * FROM t WHERE x NOT IN (?)", []interface{}{[]string{}}, "SELECT * FROM t WHERE x NOT IN (NULL)", []interface{}{}, ""},
{"not in with a scalar", "DELETE FROM server_config WHERE server_id = ? AND config_id not in(?)", []interface{}{7, []int{}}, "DELETE FROM server_config WHERE server_id = ? AND config_id not in(NULL)", []interface{}{7}, ""},
{"not in with a full slice", "SELECT * FROM t WHERE x NOT IN (?)", []interface{}{[]int{1}}, "SELECT * FROM t WHERE x NOT IN (?)", []interface{}{1}, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
q, args, err := In(tt.query, tt.args...)
if tt.wantErr != "" {
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("err = %v, want one containing %q", err, tt.wantErr)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if q != tt.wantQ {
t.Errorf("query = %q, want %q", q, tt.wantQ)
}
if !reflect.DeepEqual(args, tt.wantArgs) {
t.Errorf("args = %#v, want %#v", args, tt.wantArgs)
}
})
}
}

func TestInEmptySliceAgainstDatabase(t *testing.T) {
RunWithSchema(defaultSchema, t, func(db *DB, t *testing.T, now string) {
loadDefaultFixture(db, t)
for _, tt := range []struct {
query string
args []interface{}
}{
{"SELECT * FROM place WHERE telcode IN (?)", []interface{}{[]int{}}},
{"SELECT * FROM place WHERE country = ? AND telcode IN (?)", []interface{}{"Hong Kong", []int{}}},
{"SELECT * FROM place WHERE telcode IN (?) AND country = ?", []interface{}{[]int{}, "Hong Kong"}},
// NOT IN (NULL) matches no row either; this documents the caveat.
{"SELECT * FROM place WHERE telcode NOT IN (?)", []interface{}{[]int{}}},
} {
q, args, err := In(tt.query, tt.args...)
if err != nil {
t.Fatalf("%s: %v", tt.query, err)
}
var places []Place
if err := db.Select(&places, db.Rebind(q), args...); err != nil {
t.Fatalf("%s: %v", tt.query, err)
}
if len(places) != 0 {
t.Errorf("%s: got %d rows, want none", tt.query, len(places))
}
}
})
}
4 changes: 0 additions & 4 deletions sqlx_context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1277,10 +1277,6 @@ func TestInContext(t *testing.T) {
{"SELECT * FROM foo WHERE x = ? and y = ?",
[]interface{}{"foo", []int{1, 2, 3}, "bar"},
0},
// empty slice, should return error before parse
{"SELECT * FROM foo WHERE x = ?",
[]interface{}{[]int{}},
0},
// too *few* bindvars, should return an error
{"SELECT * FROM foo WHERE x = ? AND y in (?)",
[]interface{}{[]int{1, 2, 3}},
Expand Down
4 changes: 0 additions & 4 deletions sqlx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1586,10 +1586,6 @@ func TestIn(t *testing.T) {
{"SELECT * FROM foo WHERE x = ? and y = ?",
[]interface{}{"foo", []int{1, 2, 3}, "bar"},
0},
// empty slice, should return error before parse
{"SELECT * FROM foo WHERE x = ?",
[]interface{}{[]int{}},
0},
// too *few* bindvars, should return an error
{"SELECT * FROM foo WHERE x = ? AND y in (?)",
[]interface{}{[]int{1, 2, 3}},
Expand Down
Loading