From b638d7d6422a6f392914f1dac054284171e6d7a3 Mon Sep 17 00:00:00 2001 From: Caleb Spare Date: Fri, 31 Jan 2025 00:24:47 -0800 Subject: [PATCH 1/2] Relax constraint on VALUE clause expansion This lets it be used in non-INSERT scenarios as well. See the test for such an example. --- named.go | 2 +- named_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/named.go b/named.go index 6ac4477..af527aa 100644 --- a/named.go +++ b/named.go @@ -224,7 +224,7 @@ func bindStruct(bindType int, query string, arg interface{}, m *reflectx.Mapper) return bound, arglist, nil } -var valuesReg = regexp.MustCompile(`\)\s*(?i)VALUES\s*\(`) +var valuesReg = regexp.MustCompile(`(?i)\WVALUES\s*\(`) func findMatchingClosingBracketIndex(s string) int { count := 0 diff --git a/named_test.go b/named_test.go index 0ee5b85..c33b9bc 100644 --- a/named_test.go +++ b/named_test.go @@ -422,6 +422,30 @@ func TestFixBounds(t *testing.T) { )`, loop: 2, }, + { + name: "use of VALUES not for insert", + query: ` +UPDATE t +SET + foo = u.foo, + bar = u.bar +FROM ( + VALUES (:id, :foo, :bar) +) AS u(id, foo, bar) +WHERE t.id = u.id +`, + expect: ` +UPDATE t +SET + foo = u.foo, + bar = u.bar +FROM ( + VALUES (:id, :foo, :bar),(:id, :foo, :bar) +) AS u(id, foo, bar) +WHERE t.id = u.id +`, + loop: 2, + }, } for _, tc := range table { From 077d7507766a3281bb45eba13771812bfa62ebd8 Mon Sep 17 00:00:00 2001 From: August Vi Date: Sat, 10 Oct 2026 20:55:29 -0700 Subject: [PATCH 2/2] Expand the first VALUES group that contains a bind variable and add the cases from upstream #754, #811 and #948. --- named.go | 25 ++++++++++++++++++++++--- named_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/named.go b/named.go index 9d1ce37..29d6dcf 100644 --- a/named.go +++ b/named.go @@ -19,6 +19,7 @@ import ( "reflect" "regexp" "strconv" + "strings" "unicode" "github.com/secondwindoss/sqlx/reflectx" @@ -242,14 +243,32 @@ func findMatchingClosingBracketIndex(s string) int { return 0 } +// fixBound repeats the VALUES group of a batch insert loop-1 more times. +// The group chosen is the first one that contains a bind variable, so that a +// table named "values" or MySQL's VALUES(col) in an ON DUPLICATE KEY UPDATE +// clause is not mistaken for it. When no group contains a bind variable the +// first match is used, as before. func fixBound(bound string, loop int) string { - loc := valuesReg.FindStringIndex(bound) + locs := valuesReg.FindAllStringIndex(bound, -1) // defensive guard when "VALUES (...)" not found - if len(loc) < 2 { + if len(locs) == 0 { return bound } - openingBracketIndex := loc[1] - 1 + chosen := 0 + for i, loc := range locs { + openingBracketIndex := loc[1] - 1 + index := findMatchingClosingBracketIndex(bound[openingBracketIndex:]) + if index == 0 { + continue + } + if strings.ContainsAny(bound[openingBracketIndex:openingBracketIndex+index+1], "?$:@") { + chosen = i + break + } + } + + openingBracketIndex := locs[chosen][1] - 1 index := findMatchingClosingBracketIndex(bound[openingBracketIndex:]) // defensive guard. must have closing bracket if index == 0 { diff --git a/named_test.go b/named_test.go index c33b9bc..b549539 100644 --- a/named_test.go +++ b/named_test.go @@ -446,6 +446,36 @@ WHERE t.id = u.id `, loop: 2, }, + { + name: "VALUES in a JOIN (upstream #754)", + query: `SELECT t_1.code, t_2.name FROM table_1 t_1 INNER JOIN (VALUES (:v_code, :v_name)) t_2 (code, name) ON t_1.code = t_2.code`, + expect: `SELECT t_1.code, t_2.name FROM table_1 t_1 INNER JOIN (VALUES (:v_code, :v_name),(:v_code, :v_name)) t_2 (code, name) ON t_1.code = t_2.code`, + loop: 2, + }, + { + name: "INSERT SELECT from VALUES with a trailing comment (upstream #754)", + query: "INSERT INTO table_1 SELECT * FROM (VALUES (:v_code, 18, 'const')) --- comment\n t_2 (code, age, last);", + expect: "INSERT INTO table_1 SELECT * FROM (VALUES (:v_code, 18, 'const'),(:v_code, 18, 'const')) --- comment\n t_2 (code, age, last);", + loop: 2, + }, + { + name: "table named values (upstream #811)", + query: `INSERT INTO values (a, b) VALUES (:a, :b)`, + expect: `INSERT INTO values (a, b) VALUES (:a, :b),(:a, :b)`, + loop: 2, + }, + { + name: "SELECT from a VALUES list (upstream #811)", + query: `SELECT * FROM (VALUES (:a, :b))`, + expect: `SELECT * FROM (VALUES (:a, :b),(:a, :b))`, + loop: 2, + }, + { + name: "ClickHouse SETTINGS before VALUES (upstream #948)", + query: `INSERT INTO foo (a,b) settings async_insert=1, wait_for_async_insert=1 VALUES(:a, :b)`, + expect: `INSERT INTO foo (a,b) settings async_insert=1, wait_for_async_insert=1 VALUES(:a, :b),(:a, :b)`, + loop: 2, + }, } for _, tc := range table {