Skip to content

Commit ff55507

Browse files
authored
Fix template suffix logic to handle ON DUPLICATE/ON CONFLICT clauses (#10)
- Refactor buildBulkInsertQuery to detect suffix clauses before parsing VALUES - Prevent VALUES() function calls in suffix from being incorrectly matched - Add support for ON DUPLICATE KEY UPDATE and ON CONFLICT clauses - Add test cases for upsert queries with ON DUPLICATE and ON CONFLICT - Use range-based iteration for numArgs loop
1 parent f4b1f4b commit ff55507

File tree

2 files changed

+68
-20
lines changed

2 files changed

+68
-20
lines changed

templates/template.go

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,37 @@ func buildBulkInsertQuery(originalQuery string, numArgs int, numParamsPerArg int
5858
trimmedQuery = trimmedQuery[:len(trimmedQuery)-1]
5959
}
6060

61-
// search "VALUES" (case insensitive)
62-
valuesUpperIndex := strings.LastIndex(strings.ToUpper(trimmedQuery), "VALUES")
61+
// Find the start of the suffix (e.g., "ON DUPLICATE", "ON CONFLICT")
62+
// We must do this *before* searching for "VALUES" to avoid matching "VALUES()"
63+
// functions inside the suffix.
64+
var querySuffixStr string
65+
var queryWithoutSuffix string
66+
67+
// Use LastIndex to find the main clause
68+
onDuplicateUpperIndex := strings.LastIndex(strings.ToUpper(trimmedQuery), "ON DUPLICATE KEY UPDATE")
69+
onConflictUpperIndex := strings.LastIndex(strings.ToUpper(trimmedQuery), "ON CONFLICT")
70+
71+
// Find the earliest starting position of any suffix keyword
72+
suffixBoundary := len(trimmedQuery)
73+
if onDuplicateUpperIndex != -1 {
74+
suffixBoundary = onDuplicateUpperIndex
75+
}
76+
if onConflictUpperIndex != -1 && onConflictUpperIndex < suffixBoundary {
77+
suffixBoundary = onConflictUpperIndex
78+
}
79+
80+
if suffixBoundary < len(trimmedQuery) {
81+
// Suffix found
82+
queryWithoutSuffix = trimmedQuery[:suffixBoundary]
83+
querySuffixStr = " " + strings.TrimSpace(trimmedQuery[suffixBoundary:])
84+
} else {
85+
// No suffix
86+
queryWithoutSuffix = trimmedQuery
87+
querySuffixStr = ""
88+
}
89+
90+
// search "VALUES" (case insensitive) in the part before the suffix
91+
valuesUpperIndex := strings.LastIndex(strings.ToUpper(queryWithoutSuffix), "VALUES")
6392
if valuesUpperIndex == -1 {
6493
return "", fmt.Errorf("invalid query format: VALUES clause not found in original query: %s", originalQuery)
6594
}
@@ -69,28 +98,11 @@ func buildBulkInsertQuery(originalQuery string, numArgs int, numParamsPerArg int
6998
// Add "VALUES" to this
7099
queryPrefixStr := strings.TrimSpace(trimmedQuery[:valuesUpperIndex]) + " VALUES "
71100

72-
// Find the suffix of the query (e.g., "ON DUPLICATE KEY UPDATE ...")
73-
// The suffix starts after the original placeholder, e.g., after `VALUES (?, ?)`
74-
var querySuffixStr string
75-
// Start searching after the "VALUES" keyword
76-
searchStartIndex := valuesUpperIndex + len("VALUES")
77-
// Find the first closing parenthesis ')' after the "VALUES" clause
78-
firstClosingParenIndex := strings.Index(trimmedQuery[searchStartIndex:], ")")
79-
80-
if firstClosingParenIndex != -1 {
81-
// The suffix is the part of the string after the closing parenthesis.
82-
// We add 1 to skip the ')' character itself.
83-
suffixStartIndex := searchStartIndex + firstClosingParenIndex + 1
84-
if suffixStartIndex < len(trimmedQuery) {
85-
querySuffixStr = " " + strings.TrimSpace(trimmedQuery[suffixStartIndex:])
86-
}
87-
}
88-
89101
var queryBuilder strings.Builder
90102
queryBuilder.WriteString(queryPrefixStr)
91103

92104
valueStrings := make([]string, numArgs)
93-
for i := 0; i < numArgs; i++ {
105+
for i := range numArgs {
94106
placeholders := make([]string, numParamsPerArg)
95107
for j := 0; j < numParamsPerArg; j++ {
96108
placeholders[j] = "?"

templates/template_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,42 @@ func TestBuildBulkInsertQuery(t *testing.T) {
149149
}
150150
},
151151
},
152+
"valid:upsert": {
153+
arrange: func(t *testing.T) (Args, Expected) {
154+
return Args{
155+
originalQuery: "INSERT INTO users (id, name) VALUES (?, ?) ON DUPLICATE KEY UPDATE id = VALUES(id), name = VALUES(name);",
156+
numArgs: 3,
157+
numParamsPerArg: 2,
158+
}, Expected{
159+
query: "INSERT INTO users (id, name) VALUES (?,?),(?,?),(?,?) ON DUPLICATE KEY UPDATE id = VALUES(id), name = VALUES(name)",
160+
err: nil,
161+
}
162+
},
163+
},
164+
"valid:upsert (ON CONFLICT)": {
165+
arrange: func(t *testing.T) (Args, Expected) {
166+
return Args{
167+
originalQuery: "INSERT INTO users (id, name) VALUES (?, ?) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;",
168+
numArgs: 2,
169+
numParamsPerArg: 2,
170+
}, Expected{
171+
query: "INSERT INTO users (id, name) VALUES (?,?),(?,?) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name",
172+
err: nil,
173+
}
174+
},
175+
},
176+
"valid:upsert (ON CONFLICT) case-insensitive": {
177+
arrange: func(t *testing.T) (Args, Expected) {
178+
return Args{
179+
originalQuery: "insert into users (id, name) values (?, ?) on conflict (id) do nothing;",
180+
numArgs: 2,
181+
numParamsPerArg: 2,
182+
}, Expected{
183+
query: "insert into users (id, name) VALUES (?,?),(?,?) on conflict (id) do nothing",
184+
err: nil,
185+
}
186+
},
187+
},
152188
"valid:Squeeze spaces": {
153189
arrange: func(t *testing.T) (Args, Expected) {
154190
return Args{

0 commit comments

Comments
 (0)