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:
114
internal/sql/preprocess/CLAUDE.md
Normal file
114
internal/sql/preprocess/CLAUDE.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# SQL Preprocess Package - Claude Code Guide
|
||||
|
||||
This package rewrites sqlc's query syntax into native SQL **before** any engine
|
||||
parser sees a query. After it runs, no sqlc syntax remains in the text, so
|
||||
parsers, converters and `astutils` traversals only ever deal with SQL.
|
||||
|
||||
## What it rewrites
|
||||
|
||||
| syntax | becomes |
|
||||
| --- | --- |
|
||||
| `sqlc.arg(name)` / `sqlc.narg(name)` | the dialect's native placeholder |
|
||||
| `sqlc.slice(name)` | the placeholder wrapped in `/*SLICE:name*/` |
|
||||
| `sqlc.embed(table)` | `table.*` |
|
||||
| `@name` | the native placeholder, on dialects where `@` is sqlc syntax |
|
||||
|
||||
Native placeholders by dialect:
|
||||
|
||||
| engine | placeholder | `@name` |
|
||||
| --- | --- | --- |
|
||||
| postgresql | `$1` | sqlc syntax |
|
||||
| mysql | `?` | user variable, left alone |
|
||||
| sqlite | `?1` | sqlc syntax |
|
||||
|
||||
GoogleSQL and ClickHouse are deliberately absent. They handle their own
|
||||
parameter syntax, so `File` returns their source unchanged and sqlc syntax is
|
||||
not available to them — `sqlc.arg()` reaches the parser as the function call it
|
||||
looks like.
|
||||
|
||||
## How it works
|
||||
|
||||
`lexer` is a dialect-parameterized scanner. It does not parse SQL — it knows
|
||||
only enough to skip the regions where sqlc syntax must not be rewritten:
|
||||
comments (`--`, `/* */`, `#`), string literals, quoted identifiers, backticks
|
||||
and PostgreSQL dollar-quoted strings. `Dialect` (dialect.go) is the single
|
||||
place those lexical rules live.
|
||||
|
||||
`File(engine, src)` looks up the dialect; an engine with no entry gets its
|
||||
source back untouched. Otherwise it walks the whole file, splits it at
|
||||
top-level semicolons and, for each statement:
|
||||
|
||||
1. `scan` collects every sqlc construct **and** every native placeholder, in
|
||||
source order.
|
||||
2. `number` assigns placeholder numbers. Numbered dialects keep the numbers the
|
||||
user wrote and fill in the gaps; `?` dialects renumber everything in source
|
||||
order, because each `?` is its own argument.
|
||||
3. The statement is rewritten into the output buffer and a `Statement` records
|
||||
what changed.
|
||||
|
||||
## The side table
|
||||
|
||||
`Result.Statement(offset)` returns the `Statement` covering an offset in the
|
||||
**rewritten** text:
|
||||
|
||||
- `Params` — the `named.ParamSet` for the statement
|
||||
- `Embeds` — each rewritten `table.*`, keyed by its location so the compiler can
|
||||
tell it apart from a star reference the user wrote
|
||||
- `Slices` — the location of each `sqlc.slice()` placeholder
|
||||
- `Numbers` — location → parameter number, used by the compiler to override the
|
||||
numbers an engine assigned in AST-conversion order
|
||||
- `Dollar` / `ParamErr` — the placeholder-style validation that used to live in
|
||||
`validate.ParamRef`
|
||||
- `Err` — a sqlc syntax error (unknown `sqlc.*` function, wrong arity, an
|
||||
argument that is not an identifier or string)
|
||||
|
||||
`Result.Origin(offset)` maps an offset in the rewritten text back to the
|
||||
original source, so errors point at what the user wrote.
|
||||
|
||||
## Invariants
|
||||
|
||||
- **A rewrite never adds a line.** Replacements are single-line, so a line
|
||||
number taken from the rewritten text is never past the end of the original. It
|
||||
can *lose* lines, when the sqlc call itself spanned several — map offsets
|
||||
through `Origin` rather than trusting line numbers.
|
||||
- **An invalid statement is copied through untouched.** The engine still parses
|
||||
what the user wrote and the error is reported per statement, not per file.
|
||||
- **Nothing inside a comment or literal is rewritten.** Query annotations like
|
||||
`-- name: GetAuthor :one` are comments, so they are always safe.
|
||||
|
||||
## Tests
|
||||
|
||||
`testdata/<engine>/<case>/` holds the input and the expected results:
|
||||
|
||||
| file | contents |
|
||||
| --- | --- |
|
||||
| `input.sql` | the query file |
|
||||
| `output.sql` | the rewritten SQL |
|
||||
| `side_table.json` | everything the preprocessor recorded |
|
||||
| `stderr.txt` | reported errors, only when the input is invalid |
|
||||
|
||||
`TestRewrite` runs one subtest per directory. `side_table.json` is rendered by
|
||||
reading the result back through the same API the compiler uses, so it covers
|
||||
parameter names and nullability, embed and slice spans, placeholder numbering
|
||||
and the offset map — a parameter's `location` is its offset in the rewritten
|
||||
text and its `origin` is where it came from.
|
||||
|
||||
Regenerate every golden with:
|
||||
|
||||
```bash
|
||||
go test ./internal/sql/preprocess -update
|
||||
```
|
||||
|
||||
The cases cover every shape of sqlc syntax that appears in
|
||||
`internal/endtoend`, per preprocessed engine — each function against a bare
|
||||
reference, a string constant and a quoted identifier, the placeholder styles,
|
||||
and the constructs that must be left alone (comments, literals, MySQL user
|
||||
variables and `$1`, PostgreSQL's `@>` and `?` operators). When you add a shape
|
||||
to the corpus, add it here too.
|
||||
|
||||
## Adding a dialect
|
||||
|
||||
Add an entry to `dialects` in dialect.go. Nothing else in the codebase needs to
|
||||
change for `sqlc.arg`/`narg`/`slice`/`embed` to work for a new engine — but only
|
||||
add one if the engine should support sqlc syntax at all. Leaving an engine out
|
||||
is a deliberate choice, not an oversight.
|
||||
114
internal/sql/preprocess/dialect.go
Normal file
114
internal/sql/preprocess/dialect.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package preprocess
|
||||
|
||||
import (
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
)
|
||||
|
||||
// Style is the native placeholder syntax a dialect uses for bind parameters.
|
||||
type Style int
|
||||
|
||||
const (
|
||||
// StyleDollar numbers parameters as $1, $2, ... (PostgreSQL)
|
||||
StyleDollar Style = iota
|
||||
// StyleQuestion uses an unnumbered ? for every parameter (MySQL)
|
||||
StyleQuestion
|
||||
// StyleOrdinal numbers parameters as ?1, ?2, ... (SQLite)
|
||||
StyleOrdinal
|
||||
)
|
||||
|
||||
// Dialect describes the lexical rules the preprocessor needs to walk a query
|
||||
// without parsing it: how the dialect quotes strings and identifiers, how it
|
||||
// writes comments and which placeholder syntax it accepts.
|
||||
//
|
||||
// The preprocessor never interprets SQL. It only needs to know enough to skip
|
||||
// the regions of a query where sqlc syntax must not be rewritten.
|
||||
type Dialect struct {
|
||||
// Style is the native placeholder syntax used for rewritten parameters.
|
||||
Style Style
|
||||
|
||||
// AtSign reports whether a bare "@name" is sqlc named-parameter syntax.
|
||||
// It is false for MySQL, where "@name" is a user variable.
|
||||
AtSign bool
|
||||
|
||||
// DollarQuote reports whether $tag$ ... $tag$ string literals are valid.
|
||||
DollarQuote bool
|
||||
|
||||
// DollarNumber reports whether $1 is a bind parameter.
|
||||
DollarNumber bool
|
||||
|
||||
// Question reports whether ? is a bind parameter.
|
||||
Question bool
|
||||
|
||||
// Backtick reports whether `ident` is a quoted identifier.
|
||||
Backtick bool
|
||||
|
||||
// HashComment reports whether # starts a line comment.
|
||||
HashComment bool
|
||||
|
||||
// DoubleQuoteString reports whether "..." is a string literal rather than
|
||||
// a quoted identifier. MySQL treats it as a string unless ANSI_QUOTES is
|
||||
// enabled.
|
||||
DoubleQuoteString bool
|
||||
|
||||
// NestedBlockComment reports whether /* ... /* ... */ ... */ nests.
|
||||
NestedBlockComment bool
|
||||
|
||||
// Backslash reports whether a backslash escapes the next character inside
|
||||
// a string literal.
|
||||
Backslash bool
|
||||
|
||||
// FoldIdentifier reports whether the dialect lowercases unquoted
|
||||
// identifiers. It decides the case of a parameter named by a bare
|
||||
// reference, e.g. sqlc.arg(FooBar).
|
||||
FoldIdentifier bool
|
||||
}
|
||||
|
||||
var dialects = map[config.Engine]Dialect{
|
||||
config.EnginePostgreSQL: {
|
||||
Style: StyleDollar,
|
||||
AtSign: true,
|
||||
DollarQuote: true,
|
||||
DollarNumber: true,
|
||||
NestedBlockComment: true,
|
||||
FoldIdentifier: true,
|
||||
},
|
||||
config.EngineMySQL: {
|
||||
Style: StyleQuestion,
|
||||
Question: true,
|
||||
Backtick: true,
|
||||
HashComment: true,
|
||||
DoubleQuoteString: true,
|
||||
Backslash: true,
|
||||
FoldIdentifier: true,
|
||||
},
|
||||
config.EngineSQLite: {
|
||||
Style: StyleOrdinal,
|
||||
AtSign: true,
|
||||
Question: true,
|
||||
Backtick: true,
|
||||
Backslash: true,
|
||||
},
|
||||
// ClickHouse binds with an unnumbered ?, like MySQL, and its identifiers
|
||||
// keep their case.
|
||||
config.EngineClickHouse: {
|
||||
Style: StyleQuestion,
|
||||
Question: true,
|
||||
Backtick: true,
|
||||
Backslash: true,
|
||||
},
|
||||
}
|
||||
|
||||
// DialectFor returns the lexical rules for an engine, and whether the engine is
|
||||
// preprocessed at all. GoogleSQL is not: it handles its own parameter syntax,
|
||||
// so its queries reach the parser unchanged.
|
||||
func DialectFor(engine config.Engine) (Dialect, bool) {
|
||||
d, ok := dialects[engine]
|
||||
return d, ok
|
||||
}
|
||||
|
||||
// hasNamedSupport reports whether repeated uses of the same parameter name can
|
||||
// share a single placeholder. MySQL sends an argument per "?", so every
|
||||
// occurrence needs its own number.
|
||||
func (d Dialect) hasNamedSupport() bool {
|
||||
return d.Style != StyleQuestion
|
||||
}
|
||||
284
internal/sql/preprocess/lexer.go
Normal file
284
internal/sql/preprocess/lexer.go
Normal file
@@ -0,0 +1,284 @@
|
||||
package preprocess
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// lexer is a dialect-parameterized scanner over a SQL file. It does not parse
|
||||
// SQL; it knows just enough about comments, string literals, quoted identifiers
|
||||
// and dollar-quoted strings to walk a query without ever looking inside a
|
||||
// region where sqlc syntax must be left alone.
|
||||
type lexer struct {
|
||||
d Dialect
|
||||
src string
|
||||
}
|
||||
|
||||
func isIdentStart(c byte) bool {
|
||||
return c == '_' ||
|
||||
(c >= 'a' && c <= 'z') ||
|
||||
(c >= 'A' && c <= 'Z') ||
|
||||
c >= 0x80
|
||||
}
|
||||
|
||||
func isIdentPart(c byte) bool {
|
||||
return isIdentStart(c) || (c >= '0' && c <= '9') || c == '$'
|
||||
}
|
||||
|
||||
func isDigit(c byte) bool {
|
||||
return c >= '0' && c <= '9'
|
||||
}
|
||||
|
||||
func isSpace(c byte) bool {
|
||||
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'
|
||||
}
|
||||
|
||||
func (l *lexer) at(i int) byte {
|
||||
if i < 0 || i >= len(l.src) {
|
||||
return 0
|
||||
}
|
||||
return l.src[i]
|
||||
}
|
||||
|
||||
// skip returns the offset just past the comment, string literal, quoted
|
||||
// identifier or dollar-quoted string that starts at i. It returns -1 when no
|
||||
// such token starts at i.
|
||||
func (l *lexer) skip(i int) int {
|
||||
switch c := l.at(i); c {
|
||||
case '\'':
|
||||
return l.skipQuoted(i, '\'', l.d.Backslash || l.isEscapeString(i))
|
||||
case '"':
|
||||
return l.skipQuoted(i, '"', l.d.DoubleQuoteString && l.d.Backslash)
|
||||
case '`':
|
||||
if !l.d.Backtick {
|
||||
return -1
|
||||
}
|
||||
return l.skipQuoted(i, '`', false)
|
||||
case '-':
|
||||
if l.at(i+1) == '-' {
|
||||
return l.skipLine(i)
|
||||
}
|
||||
case '#':
|
||||
if l.d.HashComment {
|
||||
return l.skipLine(i)
|
||||
}
|
||||
case '/':
|
||||
if l.at(i+1) == '*' {
|
||||
return l.skipBlockComment(i)
|
||||
}
|
||||
case '$':
|
||||
if l.d.DollarQuote {
|
||||
if end := l.skipDollarQuoted(i); end >= 0 {
|
||||
return end
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// isEscapeString reports whether the string literal starting at i is a
|
||||
// PostgreSQL escape string constant, written E'...', where a backslash escapes
|
||||
// the following character.
|
||||
func (l *lexer) isEscapeString(i int) bool {
|
||||
if !l.d.DollarQuote { // PostgreSQL-only syntax
|
||||
return false
|
||||
}
|
||||
c := l.at(i - 1)
|
||||
if c != 'e' && c != 'E' {
|
||||
return false
|
||||
}
|
||||
// The E must not be the tail of a longer identifier.
|
||||
return !isIdentPart(l.at(i - 2))
|
||||
}
|
||||
|
||||
func (l *lexer) skipQuoted(i int, quote byte, backslash bool) int {
|
||||
for j := i + 1; j < len(l.src); j++ {
|
||||
switch l.src[j] {
|
||||
case '\\':
|
||||
if backslash {
|
||||
j++
|
||||
}
|
||||
case quote:
|
||||
// A doubled quote is an escaped quote, not the end.
|
||||
if l.at(j+1) == quote {
|
||||
j++
|
||||
continue
|
||||
}
|
||||
return j + 1
|
||||
}
|
||||
}
|
||||
return len(l.src)
|
||||
}
|
||||
|
||||
func (l *lexer) skipLine(i int) int {
|
||||
if idx := strings.IndexByte(l.src[i:], '\n'); idx >= 0 {
|
||||
return i + idx + 1
|
||||
}
|
||||
return len(l.src)
|
||||
}
|
||||
|
||||
func (l *lexer) skipBlockComment(i int) int {
|
||||
depth := 0
|
||||
for j := i; j < len(l.src)-1; j++ {
|
||||
if l.src[j] == '/' && l.src[j+1] == '*' {
|
||||
depth++
|
||||
j++
|
||||
continue
|
||||
}
|
||||
if l.src[j] == '*' && l.src[j+1] == '/' {
|
||||
depth--
|
||||
j++
|
||||
if depth == 0 || !l.d.NestedBlockComment {
|
||||
return j + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return len(l.src)
|
||||
}
|
||||
|
||||
// skipDollarQuoted handles PostgreSQL's $tag$ ... $tag$ string literals. It
|
||||
// returns -1 when i does not start a dollar quote.
|
||||
func (l *lexer) skipDollarQuoted(i int) int {
|
||||
tag := l.dollarTag(i)
|
||||
if tag == "" {
|
||||
return -1
|
||||
}
|
||||
if idx := strings.Index(l.src[i+len(tag):], tag); idx >= 0 {
|
||||
return i + len(tag) + idx + len(tag)
|
||||
}
|
||||
return len(l.src)
|
||||
}
|
||||
|
||||
// dollarTag returns the full opening delimiter ("$$" or "$tag$") when one
|
||||
// starts at i, and "" otherwise.
|
||||
func (l *lexer) dollarTag(i int) string {
|
||||
if l.at(i) != '$' {
|
||||
return ""
|
||||
}
|
||||
j := i + 1
|
||||
for j < len(l.src) && l.src[j] != '$' {
|
||||
c := l.src[j]
|
||||
if isDigit(c) && j == i+1 {
|
||||
return "" // $1 is a bind parameter, not a tag
|
||||
}
|
||||
if !isIdentPart(c) || c == '$' {
|
||||
return ""
|
||||
}
|
||||
j++
|
||||
}
|
||||
if j >= len(l.src) {
|
||||
return ""
|
||||
}
|
||||
return l.src[i : j+1]
|
||||
}
|
||||
|
||||
// identEnd returns the offset just past the identifier that starts at i.
|
||||
func (l *lexer) identEnd(i int) int {
|
||||
j := i
|
||||
for j < len(l.src) && isIdentPart(l.src[j]) {
|
||||
j++
|
||||
}
|
||||
return j
|
||||
}
|
||||
|
||||
// skipSpace advances past whitespace and comments.
|
||||
func (l *lexer) skipSpace(i int) int {
|
||||
for i < len(l.src) {
|
||||
if isSpace(l.src[i]) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if l.src[i] == '-' && l.at(i+1) == '-' {
|
||||
i = l.skipLine(i)
|
||||
continue
|
||||
}
|
||||
if l.src[i] == '/' && l.at(i+1) == '*' {
|
||||
i = l.skipBlockComment(i)
|
||||
continue
|
||||
}
|
||||
if l.src[i] == '#' && l.d.HashComment {
|
||||
i = l.skipLine(i)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// statements splits the source into statement spans at top-level semicolons.
|
||||
// The spans cover the whole source, so every byte belongs to exactly one
|
||||
// statement.
|
||||
func (l *lexer) statements() [][2]int {
|
||||
var out [][2]int
|
||||
start := 0
|
||||
for i := 0; i < len(l.src); {
|
||||
if end := l.skip(i); end >= 0 {
|
||||
i = end
|
||||
continue
|
||||
}
|
||||
if l.src[i] == ';' {
|
||||
out = append(out, [2]int{start, i + 1})
|
||||
start = i + 1
|
||||
}
|
||||
i++
|
||||
}
|
||||
if start < len(l.src) {
|
||||
out = append(out, [2]int{start, len(l.src)})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// matchParen returns the offset of the ")" that closes the "(" at i, or -1.
|
||||
func (l *lexer) matchParen(i int) int {
|
||||
depth := 0
|
||||
for j := i; j < len(l.src); {
|
||||
if end := l.skip(j); end >= 0 {
|
||||
j = end
|
||||
continue
|
||||
}
|
||||
switch l.src[j] {
|
||||
case '(':
|
||||
depth++
|
||||
case ')':
|
||||
depth--
|
||||
if depth == 0 {
|
||||
return j
|
||||
}
|
||||
}
|
||||
j++
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// splitArgs splits the text between parentheses on top-level commas. Empty or
|
||||
// whitespace-only input yields no arguments.
|
||||
func (l *lexer) splitArgs(start, end int) []string {
|
||||
var args []string
|
||||
depth := 0
|
||||
last := start
|
||||
for j := start; j < end; {
|
||||
if stop := l.skip(j); stop >= 0 {
|
||||
if stop > end {
|
||||
stop = end
|
||||
}
|
||||
j = stop
|
||||
continue
|
||||
}
|
||||
switch l.src[j] {
|
||||
case '(':
|
||||
depth++
|
||||
case ')':
|
||||
depth--
|
||||
case ',':
|
||||
if depth == 0 {
|
||||
args = append(args, l.src[last:j])
|
||||
last = j + 1
|
||||
}
|
||||
}
|
||||
j++
|
||||
}
|
||||
tail := l.src[last:end]
|
||||
if len(args) == 0 && strings.TrimSpace(tail) == "" {
|
||||
return nil
|
||||
}
|
||||
return append(args, tail)
|
||||
}
|
||||
398
internal/sql/preprocess/preprocess.go
Normal file
398
internal/sql/preprocess/preprocess.go
Normal file
@@ -0,0 +1,398 @@
|
||||
// Package preprocess rewrites sqlc's query syntax into native SQL before any
|
||||
// engine parser sees a query.
|
||||
//
|
||||
// sqlc.arg(), sqlc.narg(), sqlc.slice(), sqlc.embed() and @name are sqlc
|
||||
// syntax, not SQL. Historically they survived all the way through each engine's
|
||||
// parser and were replaced by walking the resulting AST, which meant every
|
||||
// engine had to round-trip a schema-qualified function call it did not
|
||||
// understand. This package removes that requirement: it scans the query text
|
||||
// with a dialect-parameterized lexer, replaces each construct with the engine's
|
||||
// native placeholder, and records what it replaced. Engines then only ever see
|
||||
// valid SQL.
|
||||
package preprocess
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/named"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/sqlerr"
|
||||
)
|
||||
|
||||
// Embed is a rewritten sqlc.embed(table) call.
|
||||
type Embed struct {
|
||||
Table *ast.TableName
|
||||
|
||||
// Location is the offset of the rewritten "table.*" in the preprocessed
|
||||
// text. The compiler uses it to tell a rewritten star reference apart from
|
||||
// one the user wrote.
|
||||
Location int
|
||||
|
||||
param string
|
||||
}
|
||||
|
||||
// Orig returns the sqlc syntax this embed was rewritten from.
|
||||
func (e Embed) Orig() string {
|
||||
return fmt.Sprintf("sqlc.embed(%s)", e.param)
|
||||
}
|
||||
|
||||
// EmbedSet is the set of embeds in a single statement.
|
||||
type EmbedSet []*Embed
|
||||
|
||||
// Find returns the embed recorded at any of the given locations. Engines do not
|
||||
// all populate the same location fields, so callers pass every location that
|
||||
// could identify the node.
|
||||
func (es EmbedSet) Find(locations ...int) (*Embed, bool) {
|
||||
for _, e := range es {
|
||||
for _, loc := range locations {
|
||||
if loc == e.Location {
|
||||
return e, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Statement holds everything the preprocessor learned about a single statement.
|
||||
type Statement struct {
|
||||
// Start and End delimit the statement in the rewritten text.
|
||||
Start, End int
|
||||
|
||||
// Params maps each placeholder number to its sqlc metadata.
|
||||
Params *named.ParamSet
|
||||
|
||||
// Embeds records the sqlc.embed() calls that were rewritten to "table.*".
|
||||
Embeds EmbedSet
|
||||
|
||||
// Slices holds the offset, in the rewritten text, of every placeholder that
|
||||
// came from sqlc.slice(), mapped to the parameter name.
|
||||
Slices map[int]string
|
||||
|
||||
// Numbers maps the offset of every placeholder in the rewritten text to the
|
||||
// number assigned to it. Engines number bind parameters in whatever order
|
||||
// they happen to convert the AST, so the compiler uses this to restore
|
||||
// source order.
|
||||
Numbers map[int]int
|
||||
|
||||
// Dollar reports whether the statement's own placeholders are numbered.
|
||||
Dollar bool
|
||||
|
||||
// ParamErr is set when the placeholders the user wrote are inconsistent:
|
||||
// numbered and unnumbered styles mixed, or a gap in the numbering.
|
||||
ParamErr error
|
||||
|
||||
// Err is a validation error found while rewriting. The statement text is
|
||||
// left untouched when this is set, so the engine still sees the original
|
||||
// query and the error can be reported against it.
|
||||
Err error
|
||||
}
|
||||
|
||||
// Result is the outcome of preprocessing a SQL file.
|
||||
type Result struct {
|
||||
// Text is the rewritten SQL. It has the same number of statements and the
|
||||
// same line structure as the input.
|
||||
Text string
|
||||
|
||||
stmts []*Statement
|
||||
edits []edit
|
||||
}
|
||||
|
||||
// edit records one rewritten span, in both coordinate systems.
|
||||
type edit struct {
|
||||
oldStart, oldEnd int
|
||||
newStart, newEnd int
|
||||
}
|
||||
|
||||
// Statements returns every statement in the rewritten text, in source order.
|
||||
func (r *Result) Statements() []*Statement {
|
||||
return r.stmts
|
||||
}
|
||||
|
||||
// Statement returns the preprocessing results for the statement covering the
|
||||
// given offset in the rewritten text. It never returns nil.
|
||||
func (r *Result) Statement(location int) *Statement {
|
||||
for _, s := range r.stmts {
|
||||
if location < s.End {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return &Statement{Params: named.NewParamSet(nil, true)}
|
||||
}
|
||||
|
||||
// Origin maps an offset in the rewritten text back to the offset in the
|
||||
// original source, so errors can be reported against what the user wrote.
|
||||
func (r *Result) Origin(location int) int {
|
||||
delta := 0
|
||||
for _, e := range r.edits {
|
||||
if location < e.newStart {
|
||||
break
|
||||
}
|
||||
if location < e.newEnd {
|
||||
return e.oldStart
|
||||
}
|
||||
delta += (e.oldEnd - e.oldStart) - (e.newEnd - e.newStart)
|
||||
}
|
||||
return location + delta
|
||||
}
|
||||
|
||||
type kind int
|
||||
|
||||
const (
|
||||
kindArg kind = iota
|
||||
kindNarg
|
||||
kindSlice
|
||||
kindEmbed
|
||||
kindNative
|
||||
)
|
||||
|
||||
// occurrence is one construct found in the query text.
|
||||
type occurrence struct {
|
||||
kind kind
|
||||
start int // offset in the original source
|
||||
end int
|
||||
name string
|
||||
// ident is the argument exactly as written, used to rebuild "table.*" for
|
||||
// embeds so quoting is preserved.
|
||||
ident string
|
||||
// number is the placeholder number assigned to this occurrence.
|
||||
number int
|
||||
// explicit reports whether a native placeholder carried its own number.
|
||||
explicit bool
|
||||
err *sqlerr.Error
|
||||
}
|
||||
|
||||
// File rewrites every sqlc construct in src to native SQL for the given engine.
|
||||
//
|
||||
// An engine that is not preprocessed — GoogleSQL, which handles its own
|
||||
// parameter syntax — gets the source back unchanged, with an empty side
|
||||
// table. sqlc.arg() and friends are not rewritten for it, so they reach the
|
||||
// parser as the function calls they look like.
|
||||
func File(engine config.Engine, src string) *Result {
|
||||
d, ok := DialectFor(engine)
|
||||
if !ok {
|
||||
return &Result{Text: src}
|
||||
}
|
||||
return Dialected(d, src)
|
||||
}
|
||||
|
||||
// Dialected rewrites src using an explicit dialect.
|
||||
func Dialected(d Dialect, src string) *Result {
|
||||
l := &lexer{d: d, src: src}
|
||||
res := &Result{}
|
||||
|
||||
var b strings.Builder
|
||||
b.Grow(len(src))
|
||||
|
||||
spans := l.statements()
|
||||
for i, span := range spans {
|
||||
start, end := span[0], span[1]
|
||||
|
||||
// Whatever trails the last semicolon is only whitespace in a
|
||||
// well-formed file. It still belongs in the output, but it is not a
|
||||
// statement.
|
||||
if i == len(spans)-1 && strings.TrimSpace(src[start:end]) == "" {
|
||||
b.WriteString(src[start:end])
|
||||
continue
|
||||
}
|
||||
|
||||
occs := l.scan(start, end)
|
||||
stmt := &Statement{Start: b.Len()}
|
||||
|
||||
// A statement that fails validation is copied through unchanged so the
|
||||
// engine parses what the user actually wrote and the error keeps its
|
||||
// original shape.
|
||||
if err := firstError(occs); err != nil {
|
||||
b.WriteString(src[start:end])
|
||||
stmt.End = b.Len()
|
||||
err.Location = stmt.Start + (err.Location - start)
|
||||
stmt.Err = err
|
||||
stmt.Params = named.NewParamSet(nil, d.hasNamedSupport())
|
||||
stmt.Dollar = true
|
||||
res.stmts = append(res.stmts, stmt)
|
||||
continue
|
||||
}
|
||||
|
||||
stmt.Dollar, stmt.ParamErr = validatePlaceholders(occs)
|
||||
stmt.Params = number(d, occs)
|
||||
stmt.Numbers = map[int]int{}
|
||||
|
||||
prev := start
|
||||
for i := range occs {
|
||||
occ := &occs[i]
|
||||
b.WriteString(src[prev:occ.start])
|
||||
newStart := b.Len()
|
||||
replacement := d.replacement(occ)
|
||||
b.WriteString(replacement)
|
||||
newEnd := b.Len()
|
||||
prev = occ.end
|
||||
|
||||
if replacement != src[occ.start:occ.end] {
|
||||
res.edits = append(res.edits, edit{
|
||||
oldStart: occ.start, oldEnd: occ.end,
|
||||
newStart: newStart, newEnd: newEnd,
|
||||
})
|
||||
}
|
||||
|
||||
if occ.kind != kindEmbed {
|
||||
stmt.Numbers[newStart+sliceMarkerLen(d, occ)] = occ.number
|
||||
}
|
||||
|
||||
switch occ.kind {
|
||||
case kindEmbed:
|
||||
stmt.Embeds = append(stmt.Embeds, &Embed{
|
||||
Table: &ast.TableName{Name: occ.name},
|
||||
Location: newStart,
|
||||
param: occ.ident,
|
||||
})
|
||||
case kindSlice:
|
||||
if stmt.Slices == nil {
|
||||
stmt.Slices = map[int]string{}
|
||||
}
|
||||
// The placeholder follows the /*SLICE:name*/ marker, so record
|
||||
// the offset the engine will report for the parameter node.
|
||||
stmt.Slices[newStart+sliceMarkerLen(d, occ)] = occ.name
|
||||
}
|
||||
}
|
||||
b.WriteString(src[prev:end])
|
||||
stmt.End = b.Len()
|
||||
res.stmts = append(res.stmts, stmt)
|
||||
}
|
||||
|
||||
res.Text = b.String()
|
||||
return res
|
||||
}
|
||||
|
||||
func firstError(occs []occurrence) *sqlerr.Error {
|
||||
for _, occ := range occs {
|
||||
if occ.err != nil {
|
||||
return occ.err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sliceMarkerLen returns the length of the /*SLICE:name*/ prefix a slice
|
||||
// placeholder is written with, or 0 when there is none.
|
||||
func sliceMarkerLen(d Dialect, occ *occurrence) int {
|
||||
if occ.kind != kindSlice || d.Style == StyleDollar {
|
||||
return 0
|
||||
}
|
||||
return len("/*SLICE:") + len(occ.name) + len("*/")
|
||||
}
|
||||
|
||||
// validatePlaceholders checks the placeholders the user wrote, reproducing the
|
||||
// rules the AST-based validator applied before sqlc syntax was rewritten: a
|
||||
// query may not mix numbered and unnumbered placeholders, and the numbers it
|
||||
// does use may not have gaps.
|
||||
func validatePlaceholders(occs []occurrence) (dollar bool, err error) {
|
||||
var numbered, unnumbered bool
|
||||
seen := map[int]bool{}
|
||||
for _, occ := range occs {
|
||||
if occ.kind != kindNative {
|
||||
continue
|
||||
}
|
||||
if occ.explicit {
|
||||
numbered = true
|
||||
seen[occ.number] = true
|
||||
} else {
|
||||
unnumbered = true
|
||||
}
|
||||
}
|
||||
if numbered && unnumbered {
|
||||
return false, errors.New("can not mix $1 format with ? format")
|
||||
}
|
||||
for i := 1; i <= len(seen); i++ {
|
||||
if !seen[i] {
|
||||
return !unnumbered, &sqlerr.Error{
|
||||
Code: "42P18",
|
||||
Message: fmt.Sprintf("could not determine data type of parameter $%d", i),
|
||||
}
|
||||
}
|
||||
}
|
||||
return !unnumbered, nil
|
||||
}
|
||||
|
||||
// number assigns a placeholder number to every occurrence and builds the
|
||||
// parameter set for the statement.
|
||||
func number(d Dialect, occs []occurrence) *named.ParamSet {
|
||||
if d.Style == StyleQuestion {
|
||||
// Every "?" is sent as its own argument, so existing and rewritten
|
||||
// placeholders are numbered together in source order.
|
||||
ps := named.NewParamSet(nil, false)
|
||||
for i := range occs {
|
||||
occ := &occs[i]
|
||||
if occ.kind == kindEmbed {
|
||||
continue
|
||||
}
|
||||
occ.number = ps.Add(param(occ))
|
||||
}
|
||||
return ps
|
||||
}
|
||||
|
||||
// Numbered dialects keep the numbers the user wrote and fill in the gaps.
|
||||
numbs := map[int]bool{}
|
||||
count := 0
|
||||
for i := range occs {
|
||||
occ := &occs[i]
|
||||
if occ.kind != kindNative {
|
||||
continue
|
||||
}
|
||||
count++
|
||||
if !occ.explicit {
|
||||
occ.number = count
|
||||
}
|
||||
numbs[occ.number] = true
|
||||
}
|
||||
ps := named.NewParamSet(numbs, d.hasNamedSupport())
|
||||
for i := range occs {
|
||||
occ := &occs[i]
|
||||
if occ.kind == kindEmbed || occ.kind == kindNative {
|
||||
continue
|
||||
}
|
||||
occ.number = ps.Add(param(occ))
|
||||
}
|
||||
return ps
|
||||
}
|
||||
|
||||
func param(occ *occurrence) named.Param {
|
||||
switch occ.kind {
|
||||
case kindNarg:
|
||||
return named.NewUserNullableParam(occ.name)
|
||||
case kindSlice:
|
||||
return named.NewSqlcSlice(occ.name)
|
||||
default:
|
||||
return named.NewParam(occ.name)
|
||||
}
|
||||
}
|
||||
|
||||
// replacement returns the native SQL that stands in for an occurrence.
|
||||
func (d Dialect) replacement(occ *occurrence) string {
|
||||
switch occ.kind {
|
||||
case kindEmbed:
|
||||
return occ.ident + ".*"
|
||||
case kindNative:
|
||||
return occ.ident
|
||||
}
|
||||
switch d.Style {
|
||||
case StyleDollar:
|
||||
// PostgreSQL passes a slice as a single array parameter, so it needs no
|
||||
// expansion marker.
|
||||
return fmt.Sprintf("$%d", occ.number)
|
||||
case StyleOrdinal:
|
||||
if occ.kind == kindSlice {
|
||||
// This sequence is also replicated in
|
||||
// internal/codegen/golang.Field since it's needed during template
|
||||
// generation for replacement.
|
||||
return fmt.Sprintf("/*SLICE:%s*/?", occ.name)
|
||||
}
|
||||
return fmt.Sprintf("?%d", occ.number)
|
||||
default:
|
||||
if occ.kind == kindSlice {
|
||||
return fmt.Sprintf("/*SLICE:%s*/?", occ.name)
|
||||
}
|
||||
return "?"
|
||||
}
|
||||
}
|
||||
232
internal/sql/preprocess/preprocess_test.go
Normal file
232
internal/sql/preprocess/preprocess_test.go
Normal file
@@ -0,0 +1,232 @@
|
||||
package preprocess_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
"github.com/sqlc-dev/sqlc/internal/source"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/named"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/preprocess"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/sqlerr"
|
||||
)
|
||||
|
||||
var update = flag.Bool("update", false, "update the testdata golden files")
|
||||
|
||||
// TestRewrite runs every case under testdata. Each case is a directory holding
|
||||
// the input and the expected results, grouped by engine:
|
||||
//
|
||||
// testdata/<engine>/<case>/input.sql the query file
|
||||
// testdata/<engine>/<case>/output.sql the rewritten SQL
|
||||
// testdata/<engine>/<case>/side_table.json what the preprocessor recorded
|
||||
// testdata/<engine>/<case>/stderr.txt reported errors (only when invalid)
|
||||
//
|
||||
// Regenerate the golden files with `go test ./internal/sql/preprocess -update`.
|
||||
func TestRewrite(t *testing.T) {
|
||||
for _, dir := range cases(t) {
|
||||
t.Run(filepath.ToSlash(dir), func(t *testing.T) {
|
||||
path := filepath.Join("testdata", dir)
|
||||
engine := config.Engine(filepath.Dir(dir))
|
||||
|
||||
input := readFile(t, filepath.Join(path, "input.sql"))
|
||||
res := preprocess.File(engine, input)
|
||||
|
||||
compare(t, filepath.Join(path, "output.sql"), res.Text)
|
||||
compare(t, filepath.Join(path, "side_table.json"), sideTable(res))
|
||||
compare(t, filepath.Join(path, "stderr.txt"), errors(input, res))
|
||||
|
||||
// A rewrite only ever replaces text with something shorter or
|
||||
// equal in lines, so a line number taken from the rewritten text is
|
||||
// never past the end of the original.
|
||||
if want, got := strings.Count(input, "\n"), strings.Count(res.Text, "\n"); got > want {
|
||||
t.Errorf("rewrite added lines: got %d, want at most %d", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The JSON shape of the side table the preprocessor records alongside the
|
||||
// rewritten SQL. Locations are offsets into the rewritten text; origins are the
|
||||
// offsets they map back to in the original source.
|
||||
type statement struct {
|
||||
Start int `json:"start"`
|
||||
End int `json:"end"`
|
||||
Dollar bool `json:"dollar"`
|
||||
Params []parameter `json:"params,omitempty"`
|
||||
Embeds []embed `json:"embeds,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
ErrorAt int `json:"error_at,omitempty"`
|
||||
ParamError string `json:"param_error,omitempty"`
|
||||
}
|
||||
|
||||
type parameter struct {
|
||||
Number int `json:"number"`
|
||||
Location int `json:"location"`
|
||||
Origin int `json:"origin"`
|
||||
Name string `json:"name,omitempty"`
|
||||
// Named is false for a placeholder the user wrote themselves, which the
|
||||
// preprocessor numbers but does not name.
|
||||
Named bool `json:"named"`
|
||||
// Nullable is true when sqlc.narg() forced the parameter nullable, even
|
||||
// though the schema says it is NOT NULL.
|
||||
Nullable bool `json:"nullable,omitempty"`
|
||||
Slice bool `json:"slice,omitempty"`
|
||||
}
|
||||
|
||||
type embed struct {
|
||||
Table string `json:"table"`
|
||||
Location int `json:"location"`
|
||||
Origin int `json:"origin"`
|
||||
Orig string `json:"orig"`
|
||||
}
|
||||
|
||||
// sideTable renders everything the preprocessor recorded, reading it back
|
||||
// through the same API the compiler uses.
|
||||
func sideTable(res *preprocess.Result) string {
|
||||
out := make([]statement, 0, len(res.Statements()))
|
||||
for _, stmt := range res.Statements() {
|
||||
s := statement{Start: stmt.Start, End: stmt.End, Dollar: stmt.Dollar}
|
||||
if stmt.Err != nil {
|
||||
s.Error = stmt.Err.Error()
|
||||
if e, ok := stmt.Err.(*sqlerr.Error); ok {
|
||||
s.ErrorAt = e.Location
|
||||
}
|
||||
}
|
||||
if stmt.ParamErr != nil {
|
||||
s.ParamError = stmt.ParamErr.Error()
|
||||
}
|
||||
|
||||
locations := make([]int, 0, len(stmt.Numbers))
|
||||
for loc := range stmt.Numbers {
|
||||
locations = append(locations, loc)
|
||||
}
|
||||
sort.Ints(locations)
|
||||
for _, loc := range locations {
|
||||
number := stmt.Numbers[loc]
|
||||
name, _ := stmt.Params.NameFor(number)
|
||||
// Merging against a NOT NULL parameter is how the compiler asks
|
||||
// whether the user overrode nullability with sqlc.narg().
|
||||
p, isNamed := stmt.Params.FetchMerge(number, named.NewInferredParam(name, true))
|
||||
_, isSlice := stmt.Slices[loc]
|
||||
s.Params = append(s.Params, parameter{
|
||||
Number: number,
|
||||
Location: loc,
|
||||
Origin: res.Origin(loc),
|
||||
Name: name,
|
||||
Named: isNamed,
|
||||
Nullable: isNamed && !p.NotNull(),
|
||||
Slice: isSlice,
|
||||
})
|
||||
}
|
||||
|
||||
for _, e := range stmt.Embeds {
|
||||
s.Embeds = append(s.Embeds, embed{
|
||||
Table: e.Table.Name,
|
||||
Location: e.Location,
|
||||
Origin: res.Origin(e.Location),
|
||||
Orig: e.Orig(),
|
||||
})
|
||||
}
|
||||
out = append(out, s)
|
||||
}
|
||||
|
||||
blob, err := json.MarshalIndent(out, "", " ")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(blob) + "\n"
|
||||
}
|
||||
|
||||
// errors renders every validation error the preprocessor recorded, one per
|
||||
// line, as "line:column: message" against the original source.
|
||||
func errors(input string, res *preprocess.Result) string {
|
||||
var out []string
|
||||
for _, stmt := range res.Statements() {
|
||||
for _, err := range []error{stmt.Err, stmt.ParamErr} {
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
loc := stmt.Start
|
||||
if e, ok := err.(*sqlerr.Error); ok && e.Location != 0 {
|
||||
loc = e.Location
|
||||
}
|
||||
line, column := source.LineNumber(input, res.Origin(loc))
|
||||
out = append(out, fmt.Sprintf("%d:%d: %s", line, column, err))
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.Join(out, "\n") + "\n"
|
||||
}
|
||||
|
||||
// cases returns every testdata directory that holds an input.sql, relative to
|
||||
// testdata and in a stable order.
|
||||
func cases(t *testing.T) []string {
|
||||
t.Helper()
|
||||
var dirs []string
|
||||
err := filepath.Walk("testdata", func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() || info.Name() != "input.sql" {
|
||||
return nil
|
||||
}
|
||||
rel, err := filepath.Rel("testdata", filepath.Dir(path))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dirs = append(dirs, rel)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(dirs) == 0 {
|
||||
t.Fatal("no test cases found in testdata")
|
||||
}
|
||||
sort.Strings(dirs)
|
||||
return dirs
|
||||
}
|
||||
|
||||
func readFile(t *testing.T, path string) string {
|
||||
t.Helper()
|
||||
blob, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(blob)
|
||||
}
|
||||
|
||||
// compare checks actual against a golden file. An empty expectation means the
|
||||
// file should not exist.
|
||||
func compare(t *testing.T, path, actual string) {
|
||||
t.Helper()
|
||||
if *update {
|
||||
if actual == "" {
|
||||
os.Remove(path)
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(actual), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
var expected string
|
||||
if blob, err := os.ReadFile(path); err == nil {
|
||||
expected = string(blob)
|
||||
} else if !os.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if diff := cmp.Diff(expected, actual); diff != "" {
|
||||
t.Errorf("%s differed (-want +got):\n%s", filepath.Base(path), diff)
|
||||
}
|
||||
}
|
||||
209
internal/sql/preprocess/scan.go
Normal file
209
internal/sql/preprocess/scan.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package preprocess
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/sqlerr"
|
||||
)
|
||||
|
||||
// scan walks a single statement and returns every sqlc construct and native
|
||||
// placeholder it contains, in source order.
|
||||
func (l *lexer) scan(start, end int) []occurrence {
|
||||
var out []occurrence
|
||||
for i := start; i < end; {
|
||||
if stop := l.skip(i); stop >= 0 {
|
||||
if stop <= i {
|
||||
stop = i + 1
|
||||
}
|
||||
i = stop
|
||||
continue
|
||||
}
|
||||
c := l.src[i]
|
||||
switch {
|
||||
case isIdentStart(c):
|
||||
j := l.identEnd(i)
|
||||
if strings.EqualFold(l.src[i:j], "sqlc") {
|
||||
if occ, next, ok := l.sqlcCall(i, j); ok {
|
||||
out = append(out, occ)
|
||||
i = next
|
||||
continue
|
||||
}
|
||||
}
|
||||
i = j
|
||||
|
||||
case c == '@' && l.d.AtSign && isIdentStart(l.at(i+1)):
|
||||
j := l.identEnd(i + 1)
|
||||
out = append(out, occurrence{
|
||||
kind: kindArg,
|
||||
start: i,
|
||||
end: j,
|
||||
name: l.src[i+1 : j],
|
||||
ident: l.src[i:j],
|
||||
})
|
||||
i = j
|
||||
|
||||
case c == '$' && l.d.DollarNumber && isDigit(l.at(i+1)):
|
||||
j := i + 1
|
||||
for j < len(l.src) && isDigit(l.src[j]) {
|
||||
j++
|
||||
}
|
||||
n, _ := strconv.Atoi(l.src[i+1 : j])
|
||||
out = append(out, occurrence{
|
||||
kind: kindNative,
|
||||
start: i,
|
||||
end: j,
|
||||
number: n,
|
||||
explicit: true,
|
||||
ident: l.src[i:j],
|
||||
})
|
||||
i = j
|
||||
|
||||
case c == '?' && l.d.Question:
|
||||
j := i + 1
|
||||
for j < len(l.src) && isDigit(l.src[j]) {
|
||||
j++
|
||||
}
|
||||
occ := occurrence{kind: kindNative, start: i, end: j, ident: l.src[i:j]}
|
||||
if j > i+1 {
|
||||
occ.number, _ = strconv.Atoi(l.src[i+1 : j])
|
||||
occ.explicit = true
|
||||
}
|
||||
out = append(out, occ)
|
||||
i = j
|
||||
|
||||
default:
|
||||
i++
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// sqlcCall parses a "sqlc.<name>(<arg>)" call that starts at start, where
|
||||
// afterSqlc is the offset just past the "sqlc" identifier. It reports false
|
||||
// when the text is not a sqlc call at all, in which case scanning continues
|
||||
// normally.
|
||||
func (l *lexer) sqlcCall(start, afterSqlc int) (occurrence, int, bool) {
|
||||
i := l.skipSpace(afterSqlc)
|
||||
if l.at(i) != '.' {
|
||||
return occurrence{}, 0, false
|
||||
}
|
||||
i = l.skipSpace(i + 1)
|
||||
if !isIdentStart(l.at(i)) {
|
||||
return occurrence{}, 0, false
|
||||
}
|
||||
nameEnd := l.identEnd(i)
|
||||
fn := strings.ToLower(l.src[i:nameEnd])
|
||||
|
||||
open := l.skipSpace(nameEnd)
|
||||
if l.at(open) != '(' {
|
||||
return occurrence{}, 0, false
|
||||
}
|
||||
closing := l.matchParen(open)
|
||||
if closing < 0 {
|
||||
return occurrence{}, 0, false
|
||||
}
|
||||
|
||||
occ := occurrence{start: start, end: closing + 1}
|
||||
switch fn {
|
||||
case "arg":
|
||||
occ.kind = kindArg
|
||||
case "narg":
|
||||
occ.kind = kindNarg
|
||||
case "slice":
|
||||
occ.kind = kindSlice
|
||||
case "embed":
|
||||
occ.kind = kindEmbed
|
||||
default:
|
||||
occ.err = sqlerr.FunctionNotFound("sqlc." + fn)
|
||||
occ.err.Location = start
|
||||
return occ, occ.end, true
|
||||
}
|
||||
|
||||
args := l.splitArgs(open+1, closing)
|
||||
if len(args) != 1 {
|
||||
occ.err = &sqlerr.Error{
|
||||
Message: fmt.Sprintf("expected 1 parameter to sqlc.%s; got %d", fn, len(args)),
|
||||
Location: start,
|
||||
}
|
||||
return occ, occ.end, true
|
||||
}
|
||||
|
||||
name, ident, ok := argument(l.d, args[0])
|
||||
if !ok {
|
||||
occ.err = &sqlerr.Error{
|
||||
Message: fmt.Sprintf("expected parameter to sqlc.%s to be string or reference; got %q", fn, strings.TrimSpace(args[0])),
|
||||
Location: start,
|
||||
}
|
||||
return occ, occ.end, true
|
||||
}
|
||||
occ.name = name
|
||||
occ.ident = ident
|
||||
return occ, occ.end, true
|
||||
}
|
||||
|
||||
// argument interprets the single argument to a sqlc function. It returns the
|
||||
// parameter name and the text to reuse when rebuilding a reference, which keeps
|
||||
// any identifier quoting the user wrote.
|
||||
func argument(d Dialect, arg string) (name, ident string, ok bool) {
|
||||
arg = strings.TrimSpace(arg)
|
||||
if arg == "" {
|
||||
return "", "", false
|
||||
}
|
||||
switch arg[0] {
|
||||
case '\'':
|
||||
// A string constant names the parameter but is not a reference, so the
|
||||
// rebuilt identifier drops the quotes.
|
||||
s, ok := unquote(arg, '\'')
|
||||
return s, s, ok
|
||||
case '"':
|
||||
s, ok := unquote(arg, '"')
|
||||
if !ok {
|
||||
return "", "", false
|
||||
}
|
||||
if d.DoubleQuoteString {
|
||||
return s, s, true
|
||||
}
|
||||
return s, arg, true
|
||||
case '`':
|
||||
if !d.Backtick {
|
||||
return "", "", false
|
||||
}
|
||||
s, ok := unquote(arg, '`')
|
||||
if !ok {
|
||||
return "", "", false
|
||||
}
|
||||
return s, arg, true
|
||||
}
|
||||
if !isIdentStart(arg[0]) {
|
||||
return "", "", false
|
||||
}
|
||||
for i := 0; i < len(arg); i++ {
|
||||
if !isIdentPart(arg[i]) && arg[i] != '.' {
|
||||
return "", "", false
|
||||
}
|
||||
}
|
||||
// A qualified reference flattens to a single name, matching how the AST
|
||||
// rewrite joined the parts of a ColumnRef.
|
||||
name = strings.ReplaceAll(arg, ".", "")
|
||||
if d.FoldIdentifier {
|
||||
name = strings.ToLower(name)
|
||||
}
|
||||
return name, arg, true
|
||||
}
|
||||
|
||||
func unquote(s string, quote byte) (string, bool) {
|
||||
if len(s) < 2 || s[0] != quote || s[len(s)-1] != quote {
|
||||
return "", false
|
||||
}
|
||||
inner := s[1 : len(s)-1]
|
||||
if strings.IndexByte(inner, quote) >= 0 {
|
||||
doubled := string([]byte{quote, quote})
|
||||
if strings.Contains(inner, doubled) {
|
||||
return strings.ReplaceAll(inner, doubled, string(quote)), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
return inner, true
|
||||
}
|
||||
1
internal/sql/preprocess/testdata/mysql/arg/input.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/mysql/arg/input.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT id FROM users WHERE a = sqlc.arg(x) AND b = sqlc.arg(x);
|
||||
1
internal/sql/preprocess/testdata/mysql/arg/output.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/mysql/arg/output.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT id FROM users WHERE a = ? AND b = ?;
|
||||
23
internal/sql/preprocess/testdata/mysql/arg/side_table.json
vendored
Normal file
23
internal/sql/preprocess/testdata/mysql/arg/side_table.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 43,
|
||||
"dollar": true,
|
||||
"params": [
|
||||
{
|
||||
"number": 1,
|
||||
"location": 31,
|
||||
"origin": 31,
|
||||
"name": "x",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"location": 41,
|
||||
"origin": 51,
|
||||
"name": "x",
|
||||
"named": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
1
internal/sql/preprocess/testdata/mysql/dollar_number_is_not_a_placeholder/input.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/mysql/dollar_number_is_not_a_placeholder/input.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT * FROM second_view WHERE val2 = $1;
|
||||
1
internal/sql/preprocess/testdata/mysql/dollar_number_is_not_a_placeholder/output.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/mysql/dollar_number_is_not_a_placeholder/output.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT * FROM second_view WHERE val2 = $1;
|
||||
7
internal/sql/preprocess/testdata/mysql/dollar_number_is_not_a_placeholder/side_table.json
vendored
Normal file
7
internal/sql/preprocess/testdata/mysql/dollar_number_is_not_a_placeholder/side_table.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 42,
|
||||
"dollar": true
|
||||
}
|
||||
]
|
||||
4
internal/sql/preprocess/testdata/mysql/double_quoted_argument/input.sql
vendored
Normal file
4
internal/sql/preprocess/testdata/mysql/double_quoted_argument/input.sql
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
SELECT
|
||||
(CASE WHEN sqlc.arg("Timezone") = "calendar" THEN cal.Timezone ELSE sqlc.arg("Timezone") END),
|
||||
COALESCE(sqlc.narg("calName"), "")
|
||||
FROM Calendar cal;
|
||||
4
internal/sql/preprocess/testdata/mysql/double_quoted_argument/output.sql
vendored
Normal file
4
internal/sql/preprocess/testdata/mysql/double_quoted_argument/output.sql
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
SELECT
|
||||
(CASE WHEN ? = "calendar" THEN cal.Timezone ELSE ? END),
|
||||
COALESCE(?, "")
|
||||
FROM Calendar cal;
|
||||
31
internal/sql/preprocess/testdata/mysql/double_quoted_argument/side_table.json
vendored
Normal file
31
internal/sql/preprocess/testdata/mysql/double_quoted_argument/side_table.json
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 106,
|
||||
"dollar": true,
|
||||
"params": [
|
||||
{
|
||||
"number": 1,
|
||||
"location": 22,
|
||||
"origin": 22,
|
||||
"name": "Timezone",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"location": 60,
|
||||
"origin": 79,
|
||||
"name": "Timezone",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"number": 3,
|
||||
"location": 81,
|
||||
"origin": 119,
|
||||
"name": "calName",
|
||||
"named": true,
|
||||
"nullable": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
1
internal/sql/preprocess/testdata/mysql/embed/input.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/mysql/embed/input.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT sqlc.embed(users) FROM users;
|
||||
1
internal/sql/preprocess/testdata/mysql/embed/output.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/mysql/embed/output.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT users.* FROM users;
|
||||
15
internal/sql/preprocess/testdata/mysql/embed/side_table.json
vendored
Normal file
15
internal/sql/preprocess/testdata/mysql/embed/side_table.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 26,
|
||||
"dollar": true,
|
||||
"embeds": [
|
||||
{
|
||||
"table": "users",
|
||||
"location": 7,
|
||||
"origin": 7,
|
||||
"orig": "sqlc.embed(users)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
11
internal/sql/preprocess/testdata/mysql/error_invalid_calls/input.sql
vendored
Normal file
11
internal/sql/preprocess/testdata/mysql/error_invalid_calls/input.sql
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
-- name: WrongFunc :one
|
||||
SELECT id FROM users WHERE id = sqlc.argh(target_id);
|
||||
|
||||
-- name: TooManyArgs :one
|
||||
SELECT id FROM users WHERE id = sqlc.arg('foo', 'bar');
|
||||
|
||||
-- name: TooFewArgs :one
|
||||
SELECT id FROM users WHERE id = sqlc.arg();
|
||||
|
||||
-- name: InvalidArgPlaceholder :one
|
||||
SELECT id FROM users WHERE id = sqlc.arg(?);
|
||||
11
internal/sql/preprocess/testdata/mysql/error_invalid_calls/output.sql
vendored
Normal file
11
internal/sql/preprocess/testdata/mysql/error_invalid_calls/output.sql
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
-- name: WrongFunc :one
|
||||
SELECT id FROM users WHERE id = sqlc.argh(target_id);
|
||||
|
||||
-- name: TooManyArgs :one
|
||||
SELECT id FROM users WHERE id = sqlc.arg('foo', 'bar');
|
||||
|
||||
-- name: TooFewArgs :one
|
||||
SELECT id FROM users WHERE id = sqlc.arg();
|
||||
|
||||
-- name: InvalidArgPlaceholder :one
|
||||
SELECT id FROM users WHERE id = sqlc.arg(?);
|
||||
30
internal/sql/preprocess/testdata/mysql/error_invalid_calls/side_table.json
vendored
Normal file
30
internal/sql/preprocess/testdata/mysql/error_invalid_calls/side_table.json
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 77,
|
||||
"dollar": true,
|
||||
"error": "function \"sqlc.argh\" does not exist",
|
||||
"error_at": 56
|
||||
},
|
||||
{
|
||||
"start": 77,
|
||||
"end": 160,
|
||||
"dollar": true,
|
||||
"error": "expected 1 parameter to sqlc.arg; got 2",
|
||||
"error_at": 137
|
||||
},
|
||||
{
|
||||
"start": 160,
|
||||
"end": 230,
|
||||
"dollar": true,
|
||||
"error": "expected 1 parameter to sqlc.arg; got 0",
|
||||
"error_at": 219
|
||||
},
|
||||
{
|
||||
"start": 230,
|
||||
"end": 312,
|
||||
"dollar": true,
|
||||
"error": "expected parameter to sqlc.arg to be string or reference; got \"?\"",
|
||||
"error_at": 300
|
||||
}
|
||||
]
|
||||
4
internal/sql/preprocess/testdata/mysql/error_invalid_calls/stderr.txt
vendored
Normal file
4
internal/sql/preprocess/testdata/mysql/error_invalid_calls/stderr.txt
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
2:33: function "sqlc.argh" does not exist
|
||||
5:33: expected 1 parameter to sqlc.arg; got 2
|
||||
8:33: expected 1 parameter to sqlc.arg; got 0
|
||||
11:33: expected parameter to sqlc.arg to be string or reference; got "?"
|
||||
1
internal/sql/preprocess/testdata/mysql/existing_placeholders_are_numbered_together/input.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/mysql/existing_placeholders_are_numbered_together/input.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT id FROM users WHERE a = ? AND b = sqlc.arg(x) AND c = ?;
|
||||
@@ -0,0 +1 @@
|
||||
SELECT id FROM users WHERE a = ? AND b = ? AND c = ?;
|
||||
@@ -0,0 +1,28 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 53,
|
||||
"dollar": false,
|
||||
"params": [
|
||||
{
|
||||
"number": 1,
|
||||
"location": 31,
|
||||
"origin": 31,
|
||||
"named": false
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"location": 41,
|
||||
"origin": 41,
|
||||
"name": "x",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"number": 3,
|
||||
"location": 51,
|
||||
"origin": 61,
|
||||
"named": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
1
internal/sql/preprocess/testdata/mysql/narg_identifier/input.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/mysql/narg_identifier/input.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT * FROM users WHERE name = sqlc.narg(name);
|
||||
1
internal/sql/preprocess/testdata/mysql/narg_identifier/output.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/mysql/narg_identifier/output.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT * FROM users WHERE name = ?;
|
||||
17
internal/sql/preprocess/testdata/mysql/narg_identifier/side_table.json
vendored
Normal file
17
internal/sql/preprocess/testdata/mysql/narg_identifier/side_table.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 35,
|
||||
"dollar": true,
|
||||
"params": [
|
||||
{
|
||||
"number": 1,
|
||||
"location": 33,
|
||||
"origin": 33,
|
||||
"name": "name",
|
||||
"named": true,
|
||||
"nullable": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
2
internal/sql/preprocess/testdata/mysql/on_duplicate_key_update/input.sql
vendored
Normal file
2
internal/sql/preprocess/testdata/mysql/on_duplicate_key_update/input.sql
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
INSERT INTO t (a) VALUES (sqlc.arg(val))
|
||||
ON DUPLICATE KEY UPDATE a = sqlc.arg(new_val);
|
||||
2
internal/sql/preprocess/testdata/mysql/on_duplicate_key_update/output.sql
vendored
Normal file
2
internal/sql/preprocess/testdata/mysql/on_duplicate_key_update/output.sql
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
INSERT INTO t (a) VALUES (?)
|
||||
ON DUPLICATE KEY UPDATE a = ?;
|
||||
23
internal/sql/preprocess/testdata/mysql/on_duplicate_key_update/side_table.json
vendored
Normal file
23
internal/sql/preprocess/testdata/mysql/on_duplicate_key_update/side_table.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 59,
|
||||
"dollar": true,
|
||||
"params": [
|
||||
{
|
||||
"number": 1,
|
||||
"location": 26,
|
||||
"origin": 26,
|
||||
"name": "val",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"location": 57,
|
||||
"origin": 69,
|
||||
"name": "new_val",
|
||||
"named": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
1
internal/sql/preprocess/testdata/mysql/skip_backslash_escape/input.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/mysql/skip_backslash_escape/input.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT 'a\' sqlc.arg(nope)';
|
||||
1
internal/sql/preprocess/testdata/mysql/skip_backslash_escape/output.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/mysql/skip_backslash_escape/output.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT 'a\' sqlc.arg(nope)';
|
||||
7
internal/sql/preprocess/testdata/mysql/skip_backslash_escape/side_table.json
vendored
Normal file
7
internal/sql/preprocess/testdata/mysql/skip_backslash_escape/side_table.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 28,
|
||||
"dollar": true
|
||||
}
|
||||
]
|
||||
1
internal/sql/preprocess/testdata/mysql/skip_backtick_identifier/input.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/mysql/skip_backtick_identifier/input.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT `sqlc.arg(nope)` FROM t;
|
||||
1
internal/sql/preprocess/testdata/mysql/skip_backtick_identifier/output.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/mysql/skip_backtick_identifier/output.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT `sqlc.arg(nope)` FROM t;
|
||||
7
internal/sql/preprocess/testdata/mysql/skip_backtick_identifier/side_table.json
vendored
Normal file
7
internal/sql/preprocess/testdata/mysql/skip_backtick_identifier/side_table.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 31,
|
||||
"dollar": true
|
||||
}
|
||||
]
|
||||
1
internal/sql/preprocess/testdata/mysql/skip_double_quoted_string/input.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/mysql/skip_double_quoted_string/input.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT "sqlc.arg(nope)" FROM t;
|
||||
1
internal/sql/preprocess/testdata/mysql/skip_double_quoted_string/output.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/mysql/skip_double_quoted_string/output.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT "sqlc.arg(nope)" FROM t;
|
||||
7
internal/sql/preprocess/testdata/mysql/skip_double_quoted_string/side_table.json
vendored
Normal file
7
internal/sql/preprocess/testdata/mysql/skip_double_quoted_string/side_table.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 31,
|
||||
"dollar": true
|
||||
}
|
||||
]
|
||||
2
internal/sql/preprocess/testdata/mysql/skip_hash_comment/input.sql
vendored
Normal file
2
internal/sql/preprocess/testdata/mysql/skip_hash_comment/input.sql
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# sqlc.arg(nope)
|
||||
SELECT 1;
|
||||
2
internal/sql/preprocess/testdata/mysql/skip_hash_comment/output.sql
vendored
Normal file
2
internal/sql/preprocess/testdata/mysql/skip_hash_comment/output.sql
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# sqlc.arg(nope)
|
||||
SELECT 1;
|
||||
7
internal/sql/preprocess/testdata/mysql/skip_hash_comment/side_table.json
vendored
Normal file
7
internal/sql/preprocess/testdata/mysql/skip_hash_comment/side_table.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 26,
|
||||
"dollar": true
|
||||
}
|
||||
]
|
||||
1
internal/sql/preprocess/testdata/mysql/slice/input.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/mysql/slice/input.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT id FROM users WHERE id IN (sqlc.slice(ids));
|
||||
1
internal/sql/preprocess/testdata/mysql/slice/output.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/mysql/slice/output.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT id FROM users WHERE id IN (/*SLICE:ids*/?);
|
||||
17
internal/sql/preprocess/testdata/mysql/slice/side_table.json
vendored
Normal file
17
internal/sql/preprocess/testdata/mysql/slice/side_table.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 50,
|
||||
"dollar": true,
|
||||
"params": [
|
||||
{
|
||||
"number": 1,
|
||||
"location": 47,
|
||||
"origin": 34,
|
||||
"name": "ids",
|
||||
"named": true,
|
||||
"slice": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
1
internal/sql/preprocess/testdata/mysql/slice_folds_identifier/input.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/mysql/slice_folds_identifier/input.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT * FROM foo WHERE retyped IN (sqlc.slice(paramName));
|
||||
1
internal/sql/preprocess/testdata/mysql/slice_folds_identifier/output.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/mysql/slice_folds_identifier/output.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT * FROM foo WHERE retyped IN (/*SLICE:paramname*/?);
|
||||
17
internal/sql/preprocess/testdata/mysql/slice_folds_identifier/side_table.json
vendored
Normal file
17
internal/sql/preprocess/testdata/mysql/slice_folds_identifier/side_table.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 58,
|
||||
"dollar": true,
|
||||
"params": [
|
||||
{
|
||||
"number": 1,
|
||||
"location": 55,
|
||||
"origin": 36,
|
||||
"name": "paramname",
|
||||
"named": true,
|
||||
"slice": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
1
internal/sql/preprocess/testdata/mysql/string_constant/input.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/mysql/string_constant/input.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT id FROM users WHERE a = sqlc.arg('x') AND b = sqlc.narg('y') AND c IN (sqlc.slice('ids'));
|
||||
1
internal/sql/preprocess/testdata/mysql/string_constant/output.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/mysql/string_constant/output.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT id FROM users WHERE a = ? AND b = ? AND c IN (/*SLICE:ids*/?);
|
||||
32
internal/sql/preprocess/testdata/mysql/string_constant/side_table.json
vendored
Normal file
32
internal/sql/preprocess/testdata/mysql/string_constant/side_table.json
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 69,
|
||||
"dollar": true,
|
||||
"params": [
|
||||
{
|
||||
"number": 1,
|
||||
"location": 31,
|
||||
"origin": 31,
|
||||
"name": "x",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"location": 41,
|
||||
"origin": 53,
|
||||
"name": "y",
|
||||
"named": true,
|
||||
"nullable": true
|
||||
},
|
||||
{
|
||||
"number": 3,
|
||||
"location": 66,
|
||||
"origin": 78,
|
||||
"name": "ids",
|
||||
"named": true,
|
||||
"slice": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
1
internal/sql/preprocess/testdata/mysql/string_constant_keeps_case/input.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/mysql/string_constant_keeps_case/input.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT sqlc.arg('MixedCase'), sqlc.arg(MixedCase);
|
||||
1
internal/sql/preprocess/testdata/mysql/string_constant_keeps_case/output.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/mysql/string_constant_keeps_case/output.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT ?, ?;
|
||||
23
internal/sql/preprocess/testdata/mysql/string_constant_keeps_case/side_table.json
vendored
Normal file
23
internal/sql/preprocess/testdata/mysql/string_constant_keeps_case/side_table.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 12,
|
||||
"dollar": true,
|
||||
"params": [
|
||||
{
|
||||
"number": 1,
|
||||
"location": 7,
|
||||
"origin": 7,
|
||||
"name": "MixedCase",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"location": 10,
|
||||
"origin": 30,
|
||||
"name": "mixedcase",
|
||||
"named": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
1
internal/sql/preprocess/testdata/mysql/user_variables_are_not_parameters/input.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/mysql/user_variables_are_not_parameters/input.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT @row := @row + 1 FROM users WHERE id = sqlc.arg(id);
|
||||
1
internal/sql/preprocess/testdata/mysql/user_variables_are_not_parameters/output.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/mysql/user_variables_are_not_parameters/output.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT @row := @row + 1 FROM users WHERE id = ?;
|
||||
16
internal/sql/preprocess/testdata/mysql/user_variables_are_not_parameters/side_table.json
vendored
Normal file
16
internal/sql/preprocess/testdata/mysql/user_variables_are_not_parameters/side_table.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 48,
|
||||
"dollar": true,
|
||||
"params": [
|
||||
{
|
||||
"number": 1,
|
||||
"location": 46,
|
||||
"origin": 46,
|
||||
"name": "id",
|
||||
"named": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
2
internal/sql/preprocess/testdata/postgresql/arg/input.sql
vendored
Normal file
2
internal/sql/preprocess/testdata/postgresql/arg/input.sql
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
-- name: GetUser :one
|
||||
SELECT id FROM users WHERE name = sqlc.arg(name);
|
||||
2
internal/sql/preprocess/testdata/postgresql/arg/output.sql
vendored
Normal file
2
internal/sql/preprocess/testdata/postgresql/arg/output.sql
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
-- name: GetUser :one
|
||||
SELECT id FROM users WHERE name = $1;
|
||||
16
internal/sql/preprocess/testdata/postgresql/arg/side_table.json
vendored
Normal file
16
internal/sql/preprocess/testdata/postgresql/arg/side_table.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 59,
|
||||
"dollar": true,
|
||||
"params": [
|
||||
{
|
||||
"number": 1,
|
||||
"location": 56,
|
||||
"origin": 56,
|
||||
"name": "name",
|
||||
"named": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
1
internal/sql/preprocess/testdata/postgresql/at_sign/input.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/postgresql/at_sign/input.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT id FROM users WHERE name = @name AND age = @age;
|
||||
1
internal/sql/preprocess/testdata/postgresql/at_sign/output.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/postgresql/at_sign/output.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT id FROM users WHERE name = $1 AND age = $2;
|
||||
23
internal/sql/preprocess/testdata/postgresql/at_sign/side_table.json
vendored
Normal file
23
internal/sql/preprocess/testdata/postgresql/at_sign/side_table.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 50,
|
||||
"dollar": true,
|
||||
"params": [
|
||||
{
|
||||
"number": 1,
|
||||
"location": 34,
|
||||
"origin": 34,
|
||||
"name": "name",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"location": 47,
|
||||
"origin": 50,
|
||||
"name": "age",
|
||||
"named": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
1
internal/sql/preprocess/testdata/postgresql/at_sign_cast/input.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/postgresql/at_sign_cast/input.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT id FROM users WHERE ok = @flag::bool;
|
||||
1
internal/sql/preprocess/testdata/postgresql/at_sign_cast/output.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/postgresql/at_sign_cast/output.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT id FROM users WHERE ok = $1::bool;
|
||||
16
internal/sql/preprocess/testdata/postgresql/at_sign_cast/side_table.json
vendored
Normal file
16
internal/sql/preprocess/testdata/postgresql/at_sign_cast/side_table.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 41,
|
||||
"dollar": true,
|
||||
"params": [
|
||||
{
|
||||
"number": 1,
|
||||
"location": 32,
|
||||
"origin": 32,
|
||||
"name": "flag",
|
||||
"named": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
3
internal/sql/preprocess/testdata/postgresql/at_sign_in_vet_directive/input.sql
vendored
Normal file
3
internal/sql/preprocess/testdata/postgresql/at_sign_in_vet_directive/input.sql
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
-- name: SkipVetAll :exec
|
||||
-- @sqlc-vet-disable always-fail no-exec
|
||||
SELECT true FROM users WHERE id = sqlc.arg(id);
|
||||
3
internal/sql/preprocess/testdata/postgresql/at_sign_in_vet_directive/output.sql
vendored
Normal file
3
internal/sql/preprocess/testdata/postgresql/at_sign_in_vet_directive/output.sql
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
-- name: SkipVetAll :exec
|
||||
-- @sqlc-vet-disable always-fail no-exec
|
||||
SELECT true FROM users WHERE id = $1;
|
||||
16
internal/sql/preprocess/testdata/postgresql/at_sign_in_vet_directive/side_table.json
vendored
Normal file
16
internal/sql/preprocess/testdata/postgresql/at_sign_in_vet_directive/side_table.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 104,
|
||||
"dollar": true,
|
||||
"params": [
|
||||
{
|
||||
"number": 1,
|
||||
"location": 101,
|
||||
"origin": 101,
|
||||
"name": "id",
|
||||
"named": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
1
internal/sql/preprocess/testdata/postgresql/comment_inside_call/input.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/postgresql/comment_inside_call/input.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT sqlc.arg/* here */(name);
|
||||
1
internal/sql/preprocess/testdata/postgresql/comment_inside_call/output.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/postgresql/comment_inside_call/output.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT $1;
|
||||
16
internal/sql/preprocess/testdata/postgresql/comment_inside_call/side_table.json
vendored
Normal file
16
internal/sql/preprocess/testdata/postgresql/comment_inside_call/side_table.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 10,
|
||||
"dollar": true,
|
||||
"params": [
|
||||
{
|
||||
"number": 1,
|
||||
"location": 7,
|
||||
"origin": 7,
|
||||
"name": "name",
|
||||
"named": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
1
internal/sql/preprocess/testdata/postgresql/embed/input.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/postgresql/embed/input.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT sqlc.embed(users), sqlc.embed(pets) FROM users JOIN pets ON true;
|
||||
1
internal/sql/preprocess/testdata/postgresql/embed/output.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/postgresql/embed/output.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT users.*, pets.* FROM users JOIN pets ON true;
|
||||
21
internal/sql/preprocess/testdata/postgresql/embed/side_table.json
vendored
Normal file
21
internal/sql/preprocess/testdata/postgresql/embed/side_table.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 52,
|
||||
"dollar": true,
|
||||
"embeds": [
|
||||
{
|
||||
"table": "users",
|
||||
"location": 7,
|
||||
"origin": 7,
|
||||
"orig": "sqlc.embed(users)"
|
||||
},
|
||||
{
|
||||
"table": "pets",
|
||||
"location": 16,
|
||||
"origin": 26,
|
||||
"orig": "sqlc.embed(pets)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
1
internal/sql/preprocess/testdata/postgresql/embed_and_user_star/input.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/postgresql/embed_and_user_star/input.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT sqlc.embed(a), b.* FROM a, b;
|
||||
1
internal/sql/preprocess/testdata/postgresql/embed_and_user_star/output.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/postgresql/embed_and_user_star/output.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT a.*, b.* FROM a, b;
|
||||
15
internal/sql/preprocess/testdata/postgresql/embed_and_user_star/side_table.json
vendored
Normal file
15
internal/sql/preprocess/testdata/postgresql/embed_and_user_star/side_table.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 26,
|
||||
"dollar": true,
|
||||
"embeds": [
|
||||
{
|
||||
"table": "a",
|
||||
"location": 7,
|
||||
"origin": 7,
|
||||
"orig": "sqlc.embed(a)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
1
internal/sql/preprocess/testdata/postgresql/embed_quoted_identifier/input.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/postgresql/embed_quoted_identifier/input.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT sqlc.embed("Users") FROM "Users";
|
||||
1
internal/sql/preprocess/testdata/postgresql/embed_quoted_identifier/output.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/postgresql/embed_quoted_identifier/output.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT "Users".* FROM "Users";
|
||||
15
internal/sql/preprocess/testdata/postgresql/embed_quoted_identifier/side_table.json
vendored
Normal file
15
internal/sql/preprocess/testdata/postgresql/embed_quoted_identifier/side_table.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 30,
|
||||
"dollar": true,
|
||||
"embeds": [
|
||||
{
|
||||
"table": "Users",
|
||||
"location": 7,
|
||||
"origin": 7,
|
||||
"orig": "sqlc.embed(\"Users\")"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
1
internal/sql/preprocess/testdata/postgresql/embed_string_constant/input.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/postgresql/embed_string_constant/input.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT sqlc.embed('t') FROM t;
|
||||
1
internal/sql/preprocess/testdata/postgresql/embed_string_constant/output.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/postgresql/embed_string_constant/output.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT t.* FROM t;
|
||||
15
internal/sql/preprocess/testdata/postgresql/embed_string_constant/side_table.json
vendored
Normal file
15
internal/sql/preprocess/testdata/postgresql/embed_string_constant/side_table.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 18,
|
||||
"dollar": true,
|
||||
"embeds": [
|
||||
{
|
||||
"table": "t",
|
||||
"location": 7,
|
||||
"origin": 7,
|
||||
"orig": "sqlc.embed(t)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
1
internal/sql/preprocess/testdata/postgresql/embed_with_args/input.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/postgresql/embed_with_args/input.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT sqlc.embed(a), b.name FROM a JOIN b ON b.id = a.b_id WHERE a.id = sqlc.arg(id);
|
||||
1
internal/sql/preprocess/testdata/postgresql/embed_with_args/output.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/postgresql/embed_with_args/output.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT a.*, b.name FROM a JOIN b ON b.id = a.b_id WHERE a.id = $1;
|
||||
24
internal/sql/preprocess/testdata/postgresql/embed_with_args/side_table.json
vendored
Normal file
24
internal/sql/preprocess/testdata/postgresql/embed_with_args/side_table.json
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 66,
|
||||
"dollar": true,
|
||||
"params": [
|
||||
{
|
||||
"number": 1,
|
||||
"location": 63,
|
||||
"origin": 73,
|
||||
"name": "id",
|
||||
"named": true
|
||||
}
|
||||
],
|
||||
"embeds": [
|
||||
{
|
||||
"table": "a",
|
||||
"location": 7,
|
||||
"origin": 7,
|
||||
"orig": "sqlc.embed(a)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
2
internal/sql/preprocess/testdata/postgresql/error_is_per_statement/input.sql
vendored
Normal file
2
internal/sql/preprocess/testdata/postgresql/error_is_per_statement/input.sql
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
SELECT sqlc.argh(a);
|
||||
SELECT sqlc.arg(b);
|
||||
2
internal/sql/preprocess/testdata/postgresql/error_is_per_statement/output.sql
vendored
Normal file
2
internal/sql/preprocess/testdata/postgresql/error_is_per_statement/output.sql
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
SELECT sqlc.argh(a);
|
||||
SELECT $1;
|
||||
23
internal/sql/preprocess/testdata/postgresql/error_is_per_statement/side_table.json
vendored
Normal file
23
internal/sql/preprocess/testdata/postgresql/error_is_per_statement/side_table.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 20,
|
||||
"dollar": true,
|
||||
"error": "function \"sqlc.argh\" does not exist",
|
||||
"error_at": 7
|
||||
},
|
||||
{
|
||||
"start": 20,
|
||||
"end": 31,
|
||||
"dollar": true,
|
||||
"params": [
|
||||
{
|
||||
"number": 1,
|
||||
"location": 28,
|
||||
"origin": 28,
|
||||
"name": "b",
|
||||
"named": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
1
internal/sql/preprocess/testdata/postgresql/error_is_per_statement/stderr.txt
vendored
Normal file
1
internal/sql/preprocess/testdata/postgresql/error_is_per_statement/stderr.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
1:8: function "sqlc.argh" does not exist
|
||||
1
internal/sql/preprocess/testdata/postgresql/error_nested_call/input.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/postgresql/error_nested_call/input.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT sqlc.arg(sqlc.arg(a));
|
||||
1
internal/sql/preprocess/testdata/postgresql/error_nested_call/output.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/postgresql/error_nested_call/output.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT sqlc.arg(sqlc.arg(a));
|
||||
9
internal/sql/preprocess/testdata/postgresql/error_nested_call/side_table.json
vendored
Normal file
9
internal/sql/preprocess/testdata/postgresql/error_nested_call/side_table.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 29,
|
||||
"dollar": true,
|
||||
"error": "expected parameter to sqlc.arg to be string or reference; got \"sqlc.arg(a)\"",
|
||||
"error_at": 7
|
||||
}
|
||||
]
|
||||
1
internal/sql/preprocess/testdata/postgresql/error_nested_call/stderr.txt
vendored
Normal file
1
internal/sql/preprocess/testdata/postgresql/error_nested_call/stderr.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
1:8: expected parameter to sqlc.arg to be string or reference; got "sqlc.arg(a)"
|
||||
1
internal/sql/preprocess/testdata/postgresql/error_no_arguments/input.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/postgresql/error_no_arguments/input.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT sqlc.arg();
|
||||
1
internal/sql/preprocess/testdata/postgresql/error_no_arguments/output.sql
vendored
Normal file
1
internal/sql/preprocess/testdata/postgresql/error_no_arguments/output.sql
vendored
Normal file
@@ -0,0 +1 @@
|
||||
SELECT sqlc.arg();
|
||||
9
internal/sql/preprocess/testdata/postgresql/error_no_arguments/side_table.json
vendored
Normal file
9
internal/sql/preprocess/testdata/postgresql/error_no_arguments/side_table.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"start": 0,
|
||||
"end": 18,
|
||||
"dollar": true,
|
||||
"error": "expected 1 parameter to sqlc.arg; got 0",
|
||||
"error_at": 7
|
||||
}
|
||||
]
|
||||
1
internal/sql/preprocess/testdata/postgresql/error_no_arguments/stderr.txt
vendored
Normal file
1
internal/sql/preprocess/testdata/postgresql/error_no_arguments/stderr.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
1:8: expected 1 parameter to sqlc.arg; got 0
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user