From 8622ee19eeb08a3beb53cc9e67c45c38e6e19f26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= Date: Wed, 10 Jun 2026 22:10:36 -0300 Subject: [PATCH 1/2] dbutil: rewrite query placeholders without regexp --- dbutil/database.go | 36 +++++++++++++++-- dbutil/database_test.go | 90 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 dbutil/database_test.go diff --git a/dbutil/database.go b/dbutil/database.go index a9960ce..3008335 100644 --- a/dbutil/database.go +++ b/dbutil/database.go @@ -11,7 +11,6 @@ import ( "database/sql" "fmt" "net/url" - "regexp" "strings" "time" @@ -126,12 +125,43 @@ type Database struct { var ForceDeadlockDetection bool -var positionalParamPattern = regexp.MustCompile(`\$(\d+)`) +func isASCIIDigit(c byte) bool { + return c >= '0' && c <= '9' +} + +// replacePositionalParams rewrites $1-style placeholders into SQLite's ?1 +// form, equivalent to regexp `\$(\d+)` -> "?$1". Hand-rolled because it +// runs on every query and the regexp engine cost adds up. +func replacePositionalParams(query string) string { + dollar := strings.IndexByte(query, '$') + for dollar != -1 && (dollar == len(query)-1 || !isASCIIDigit(query[dollar+1])) { + next := strings.IndexByte(query[dollar+1:], '$') + if next == -1 { + dollar = -1 + } else { + dollar += 1 + next + } + } + if dollar == -1 { + return query + } + var out strings.Builder + out.Grow(len(query)) + out.WriteString(query[:dollar]) + for i := dollar; i < len(query); i++ { + if query[i] == '$' && i+1 < len(query) && isASCIIDigit(query[i+1]) { + out.WriteByte('?') + } else { + out.WriteByte(query[i]) + } + } + return out.String() +} func (db *Database) mutateQuery(query string) string { switch db.Dialect { case SQLite: - return positionalParamPattern.ReplaceAllString(query, "?$1") + return replacePositionalParams(query) default: return query } diff --git a/dbutil/database_test.go b/dbutil/database_test.go new file mode 100644 index 0000000..e9c1200 --- /dev/null +++ b/dbutil/database_test.go @@ -0,0 +1,90 @@ +package dbutil + +import ( + "regexp" + "testing" +) + +var positionalParamRegex = regexp.MustCompile(`\$(\d+)`) + +func regexReplacePositionalParams(query string) string { + return positionalParamRegex.ReplaceAllString(query, "?$1") +} + +func TestReplacePositionalParams(t *testing.T) { + cases := []string{ + "", + "$", + "$1", + "$12", + "a$", + "$a", + "$$1", + "$1$2$3", + "$ 1", + "no placeholders at all", + "trailing dollar $", + "SELECT session FROM whatsmeow_sessions WHERE our_jid=$1 AND their_id=$2", + "INSERT INTO whatsmeow_sessions (our_jid, their_id, session) VALUES ($1, $2, $3) ON CONFLICT (our_jid, their_id) DO UPDATE SET session=excluded.session", + "SELECT their_id, session FROM whatsmeow_sessions WHERE our_jid=$1 AND their_id IN ($2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)", + "WHERE a=$2 AND b=$1", + "cost: US$50 AND id=$7", + "weird $0 but valid", + "unicode é$1ç $2ü", + "$999999999999", + } + for _, query := range cases { + expected := regexReplacePositionalParams(query) + actual := replacePositionalParams(query) + if actual != expected { + t.Errorf("mismatch for %q:\nregexp: %q\nscanner: %q", query, expected, actual) + } + } +} + +// Generated coverage: every 3-character combination of a small relevant +// alphabet must behave identically to the regexp it replaces. +func TestReplacePositionalParamsExhaustive(t *testing.T) { + alphabet := []byte{'$', '1', '0', 'a', ' ', '?'} + var build func(prefix string, depth int) + build = func(prefix string, depth int) { + expected := regexReplacePositionalParams(prefix) + actual := replacePositionalParams(prefix) + if actual != expected { + t.Errorf("mismatch for %q: regexp=%q scanner=%q", prefix, expected, actual) + } + if depth == 0 { + return + } + for _, c := range alphabet { + build(prefix+string(c), depth-1) + } + } + build("", 4) +} + +func TestReplacePositionalParamsNoAllocWithoutPlaceholders(t *testing.T) { + query := "SELECT * FROM table WHERE nothing" + allocs := testing.AllocsPerRun(100, func() { + _ = replacePositionalParams(query) + }) + if allocs != 0 { + t.Errorf("expected 0 allocations for query without placeholders, got %.1f", allocs) + } +} + +func BenchmarkReplacePositionalParams(b *testing.B) { + query := "INSERT INTO whatsmeow_sessions (our_jid, their_id, session) VALUES ($1, $2, $3) ON CONFLICT (our_jid, their_id) DO UPDATE SET session=excluded.session" + b.Run("scanner", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = replacePositionalParams(query) + } + }) + b.Run("regexp", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = regexReplacePositionalParams(query) + } + }) +} From deb0a4b1a2c8d0c2b870a2c7d8ec86e5709548b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= Date: Thu, 11 Jun 2026 08:40:05 -0300 Subject: [PATCH 2/2] dbutil: remove unnecessary non-placeholder skip loop --- dbutil/database.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/dbutil/database.go b/dbutil/database.go index 3008335..3e85231 100644 --- a/dbutil/database.go +++ b/dbutil/database.go @@ -134,14 +134,6 @@ func isASCIIDigit(c byte) bool { // runs on every query and the regexp engine cost adds up. func replacePositionalParams(query string) string { dollar := strings.IndexByte(query, '$') - for dollar != -1 && (dollar == len(query)-1 || !isASCIIDigit(query[dollar+1])) { - next := strings.IndexByte(query[dollar+1:], '$') - if next == -1 { - dollar = -1 - } else { - dollar += 1 + next - } - } if dollar == -1 { return query }