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
244 lines
6.4 KiB
Go
244 lines
6.4 KiB
Go
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
|
|
}
|