Files
upstream-mirror 0f4df23290
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
snapshot of https://github.com/sqlc-dev/sqlc.git (main, 2026-09-16; blobs > 131072B omitted)
2026-09-16 02:17:34 +05:00

115 lines
4.8 KiB
Markdown

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