snapshot of https://github.com/sqlc-dev/sqlc.git (main, 2026-09-16; blobs > 131072B omitted)
Some checks failed
kotlin / test (push) Has been skipped
python / test (push) Has been skipped
typescript / test (push) Has been skipped
go / build darwin/amd64 (push) Has been cancelled
go / build linux/amd64 (push) Has been cancelled
go / build windows/amd64 (push) Has been cancelled
go / build darwin/arm64 (push) Has been cancelled
go / build linux/arm64 (push) Has been cancelled
go / build windows/arm64 (push) Has been cancelled
go / test (push) Has been cancelled
go / vuln_check (push) Has been cancelled
Some checks failed
kotlin / test (push) Has been skipped
python / test (push) Has been skipped
typescript / test (push) Has been skipped
go / build darwin/amd64 (push) Has been cancelled
go / build linux/amd64 (push) Has been cancelled
go / build windows/amd64 (push) Has been cancelled
go / build darwin/arm64 (push) Has been cancelled
go / build linux/arm64 (push) Has been cancelled
go / build windows/arm64 (push) Has been cancelled
go / test (push) Has been cancelled
go / vuln_check (push) Has been cancelled
This commit is contained in:
183
internal/goldeneye/endtoend/query.go
Normal file
183
internal/goldeneye/endtoend/query.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package endtoend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// Query is one entry of a sqlc query file.
|
||||
type Query struct {
|
||||
Name string
|
||||
Cmd string
|
||||
SQL string
|
||||
}
|
||||
|
||||
// Queries reads the case's query file and splits it into its queries.
|
||||
func (c Case) Queries() ([]Query, error) {
|
||||
src, err := os.ReadFile(c.Query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
queries, err := ParseQueries(string(src))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", c.Query, err)
|
||||
}
|
||||
return queries, nil
|
||||
}
|
||||
|
||||
var headerRe = regexp.MustCompile(`^\s*--\s*name:\s*(\S+)\s+(:\S+)\s*$`)
|
||||
|
||||
// ParseQueries splits a sqlc query file on its `-- name: X :cmd` headers.
|
||||
func ParseQueries(src string) ([]Query, error) {
|
||||
var queries []Query
|
||||
var cur *Query
|
||||
var body []string
|
||||
flush := func() error {
|
||||
if cur == nil {
|
||||
return nil
|
||||
}
|
||||
sql := strings.TrimSpace(strings.Join(body, "\n"))
|
||||
sql = strings.TrimRight(sql, "; \t\r\n")
|
||||
if sql == "" {
|
||||
return fmt.Errorf("query %s has no body", cur.Name)
|
||||
}
|
||||
cur.SQL = sql
|
||||
queries = append(queries, *cur)
|
||||
return nil
|
||||
}
|
||||
for _, line := range strings.Split(src, "\n") {
|
||||
if m := headerRe.FindStringSubmatch(line); m != nil {
|
||||
if err := flush(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cur = &Query{Name: m[1], Cmd: m[2]}
|
||||
body = body[:0]
|
||||
continue
|
||||
}
|
||||
if cur != nil {
|
||||
body = append(body, line)
|
||||
}
|
||||
}
|
||||
if err := flush(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(queries) == 0 {
|
||||
return nil, fmt.Errorf("no queries found: expected `-- name: Name :cmd` headers")
|
||||
}
|
||||
return queries, nil
|
||||
}
|
||||
|
||||
var namedArgRe = regexp.MustCompile(`^sqlc\.(n?arg|slice)\(\s*'?([A-Za-z_][A-Za-z0-9_]*)'?\s*\)`)
|
||||
|
||||
// typedParamRe matches ClickHouse's {name:Type} parameter, whose type is
|
||||
// the query's own business: the engine binds it as it binds any other.
|
||||
var typedParamRe = regexp.MustCompile(`^\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*:\s*[^}]+\}`)
|
||||
|
||||
// Rewrite replaces every parameter reference in a query — ?, sqlc.arg(name),
|
||||
// sqlc.narg(name), sqlc.slice(name) and ClickHouse's {name:Type} — with
|
||||
// what bind returns for it, in order of appearance, skipping string
|
||||
// literals, quoted identifiers and comments. bind is handed the name, empty for a ?, and the word before the
|
||||
// reference, so that a LIMIT or OFFSET can be bound differently from a
|
||||
// value; the second count of a LIMIT ?, ? is handed LIMIT as well. Each
|
||||
// engine decides what a reference becomes and how the references are
|
||||
// numbered.
|
||||
func Rewrite(sql string, bind func(name, lastWord string) string) string {
|
||||
var (
|
||||
out strings.Builder
|
||||
lastWord string
|
||||
i = 0
|
||||
)
|
||||
for i < len(sql) {
|
||||
c := sql[i]
|
||||
switch {
|
||||
case c == '\'' || c == '"' || c == '`':
|
||||
end := skipQuoted(sql, i)
|
||||
out.WriteString(sql[i:end])
|
||||
i = end
|
||||
case strings.HasPrefix(sql[i:], "--"):
|
||||
end := strings.IndexByte(sql[i:], '\n')
|
||||
if end < 0 {
|
||||
end = len(sql)
|
||||
} else {
|
||||
end += i
|
||||
}
|
||||
out.WriteString(sql[i:end])
|
||||
i = end
|
||||
case strings.HasPrefix(sql[i:], "/*"):
|
||||
end := strings.Index(sql[i:], "*/")
|
||||
if end < 0 {
|
||||
end = len(sql)
|
||||
} else {
|
||||
end += i + 2
|
||||
}
|
||||
out.WriteString(sql[i:end])
|
||||
i = end
|
||||
case c == '?':
|
||||
out.WriteString(bind("", lastWord))
|
||||
lastWord = afterReference(lastWord)
|
||||
i++
|
||||
case c == 's' && namedArgRe.MatchString(sql[i:]):
|
||||
m := namedArgRe.FindStringSubmatch(sql[i:])
|
||||
out.WriteString(bind(m[2], lastWord))
|
||||
lastWord = afterReference(lastWord)
|
||||
i += len(m[0])
|
||||
case c == '{' && typedParamRe.MatchString(sql[i:]):
|
||||
m := typedParamRe.FindStringSubmatch(sql[i:])
|
||||
out.WriteString(bind(m[1], lastWord))
|
||||
lastWord = afterReference(lastWord)
|
||||
i += len(m[0])
|
||||
case isWordByte(c):
|
||||
end := i
|
||||
for end < len(sql) && isWordByte(sql[end]) {
|
||||
end++
|
||||
}
|
||||
lastWord = sql[i:end]
|
||||
out.WriteString(lastWord)
|
||||
i = end
|
||||
default:
|
||||
if !unicode.IsSpace(rune(c)) && c != ',' && c != '(' {
|
||||
lastWord = ""
|
||||
}
|
||||
out.WriteByte(c)
|
||||
i++
|
||||
}
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
// afterReference is the word the reference after one is preceded by: a
|
||||
// LIMIT's, so that both counts of LIMIT ?, ? are bound as counts, and
|
||||
// otherwise none.
|
||||
func afterReference(lastWord string) string {
|
||||
if strings.EqualFold(lastWord, "limit") {
|
||||
return lastWord
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isWordByte(c byte) bool {
|
||||
return c == '_' || c == '.' || c >= '0' && c <= '9' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'
|
||||
}
|
||||
|
||||
// skipQuoted returns the index just past the quoted token starting at i,
|
||||
// honouring backslash escapes and doubled quotes.
|
||||
func skipQuoted(s string, i int) int {
|
||||
q := s[i]
|
||||
j := i + 1
|
||||
for j < len(s) {
|
||||
switch {
|
||||
case s[j] == '\\' && j+1 < len(s):
|
||||
j += 2
|
||||
case s[j] == q && j+1 < len(s) && s[j+1] == q:
|
||||
j += 2
|
||||
case s[j] == q:
|
||||
return j + 1
|
||||
default:
|
||||
j++
|
||||
}
|
||||
}
|
||||
return len(s)
|
||||
}
|
||||
Reference in New Issue
Block a user