fix: contain generated files and assets and harden SQL rendering

This commit is contained in:
2026-10-12 20:59:46 +02:00
parent 2fa48335d3
commit dd97413b8e
68 changed files with 2328 additions and 798 deletions
+101
View File
@@ -0,0 +1,101 @@
// Package sqllex recognizes PostgreSQL lexical boundaries without parsing SQL.
package sqllex
import "strings"
func start(c byte) bool { return c >= 128 || c == '_' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' }
func ident(c byte) bool { return start(c) || c >= '0' && c <= '9' || c >= 128 }
// Walk visits named parameters and standalone line comments in executable SQL. Quoted
// strings/identifiers, dollar bodies, casts and nested block comments are opaque.
// Returning replacement text is literal: it is never scanned again.
func Walk(sql string, parameter func(string) string, lineComment func(string) string) string {
var out strings.Builder
for i := 0; i < len(sql); {
begin := i
switch {
case strings.HasPrefix(sql[i:], "--"):
i += 2
for i < len(sql) && sql[i] != '\n' && sql[i] != '\r' {
i++
}
text := sql[begin:i]
if lineComment != nil && strings.TrimSpace(sql[strings.LastIndexAny(sql[:begin], "\r\n")+1:begin]) == "" {
text = lineComment(text)
}
out.WriteString(text)
continue
case strings.HasPrefix(sql[i:], "/*"):
i += 2
depth := 1
for i < len(sql) && depth > 0 {
if strings.HasPrefix(sql[i:], "/*") {
depth++
i += 2
} else if strings.HasPrefix(sql[i:], "*/") {
depth--
i += 2
} else {
i++
}
}
case sql[i] == '\'' || sql[i] == '"':
q := sql[i]
escapes := q == '\'' && i > 0 && (sql[i-1] == 'E' || sql[i-1] == 'e') && (i == 1 || !ident(sql[i-2]))
i++
for i < len(sql) {
if escapes && sql[i] == '\\' {
i++
if i < len(sql) {
i++
}
continue
}
if sql[i] == q {
i++
if i < len(sql) && sql[i] == q {
i++
continue
}
break
}
i++
}
case sql[i] == '$' && (i == 0 || !ident(sql[i-1]) && sql[i-1] != '$'):
j := i + 1
if j < len(sql) && start(sql[j]) {
for j < len(sql) && ident(sql[j]) {
j++
}
}
if j < len(sql) && sql[j] == '$' {
tag := sql[i : j+1]
end := strings.Index(sql[j+1:], tag)
if end < 0 {
i = len(sql)
} else {
i = j + 1 + end + len(tag)
}
} else {
i++
}
case strings.HasPrefix(sql[i:], "::"):
i += 2
case sql[i] == ':' && i+1 < len(sql) && start(sql[i+1]) && (i == 0 || !ident(sql[i-1]) && sql[i-1] != ':'):
i += 2
for i < len(sql) && ident(sql[i]) {
i++
}
text := sql[begin:i]
if parameter != nil {
text = parameter(text)
}
out.WriteString(text)
continue
default:
i++
}
out.WriteString(sql[begin:i])
}
return out.String()
}
+28
View File
@@ -0,0 +1,28 @@
package sqllex
import (
"strings"
"testing"
)
func TestWalkSQLContexts(t *testing.T) {
sql := `SELECT :x,:x, :x::text, ':hidden', 'it'':hidden', E'it\':hidden', ":hidden", $$:hidden$$, $名$:hidden$名$;
-- :hidden
/* :hidden /* :hidden */ :hidden */ SELECT :y, value::integer, x := 1;`
var names []string
got := Walk(sql, func(name string) string { names = append(names, name); return "$1 ${2} :inserted" }, nil)
if strings.Join(names, ",") != ":x,:x,:x,:y" {
t.Fatalf("parameters: %v", names)
}
if strings.Count(got, "$1 ${2} :inserted") != 4 || !strings.Contains(got, "value::integer") {
t.Fatalf("result: %s", got)
}
}
func TestUnterminatedSQLContextsAreOpaque(t *testing.T) {
for _, sql := range []string{"SELECT ':x", `SELECT ":x`, "DO $tag$:x", "/* :x", "-- :x"} {
if got := Walk(sql, func(string) string { t.Fatal("unexpected parameter"); return "" }, nil); got != sql {
t.Fatalf("changed %q", sql)
}
}
}