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:
58
internal/sql/named/CLAUDE.md
Normal file
58
internal/sql/named/CLAUDE.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Named Parameters Package - Claude Code Guide
|
||||
|
||||
This package models a query's parameters: their names, their nullability and
|
||||
whether they came from `sqlc.slice()`.
|
||||
|
||||
It no longer knows anything about sqlc's *syntax*. `sqlc.arg()`, `sqlc.narg()`,
|
||||
`sqlc.slice()` and `@name` are rewritten to native placeholders by
|
||||
`internal/sql/preprocess` before any engine parser runs, and that package builds
|
||||
the `ParamSet` while it rewrites.
|
||||
|
||||
## Param
|
||||
|
||||
A `Param` carries the user-facing name plus a nullability that combines what was
|
||||
inferred from the schema with what the user asked for:
|
||||
|
||||
- `NewParam(name)` — unspecified nullability, from `sqlc.arg()` or `@name`
|
||||
- `NewUserNullableParam(name)` — from `sqlc.narg()`, always nullable
|
||||
- `NewSqlcSlice(name)` — from `sqlc.slice()`
|
||||
- `NewInferredParam(name, notNull)` — what the compiler inferred
|
||||
|
||||
A user-specified nullability outranks an inferred one. `mergeParam` ORs the
|
||||
nullability bits together, so the order the two sources arrive in does not
|
||||
matter.
|
||||
|
||||
## ParamSet
|
||||
|
||||
`ParamSet` maps placeholder numbers to `Param` values for a single statement.
|
||||
|
||||
```go
|
||||
ps := named.NewParamSet(numbersAlreadyUsed, hasNamedSupport)
|
||||
n := ps.Add(named.NewParam("author_id")) // returns the placeholder number
|
||||
```
|
||||
|
||||
`hasNamedSupport` is false for dialects that send one argument per `?`
|
||||
(MySQL, ClickHouse). There, every occurrence of a name gets its own number. For
|
||||
`$1`/`?1`/`@name` dialects repeated uses of a name share a number.
|
||||
|
||||
The compiler reads the set back with:
|
||||
|
||||
- `NameFor(number)` — the user-facing name for a placeholder
|
||||
- `FetchMerge(number, inferred)` — the merged param and whether it was named
|
||||
|
||||
## MySQL @variable vs sqlc @param
|
||||
|
||||
`@name` is only sqlc syntax where the preprocessor's dialect says it is. MySQL
|
||||
sets `AtSign: false`, so `@user_id` there is a user variable and reaches the
|
||||
parser untouched (converted to `ast.VariableExpr`). PostgreSQL, SQLite and
|
||||
GoogleSQL treat `@name` as a named parameter.
|
||||
|
||||
```sql
|
||||
-- PostgreSQL with sqlc @param syntax:
|
||||
SELECT * FROM users WHERE id = @user_id
|
||||
-- Preprocessed to: SELECT * FROM users WHERE id = $1
|
||||
|
||||
-- MySQL with user variable:
|
||||
SELECT * FROM users WHERE id != @user_id
|
||||
-- Stays: SELECT * FROM users WHERE id != @user_id
|
||||
```
|
||||
129
internal/sql/named/param.go
Normal file
129
internal/sql/named/param.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package named
|
||||
|
||||
// nullability represents the nullability of a named parameter.
|
||||
// The nullability can be:
|
||||
// 1. unspecified
|
||||
// 2. inferred
|
||||
// 3. user-defined
|
||||
// A user-specified nullability carries a higher precedence than an inferred one
|
||||
//
|
||||
// The representation is such that you can bitwise OR together nullability types to
|
||||
// combine them together.
|
||||
type nullability int
|
||||
|
||||
const (
|
||||
nullUnspecified nullability = 0b0000
|
||||
inferredNull nullability = 0b0001
|
||||
inferredNotNull nullability = 0b0010
|
||||
nullable nullability = 0b0100
|
||||
notNullable nullability = 0b1000
|
||||
)
|
||||
|
||||
// String implements the Stringer interface
|
||||
func (n nullability) String() string {
|
||||
switch n {
|
||||
case nullUnspecified:
|
||||
return "NullUnspecified"
|
||||
case inferredNull:
|
||||
return "InferredNull"
|
||||
case inferredNotNull:
|
||||
return "InferredNotNull"
|
||||
case nullable:
|
||||
return "Nullable"
|
||||
case notNullable:
|
||||
return "NotNullable"
|
||||
default:
|
||||
return "NullInvalid"
|
||||
}
|
||||
}
|
||||
|
||||
// Param represents a input argument to the query which can be specified using:
|
||||
// - positional parameters $1
|
||||
// - named parameter operator @param
|
||||
// - named parameter function calls sqlc.arg(param)
|
||||
type Param struct {
|
||||
name string
|
||||
nullability nullability
|
||||
isSqlcSlice bool
|
||||
}
|
||||
|
||||
// NewParam builds a new params with unspecified nullability
|
||||
func NewParam(name string) Param {
|
||||
return Param{name: name, nullability: nullUnspecified}
|
||||
}
|
||||
|
||||
// NewInferredParam builds a new params with inferred nullability
|
||||
func NewInferredParam(name string, notNull bool) Param {
|
||||
if notNull {
|
||||
return Param{name: name, nullability: inferredNotNull}
|
||||
}
|
||||
|
||||
return Param{name: name, nullability: inferredNull}
|
||||
}
|
||||
|
||||
// NewUserNullableParam is a parameter that has been overridden
|
||||
// by the user to be nullable.
|
||||
func NewUserNullableParam(name string) Param {
|
||||
return Param{name: name, nullability: nullable}
|
||||
}
|
||||
|
||||
// NewSqlcSlice is a sqlc.slice() parameter.
|
||||
func NewSqlcSlice(name string) Param {
|
||||
return Param{name: name, nullability: nullUnspecified, isSqlcSlice: true}
|
||||
}
|
||||
|
||||
// Name is the user defined name to use for this parameter
|
||||
func (p Param) Name() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// is checks if this params object has the specified nullability bit set
|
||||
func (p Param) is(n nullability) bool {
|
||||
return (p.nullability & n) == n
|
||||
}
|
||||
|
||||
// NonNull determines whether this param should be "not null" in its current state
|
||||
func (p Param) NotNull() bool {
|
||||
const null = false
|
||||
const notNull = true
|
||||
|
||||
if p.is(notNullable) {
|
||||
return notNull
|
||||
}
|
||||
|
||||
if p.is(nullable) {
|
||||
return null
|
||||
}
|
||||
|
||||
if p.is(inferredNotNull) {
|
||||
return notNull
|
||||
}
|
||||
|
||||
if p.is(inferredNull) {
|
||||
return null
|
||||
}
|
||||
|
||||
// This param is unspecified, so by default we choose nullable
|
||||
// which matches the default behavior of most databases
|
||||
return null
|
||||
}
|
||||
|
||||
// IsSlice returns whether this param is a sqlc.slice() param.
|
||||
func (p Param) IsSqlcSlice() bool {
|
||||
return p.isSqlcSlice
|
||||
}
|
||||
|
||||
// mergeParam creates a new param from 2 partially specified params
|
||||
// If the parameters have different names, the first is preferred
|
||||
func mergeParam(a, b Param) Param {
|
||||
name := a.name
|
||||
if name == "" {
|
||||
name = b.name
|
||||
}
|
||||
|
||||
return Param{
|
||||
name: name,
|
||||
nullability: a.nullability | b.nullability,
|
||||
isSqlcSlice: a.isSqlcSlice || b.isSqlcSlice,
|
||||
}
|
||||
}
|
||||
85
internal/sql/named/param_set.go
Normal file
85
internal/sql/named/param_set.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package named
|
||||
|
||||
// ParamSet represents a set of parameters for a single query
|
||||
type ParamSet struct {
|
||||
// does this engine support named parameters?
|
||||
hasNamedSupport bool
|
||||
// the set of currently tracked named parameters
|
||||
namedParams map[string]Param
|
||||
// the locations of each of the named parameters
|
||||
namedLocs map[string][]int
|
||||
// a map of positions currently used
|
||||
positionToName map[int]string
|
||||
// argn keeps track of the last checked positional parameter used
|
||||
argn int
|
||||
}
|
||||
|
||||
// Return the name for a given parameter number and a boolean indicating if it
|
||||
// was found.
|
||||
func (p *ParamSet) NameFor(idx int) (string, bool) {
|
||||
name, ok := p.positionToName[idx]
|
||||
return name, ok
|
||||
}
|
||||
|
||||
func (p *ParamSet) nextArgNum() int {
|
||||
for {
|
||||
if _, ok := p.positionToName[p.argn]; !ok {
|
||||
return p.argn
|
||||
}
|
||||
|
||||
p.argn++
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds a parameter to this set and returns the numbered location used for it
|
||||
func (p *ParamSet) Add(param Param) int {
|
||||
name := param.name
|
||||
existing, ok := p.namedParams[name]
|
||||
|
||||
p.namedParams[name] = mergeParam(existing, param)
|
||||
if ok && p.hasNamedSupport {
|
||||
return p.namedLocs[name][0]
|
||||
}
|
||||
|
||||
argn := p.nextArgNum()
|
||||
p.positionToName[argn] = name
|
||||
p.namedLocs[name] = append(p.namedLocs[name], argn)
|
||||
return argn
|
||||
}
|
||||
|
||||
// FetchMerge fetches an indexed parameter, and merges `mergeP` into it
|
||||
// Returns: the merged parameter and whether it was a named parameter
|
||||
func (p *ParamSet) FetchMerge(idx int, mergeP Param) (param Param, isNamed bool) {
|
||||
name, exists := p.positionToName[idx]
|
||||
if !exists || name == "" {
|
||||
return mergeP, false
|
||||
}
|
||||
|
||||
param, ok := p.namedParams[name]
|
||||
if !ok {
|
||||
return mergeP, false
|
||||
}
|
||||
|
||||
return mergeParam(param, mergeP), true
|
||||
}
|
||||
|
||||
// NewParamSet creates a set of parameters with the given list of already used positions
|
||||
func NewParamSet(positionsUsed map[int]bool, hasNamedSupport bool) *ParamSet {
|
||||
positionToName := make(map[int]string, len(positionsUsed))
|
||||
for index, used := range positionsUsed {
|
||||
if !used {
|
||||
continue
|
||||
}
|
||||
|
||||
// assume the previously used params have no name
|
||||
positionToName[index] = ""
|
||||
}
|
||||
|
||||
return &ParamSet{
|
||||
argn: 1,
|
||||
namedParams: make(map[string]Param),
|
||||
namedLocs: make(map[string][]int),
|
||||
hasNamedSupport: hasNamedSupport,
|
||||
positionToName: positionToName,
|
||||
}
|
||||
}
|
||||
58
internal/sql/named/param_set_test.go
Normal file
58
internal/sql/named/param_set_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package named
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParamSet_Add(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type test struct {
|
||||
pset *ParamSet
|
||||
param Param
|
||||
expected int
|
||||
}
|
||||
|
||||
named := NewParamSet(nil, true)
|
||||
populatedNamed := NewParamSet(map[int]bool{1: true, 2: true, 4: true, 5: true, 6: true}, true)
|
||||
populatedUnnamed := NewParamSet(map[int]bool{1: true, 2: true, 4: true, 5: true, 6: true}, false)
|
||||
unnamed := NewParamSet(nil, false)
|
||||
p1 := NewParam("hello")
|
||||
p2 := NewParam("world")
|
||||
|
||||
tests := []test{
|
||||
// First parameter should be 1
|
||||
{named, p1, 1},
|
||||
// Duplicate first parameters should be 1
|
||||
{named, p1, 1},
|
||||
// A new parameter receives a new parameter number
|
||||
{named, p2, 2},
|
||||
// An additional new parameter does _not_ receive a new
|
||||
{named, p2, 2},
|
||||
|
||||
// First parameter should be 1
|
||||
{unnamed, p1, 1},
|
||||
// Duplicate first parameters should increment argn
|
||||
{unnamed, p1, 2},
|
||||
// A new parameter receives a new parameter number
|
||||
{unnamed, p2, 3},
|
||||
// An additional new parameter still does receive a new argn
|
||||
{unnamed, p2, 4},
|
||||
|
||||
// First parameter of a pre-populated should be 3
|
||||
{populatedNamed, p1, 3},
|
||||
{populatedNamed, p1, 3},
|
||||
{populatedNamed, p2, 7},
|
||||
{populatedNamed, p2, 7},
|
||||
|
||||
{populatedUnnamed, p1, 3},
|
||||
{populatedUnnamed, p1, 7},
|
||||
{populatedUnnamed, p2, 8},
|
||||
{populatedUnnamed, p2, 9},
|
||||
}
|
||||
|
||||
for _, spec := range tests {
|
||||
actual := spec.pset.Add(spec.param)
|
||||
if actual != spec.expected {
|
||||
t.Errorf("ParamSet.Add(%s) expected %v; got %v", spec.param.name, spec.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
82
internal/sql/named/param_test.go
Normal file
82
internal/sql/named/param_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package named
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMergeParamNullability(t *testing.T) {
|
||||
type test struct {
|
||||
a Param
|
||||
b Param
|
||||
notNull bool
|
||||
message string
|
||||
}
|
||||
|
||||
name := "name"
|
||||
unspec := NewParam(name)
|
||||
inferredNotNull := NewInferredParam(name, true)
|
||||
inferredNull := NewInferredParam(name, false)
|
||||
userDefNull := NewUserNullableParam(name)
|
||||
|
||||
const notNull = true
|
||||
const null = false
|
||||
|
||||
tests := []test{
|
||||
// Unspecified nullability parameter works
|
||||
{unspec, inferredNotNull, notNull, "Unspec + inferred(not null) = not null"},
|
||||
{unspec, inferredNull, null, "Unspec + inferred(not null) = null"},
|
||||
{unspec, userDefNull, null, "Unspec + userdef(null) = null"},
|
||||
|
||||
// Inferred nullability agreeing with user defined nullabilty
|
||||
{inferredNull, userDefNull, null, "inferred(null) + userdef(null) = null"},
|
||||
|
||||
// Inferred nullability disagreeing with user defined nullabilty
|
||||
{inferredNotNull, userDefNull, null, "inferred(not null) + userdef(null) = null"},
|
||||
}
|
||||
|
||||
for _, spec := range tests {
|
||||
a := spec.a
|
||||
b := spec.b
|
||||
actual := mergeParam(a, b).NotNull()
|
||||
expected := spec.notNull
|
||||
if actual != expected {
|
||||
t.Errorf("Combine(%s,%s) expected %v; got %v", a.nullability, b.nullability, expected, actual)
|
||||
}
|
||||
|
||||
// We have already tried Combine(a, b) the same result should be true for Combine(b, a)
|
||||
actual = mergeParam(b, a).NotNull()
|
||||
if actual != expected {
|
||||
t.Errorf("Combine(%s,%s) expected %v; got %v", b.nullability, a.nullability, expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeParamName(t *testing.T) {
|
||||
type test struct {
|
||||
a Param
|
||||
b Param
|
||||
name string
|
||||
}
|
||||
|
||||
a := NewParam("a")
|
||||
b := NewParam("b")
|
||||
blank := NewParam("")
|
||||
|
||||
tests := []test{
|
||||
// should prefer the first param's name if both specified
|
||||
{a, b, "a"},
|
||||
{b, a, "b"},
|
||||
|
||||
// should prefer non-blank names
|
||||
{a, blank, "a"},
|
||||
{blank, a, "a"},
|
||||
}
|
||||
|
||||
for _, spec := range tests {
|
||||
a := spec.a
|
||||
b := spec.b
|
||||
actual := mergeParam(a, b).Name()
|
||||
expected := spec.name
|
||||
if actual != expected {
|
||||
t.Errorf("Combine(%s,%s) expected %v; got %v", a.name, b.name, expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user