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:
243
internal/core/analyzer/scope.go
Normal file
243
internal/core/analyzer/scope.go
Normal file
@@ -0,0 +1,243 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/core"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
)
|
||||
|
||||
type scope struct {
|
||||
rels []scopeRel
|
||||
}
|
||||
|
||||
type scopeRel struct {
|
||||
alias string
|
||||
classOID int64
|
||||
// cols is the catalog's column list, held as-is rather than copied into a
|
||||
// scope-local column type.
|
||||
cols []core.ClassColumn
|
||||
}
|
||||
|
||||
func (a *analyzer) buildScope(from *ast.List) (*scope, error) {
|
||||
items := listItems(from)
|
||||
sc := &scope{rels: make([]scopeRel, 0, len(items))}
|
||||
defer a.binding(sc)()
|
||||
for _, item := range items {
|
||||
if err := a.appendFromItem(sc, item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
// binding makes the scope under construction the one the analyzer resolves
|
||||
// against, and returns the func that puts back the scope it replaced. A FROM
|
||||
// item can refer to the ones before it — a set-returning function takes its
|
||||
// arguments from them — so binding an item has to see what is bound so far
|
||||
// rather than no scope at all.
|
||||
func (a *analyzer) binding(sc *scope) func() {
|
||||
prev := a.scope
|
||||
a.scope = sc
|
||||
return func() { a.scope = prev }
|
||||
}
|
||||
|
||||
func (a *analyzer) appendFromItem(sc *scope, item ast.Node) error {
|
||||
switch v := item.(type) {
|
||||
case *ast.RangeVar:
|
||||
rel, err := a.bindRangeVar(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sc.rels = append(sc.rels, rel)
|
||||
return nil
|
||||
case *ast.JoinExpr:
|
||||
if err := a.appendFromItem(sc, v.Larg); err != nil {
|
||||
return err
|
||||
}
|
||||
return a.appendFromItem(sc, v.Rarg)
|
||||
case *ast.RangeFunction:
|
||||
rel, err := a.bindRangeFunction(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sc.rels = append(sc.rels, rel)
|
||||
return nil
|
||||
case *ast.RangeSubselect:
|
||||
rel, err := a.bindRangeSubselect(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sc.rels = append(sc.rels, rel)
|
||||
return nil
|
||||
case *ast.List:
|
||||
// Some engines report a comma-separated FROM as a nested list.
|
||||
for _, item := range listItems(v) {
|
||||
if err := a.appendFromItem(sc, item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("scope: unsupported FROM item %T", item)
|
||||
}
|
||||
}
|
||||
|
||||
// bindRangeFunction binds a function called in FROM. A set-returning function
|
||||
// stands in for a relation of a single column named after it.
|
||||
func (a *analyzer) bindRangeFunction(rf *ast.RangeFunction) (scopeRel, error) {
|
||||
call := findFuncCall(rf.Functions)
|
||||
if call == nil {
|
||||
return scopeRel{}, fmt.Errorf("range function: no function call")
|
||||
}
|
||||
name := funcCallName(call)
|
||||
overloads, err := a.cat.FindProcs(name, nil)
|
||||
if err != nil {
|
||||
return scopeRel{}, err
|
||||
}
|
||||
if len(overloads) == 0 {
|
||||
return scopeRel{}, fmt.Errorf("unknown function %q", name)
|
||||
}
|
||||
|
||||
// The arguments are typed against the scope built so far, which is what
|
||||
// gives a placeholder passed to the function its type.
|
||||
if _, err := a.typeFuncCall(call); err != nil {
|
||||
return scopeRel{}, err
|
||||
}
|
||||
|
||||
p := overloads[0]
|
||||
rel := scopeRel{
|
||||
alias: name,
|
||||
cols: []core.ClassColumn{{Name: name, TypeOID: p.ReturnTypeOID, NotNull: !p.ReturnNullable}},
|
||||
}
|
||||
if rf.Alias != nil && rf.Alias.Aliasname != nil && *rf.Alias.Aliasname != "" {
|
||||
rel.alias = *rf.Alias.Aliasname
|
||||
}
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
func findFuncCall(l *ast.List) *ast.FuncCall {
|
||||
for _, item := range listItems(l) {
|
||||
switch v := item.(type) {
|
||||
case *ast.FuncCall:
|
||||
return v
|
||||
case *ast.List:
|
||||
if call := findFuncCall(v); call != nil {
|
||||
return call
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// bindRangeSubselect binds a subquery used as a relation in FROM.
|
||||
func (a *analyzer) bindRangeSubselect(rs *ast.RangeSubselect) (scopeRel, error) {
|
||||
sel, ok := rs.Subquery.(*ast.SelectStmt)
|
||||
if !ok {
|
||||
return scopeRel{}, fmt.Errorf("subquery: unsupported %T", rs.Subquery)
|
||||
}
|
||||
cols, err := a.subqueryColumns(sel)
|
||||
if err != nil {
|
||||
return scopeRel{}, err
|
||||
}
|
||||
alias := ""
|
||||
if rs.Alias != nil && rs.Alias.Aliasname != nil {
|
||||
alias = *rs.Alias.Aliasname
|
||||
}
|
||||
rel := derivedRel(alias, cols)
|
||||
if rs.Alias != nil {
|
||||
renameColumns(&rel, rs.Alias.Colnames)
|
||||
}
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
func (a *analyzer) bindRangeVar(rv *ast.RangeVar) (scopeRel, error) {
|
||||
if rv.Relname == nil {
|
||||
return scopeRel{}, fmt.Errorf("range var: missing relation name")
|
||||
}
|
||||
relName := *rv.Relname
|
||||
|
||||
// A WITH clause defines a relation that shadows nothing but is not in the
|
||||
// catalog, so it is resolved before one.
|
||||
if cte, ok := a.ctes[relName]; ok && (rv.Schemaname == nil || *rv.Schemaname == "") {
|
||||
if rv.Alias != nil && rv.Alias.Aliasname != nil && *rv.Alias.Aliasname != "" {
|
||||
cte.alias = *rv.Alias.Aliasname
|
||||
}
|
||||
return cte, nil
|
||||
}
|
||||
|
||||
schema := ""
|
||||
if rv.Schemaname != nil {
|
||||
schema = *rv.Schemaname
|
||||
}
|
||||
if schema == "" {
|
||||
schema = "public"
|
||||
}
|
||||
nsOID, err := a.cat.NamespaceOID(schema)
|
||||
if err != nil {
|
||||
return scopeRel{}, fmt.Errorf("schema %q: %w", schema, err)
|
||||
}
|
||||
classOID, err := a.cat.ClassOID(nsOID, relName)
|
||||
if err != nil {
|
||||
return scopeRel{}, fmt.Errorf("relation %q.%q: %w", schema, relName, err)
|
||||
}
|
||||
rel := scopeRel{
|
||||
alias: relName,
|
||||
classOID: classOID,
|
||||
}
|
||||
if rv.Alias != nil && rv.Alias.Aliasname != nil && *rv.Alias.Aliasname != "" {
|
||||
rel.alias = *rv.Alias.Aliasname
|
||||
}
|
||||
|
||||
cols, err := a.cat.ClassColumns(classOID)
|
||||
if err != nil {
|
||||
return scopeRel{}, err
|
||||
}
|
||||
rel.cols = cols
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
// resolveColumn finds a column in this query's scope, falling back to the scope
|
||||
// of the query it is nested in, which is what a correlated subquery refers to.
|
||||
func (a *analyzer) resolveColumn(relation, column string) (scopeRel, core.ClassColumn, bool, error) {
|
||||
rel, col, ok, err := a.scope.resolveColumn(relation, column)
|
||||
if err != nil || ok {
|
||||
return rel, col, ok, err
|
||||
}
|
||||
if a.outer != nil {
|
||||
return a.outer.resolveColumn(relation, column)
|
||||
}
|
||||
return rel, col, false, nil
|
||||
}
|
||||
|
||||
// resolveColumn finds the single column named column, optionally qualified by
|
||||
// relation. It reports an error when more than one relation in scope offers
|
||||
// that name.
|
||||
func (s *scope) resolveColumn(relation, column string) (rel scopeRel, col core.ClassColumn, ok bool, err error) {
|
||||
// A statement whose scope is not built yet offers no columns. Report the
|
||||
// column as unresolved and let the caller say so, rather than crashing on
|
||||
// the way to the same answer.
|
||||
if s == nil {
|
||||
return rel, col, false, nil
|
||||
}
|
||||
found := 0
|
||||
for _, r := range s.rels {
|
||||
if relation != "" && r.alias != relation {
|
||||
continue
|
||||
}
|
||||
for _, c := range r.cols {
|
||||
if c.Name != column {
|
||||
continue
|
||||
}
|
||||
found++
|
||||
if found > 1 {
|
||||
return scopeRel{}, core.ClassColumn{}, false, fmt.Errorf("ambiguous column reference %q", column)
|
||||
}
|
||||
rel, col = r, c
|
||||
}
|
||||
}
|
||||
if found == 0 {
|
||||
return scopeRel{}, core.ClassColumn{}, false, nil
|
||||
}
|
||||
return rel, col, true, nil
|
||||
}
|
||||
Reference in New Issue
Block a user