From 603339ca55a4c7656eee530508abc61d86b96d7e Mon Sep 17 00:00:00 2001 From: August Vi Date: Sat, 10 Oct 2026 21:16:43 -0700 Subject: [PATCH] Expand an empty slice in In to NULL instead of returning an error. --- bind.go | 21 ++++++++-- in_empty_test.go | 91 ++++++++++++++++++++++++++++++++++++++++++++ sqlx_context_test.go | 4 -- sqlx_test.go | 4 -- 4 files changed, 108 insertions(+), 12 deletions(-) create mode 100644 in_empty_test.go diff --git a/bind.go b/bind.go index 30073c3..1818037 100644 --- a/bind.go +++ b/bind.go @@ -144,6 +144,12 @@ 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 @@ -151,6 +157,7 @@ func In(query string, args ...interface{}) (string, []interface{}, error) { v reflect.Value i interface{} length int + empty bool // a slice of length 0, rewritten to the literal NULL } var flatArgsCount int @@ -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++ @@ -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 diff --git a/in_empty_test.go b/in_empty_test.go new file mode 100644 index 0000000..06771da --- /dev/null +++ b/in_empty_test.go @@ -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)) + } + } + }) +} diff --git a/sqlx_context_test.go b/sqlx_context_test.go index 30126a0..1e922c9 100644 --- a/sqlx_context_test.go +++ b/sqlx_context_test.go @@ -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}}, diff --git a/sqlx_test.go b/sqlx_test.go index d3e54e5..804439d 100644 --- a/sqlx_test.go +++ b/sqlx_test.go @@ -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}},