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

This commit is contained in:
upstream-mirror
2026-09-16 02:17:34 +05:00
commit 0f4df23290
7340 changed files with 238743 additions and 0 deletions

View 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.

View 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
}

View 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)
}

View 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 "?"
}
}

View 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)
}
}

View 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
}

View File

@@ -0,0 +1 @@
SELECT id FROM users WHERE a = sqlc.arg(x) AND b = sqlc.arg(x);

View File

@@ -0,0 +1 @@
SELECT id FROM users WHERE a = ? AND b = ?;

View 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
}
]
}
]

View File

@@ -0,0 +1 @@
SELECT * FROM second_view WHERE val2 = $1;

View File

@@ -0,0 +1 @@
SELECT * FROM second_view WHERE val2 = $1;

View File

@@ -0,0 +1,7 @@
[
{
"start": 0,
"end": 42,
"dollar": true
}
]

View 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;

View File

@@ -0,0 +1,4 @@
SELECT
(CASE WHEN ? = "calendar" THEN cal.Timezone ELSE ? END),
COALESCE(?, "")
FROM Calendar cal;

View 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
}
]
}
]

View File

@@ -0,0 +1 @@
SELECT sqlc.embed(users) FROM users;

View File

@@ -0,0 +1 @@
SELECT users.* FROM users;

View File

@@ -0,0 +1,15 @@
[
{
"start": 0,
"end": 26,
"dollar": true,
"embeds": [
{
"table": "users",
"location": 7,
"origin": 7,
"orig": "sqlc.embed(users)"
}
]
}
]

View 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(?);

View 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(?);

View 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
}
]

View 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 "?"

View File

@@ -0,0 +1 @@
SELECT id FROM users WHERE a = ? AND b = sqlc.arg(x) AND c = ?;

View File

@@ -0,0 +1 @@
SELECT id FROM users WHERE a = ? AND b = ? AND c = ?;

View File

@@ -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
}
]
}
]

View File

@@ -0,0 +1 @@
SELECT * FROM users WHERE name = sqlc.narg(name);

View File

@@ -0,0 +1 @@
SELECT * FROM users WHERE name = ?;

View File

@@ -0,0 +1,17 @@
[
{
"start": 0,
"end": 35,
"dollar": true,
"params": [
{
"number": 1,
"location": 33,
"origin": 33,
"name": "name",
"named": true,
"nullable": true
}
]
}
]

View File

@@ -0,0 +1,2 @@
INSERT INTO t (a) VALUES (sqlc.arg(val))
ON DUPLICATE KEY UPDATE a = sqlc.arg(new_val);

View File

@@ -0,0 +1,2 @@
INSERT INTO t (a) VALUES (?)
ON DUPLICATE KEY UPDATE a = ?;

View 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
}
]
}
]

View File

@@ -0,0 +1 @@
SELECT 'a\' sqlc.arg(nope)';

View File

@@ -0,0 +1 @@
SELECT 'a\' sqlc.arg(nope)';

View File

@@ -0,0 +1,7 @@
[
{
"start": 0,
"end": 28,
"dollar": true
}
]

View File

@@ -0,0 +1 @@
SELECT `sqlc.arg(nope)` FROM t;

View File

@@ -0,0 +1 @@
SELECT `sqlc.arg(nope)` FROM t;

View File

@@ -0,0 +1,7 @@
[
{
"start": 0,
"end": 31,
"dollar": true
}
]

View File

@@ -0,0 +1 @@
SELECT "sqlc.arg(nope)" FROM t;

View File

@@ -0,0 +1 @@
SELECT "sqlc.arg(nope)" FROM t;

View File

@@ -0,0 +1,7 @@
[
{
"start": 0,
"end": 31,
"dollar": true
}
]

View File

@@ -0,0 +1,2 @@
# sqlc.arg(nope)
SELECT 1;

View File

@@ -0,0 +1,2 @@
# sqlc.arg(nope)
SELECT 1;

View File

@@ -0,0 +1,7 @@
[
{
"start": 0,
"end": 26,
"dollar": true
}
]

View File

@@ -0,0 +1 @@
SELECT id FROM users WHERE id IN (sqlc.slice(ids));

View File

@@ -0,0 +1 @@
SELECT id FROM users WHERE id IN (/*SLICE:ids*/?);

View File

@@ -0,0 +1,17 @@
[
{
"start": 0,
"end": 50,
"dollar": true,
"params": [
{
"number": 1,
"location": 47,
"origin": 34,
"name": "ids",
"named": true,
"slice": true
}
]
}
]

View File

@@ -0,0 +1 @@
SELECT * FROM foo WHERE retyped IN (sqlc.slice(paramName));

View File

@@ -0,0 +1 @@
SELECT * FROM foo WHERE retyped IN (/*SLICE:paramname*/?);

View File

@@ -0,0 +1,17 @@
[
{
"start": 0,
"end": 58,
"dollar": true,
"params": [
{
"number": 1,
"location": 55,
"origin": 36,
"name": "paramname",
"named": true,
"slice": true
}
]
}
]

View File

@@ -0,0 +1 @@
SELECT id FROM users WHERE a = sqlc.arg('x') AND b = sqlc.narg('y') AND c IN (sqlc.slice('ids'));

View File

@@ -0,0 +1 @@
SELECT id FROM users WHERE a = ? AND b = ? AND c IN (/*SLICE:ids*/?);

View 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
}
]
}
]

View File

@@ -0,0 +1 @@
SELECT sqlc.arg('MixedCase'), sqlc.arg(MixedCase);

View File

@@ -0,0 +1 @@
SELECT ?, ?;

View 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
}
]
}
]

View File

@@ -0,0 +1 @@
SELECT @row := @row + 1 FROM users WHERE id = sqlc.arg(id);

View File

@@ -0,0 +1 @@
SELECT @row := @row + 1 FROM users WHERE id = ?;

View File

@@ -0,0 +1,16 @@
[
{
"start": 0,
"end": 48,
"dollar": true,
"params": [
{
"number": 1,
"location": 46,
"origin": 46,
"name": "id",
"named": true
}
]
}
]

View File

@@ -0,0 +1,2 @@
-- name: GetUser :one
SELECT id FROM users WHERE name = sqlc.arg(name);

View File

@@ -0,0 +1,2 @@
-- name: GetUser :one
SELECT id FROM users WHERE name = $1;

View File

@@ -0,0 +1,16 @@
[
{
"start": 0,
"end": 59,
"dollar": true,
"params": [
{
"number": 1,
"location": 56,
"origin": 56,
"name": "name",
"named": true
}
]
}
]

View File

@@ -0,0 +1 @@
SELECT id FROM users WHERE name = @name AND age = @age;

View File

@@ -0,0 +1 @@
SELECT id FROM users WHERE name = $1 AND age = $2;

View 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
}
]
}
]

View File

@@ -0,0 +1 @@
SELECT id FROM users WHERE ok = @flag::bool;

View File

@@ -0,0 +1 @@
SELECT id FROM users WHERE ok = $1::bool;

View File

@@ -0,0 +1,16 @@
[
{
"start": 0,
"end": 41,
"dollar": true,
"params": [
{
"number": 1,
"location": 32,
"origin": 32,
"name": "flag",
"named": true
}
]
}
]

View File

@@ -0,0 +1,3 @@
-- name: SkipVetAll :exec
-- @sqlc-vet-disable always-fail no-exec
SELECT true FROM users WHERE id = sqlc.arg(id);

View File

@@ -0,0 +1,3 @@
-- name: SkipVetAll :exec
-- @sqlc-vet-disable always-fail no-exec
SELECT true FROM users WHERE id = $1;

View File

@@ -0,0 +1,16 @@
[
{
"start": 0,
"end": 104,
"dollar": true,
"params": [
{
"number": 1,
"location": 101,
"origin": 101,
"name": "id",
"named": true
}
]
}
]

View File

@@ -0,0 +1 @@
SELECT sqlc.arg/* here */(name);

View File

@@ -0,0 +1 @@
SELECT $1;

View File

@@ -0,0 +1,16 @@
[
{
"start": 0,
"end": 10,
"dollar": true,
"params": [
{
"number": 1,
"location": 7,
"origin": 7,
"name": "name",
"named": true
}
]
}
]

View File

@@ -0,0 +1 @@
SELECT sqlc.embed(users), sqlc.embed(pets) FROM users JOIN pets ON true;

View File

@@ -0,0 +1 @@
SELECT users.*, pets.* FROM users JOIN pets ON true;

View 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)"
}
]
}
]

View File

@@ -0,0 +1 @@
SELECT sqlc.embed(a), b.* FROM a, b;

View File

@@ -0,0 +1 @@
SELECT a.*, b.* FROM a, b;

View File

@@ -0,0 +1,15 @@
[
{
"start": 0,
"end": 26,
"dollar": true,
"embeds": [
{
"table": "a",
"location": 7,
"origin": 7,
"orig": "sqlc.embed(a)"
}
]
}
]

View File

@@ -0,0 +1 @@
SELECT sqlc.embed("Users") FROM "Users";

View File

@@ -0,0 +1 @@
SELECT "Users".* FROM "Users";

View File

@@ -0,0 +1,15 @@
[
{
"start": 0,
"end": 30,
"dollar": true,
"embeds": [
{
"table": "Users",
"location": 7,
"origin": 7,
"orig": "sqlc.embed(\"Users\")"
}
]
}
]

View File

@@ -0,0 +1 @@
SELECT sqlc.embed('t') FROM t;

View File

@@ -0,0 +1 @@
SELECT t.* FROM t;

View File

@@ -0,0 +1,15 @@
[
{
"start": 0,
"end": 18,
"dollar": true,
"embeds": [
{
"table": "t",
"location": 7,
"origin": 7,
"orig": "sqlc.embed(t)"
}
]
}
]

View 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);

View File

@@ -0,0 +1 @@
SELECT a.*, b.name FROM a JOIN b ON b.id = a.b_id WHERE a.id = $1;

View 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)"
}
]
}
]

View File

@@ -0,0 +1,2 @@
SELECT sqlc.argh(a);
SELECT sqlc.arg(b);

View File

@@ -0,0 +1,2 @@
SELECT sqlc.argh(a);
SELECT $1;

View 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
}
]
}
]

View File

@@ -0,0 +1 @@
1:8: function "sqlc.argh" does not exist

View File

@@ -0,0 +1 @@
SELECT sqlc.arg(sqlc.arg(a));

View File

@@ -0,0 +1 @@
SELECT sqlc.arg(sqlc.arg(a));

View 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
}
]

View File

@@ -0,0 +1 @@
1:8: expected parameter to sqlc.arg to be string or reference; got "sqlc.arg(a)"

View File

@@ -0,0 +1 @@
SELECT sqlc.arg();

View File

@@ -0,0 +1 @@
SELECT sqlc.arg();

View File

@@ -0,0 +1,9 @@
[
{
"start": 0,
"end": 18,
"dollar": true,
"error": "expected 1 parameter to sqlc.arg; got 0",
"error_at": 7
}
]

View 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