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,389 @@
package analyzer
import (
"fmt"
"github.com/sqlc-dev/sqlc/internal/core"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
)
func Prepare(cat *core.Catalog, stmt ast.Node) (core.PrepareResult, error) {
if rs, ok := stmt.(*ast.RawStmt); ok {
stmt = rs.Stmt
}
a := &analyzer{
cat: cat,
params: map[int]core.Parameter{},
stars: &[]core.StarExpansion{},
}
switch s := stmt.(type) {
case *ast.SelectStmt:
if err := a.analyzeSelect(s); err != nil {
return core.PrepareResult{}, err
}
a.command = core.CommandSelect
case *ast.InsertStmt:
if err := a.analyzeInsert(s); err != nil {
return core.PrepareResult{}, err
}
a.command = core.CommandInsert
case *ast.UpdateStmt:
if err := a.analyzeUpdate(s); err != nil {
return core.PrepareResult{}, err
}
a.command = core.CommandUpdate
case *ast.DeleteStmt:
if err := a.analyzeDelete(s); err != nil {
return core.PrepareResult{}, err
}
a.command = core.CommandDelete
default:
return core.PrepareResult{}, fmt.Errorf("analyzer: unsupported statement %T", stmt)
}
return a.result(), nil
}
type analyzer struct {
cat *core.Catalog
scope *scope
columns []core.Column
params map[int]core.Parameter
command core.Command
// outer is the scope of the query this one is nested in, which a
// correlated subquery refers to.
outer *scope
// ctes are the relations a WITH clause defined, visible to this query and
// to the ones nested in it.
ctes map[string]scopeRel
// aliases are the names the target list gives its expressions. GROUP BY,
// HAVING and ORDER BY may refer to a result column by its output name.
aliases map[string]ast.Node
// resolving guards against an alias that refers to itself.
resolving map[string]bool
// stars are the expansions every star in the statement asked for, shared
// with the analyzers of the queries nested in it so one statement reports
// all of them.
stars *[]core.StarExpansion
}
func (a *analyzer) recordStar(s core.StarExpansion) {
if a.stars == nil {
return
}
*a.stars = append(*a.stars, s)
}
// subquery analyzes a nested SELECT. It shares the parameter set, so a
// placeholder inside the subquery is reported with the rest, and it sees this
// query's scope, so a correlated reference resolves.
func (a *analyzer) subquery(s *ast.SelectStmt) (*analyzer, error) {
sub := &analyzer{
cat: a.cat,
params: a.params,
outer: a.scope,
ctes: a.ctes,
stars: a.stars,
}
if err := sub.analyzeSelect(s); err != nil {
return nil, err
}
return sub, nil
}
func (a *analyzer) subqueryColumns(s *ast.SelectStmt) ([]core.Column, error) {
sub, err := a.subquery(s)
if err != nil {
return nil, err
}
return sub.columns, nil
}
// derivedRel turns a subquery's result columns into a relation, so that the
// query selecting from it resolves columns the same way as from a table.
func derivedRel(alias string, cols []core.Column) scopeRel {
rel := scopeRel{alias: alias, cols: make([]core.ClassColumn, 0, len(cols))}
for _, col := range cols {
rel.cols = append(rel.cols, core.ClassColumn{
AttOID: col.SourceAttributeOID,
Name: col.Name,
TypeOID: col.TypeOID,
Type: col.Type.WithNullable(false),
NotNull: col.NotNull,
})
}
return rel
}
func (a *analyzer) result() core.PrepareResult {
// A placeholder nothing constrained takes the dialect's type for one, when
// the dialect has such a type.
if oid, ok := a.cat.UntypedTypeOID(); ok {
for n, p := range a.params {
if p.TypeOID == 0 && p.Type == nil {
t := exprType{typeOID: oid, nullable: true}
p.TypeOID = oid
p.DataType, p.IsArray = a.typeNameOf(t)
p.NotNull = false
p.Type = a.typeExprOf(t)
a.params[n] = p
}
}
}
res := core.PrepareResult{
Command: a.command,
Columns: a.columns,
Parameters: orderedParams(a.params),
}
if a.stars != nil {
res.Stars = *a.stars
}
return res
}
func orderedParams(m map[int]core.Parameter) []core.Parameter {
if len(m) == 0 {
return nil
}
maxN := 0
for n := range m {
if n > maxN {
maxN = n
}
}
out := make([]core.Parameter, 0, len(m))
for i := 1; i <= maxN; i++ {
if p, ok := m[i]; ok {
out = append(out, p)
}
}
return out
}
func (a *analyzer) analyzeSelect(s *ast.SelectStmt) error {
if err := a.bindCTEs(s.WithClause); err != nil {
return err
}
// A set operation reports the columns of its first branch. The other
// branches still get analyzed, for the placeholders they hold.
if s.Larg != nil && s.Rarg != nil {
if err := a.analyzeSetOperation(s); err != nil {
return err
}
return nil
}
sc, err := a.buildScope(s.FromClause)
if err != nil {
return err
}
a.scope = sc
a.bindAliases(s.TargetList)
for _, item := range listItems(s.FromClause) {
if err := a.typeJoinConditions(item); err != nil {
return fmt.Errorf("join: %w", err)
}
}
if s.WhereClause != nil {
if _, err := a.typeExpr(s.WhereClause); err != nil {
return fmt.Errorf("where: %w", err)
}
}
if items := listItems(s.GroupClause); items != nil {
for _, g := range items {
if _, err := a.typeExpr(g); err != nil {
return fmt.Errorf("group by: %w", err)
}
}
}
if s.HavingClause != nil {
if _, err := a.typeExpr(s.HavingClause); err != nil {
return fmt.Errorf("having: %w", err)
}
}
targets := listItems(s.TargetList)
if targets == nil {
// A VALUES list produces rows without naming columns.
if s.ValuesLists != nil {
return a.typeValuesLists(s.ValuesLists)
}
return fmt.Errorf("select: empty target list")
}
a.columns = make([]core.Column, 0, len(targets))
for _, t := range targets {
rt, ok := t.(*ast.ResTarget)
if !ok {
continue
}
if err := a.projectTarget(rt); err != nil {
return err
}
}
for _, item := range listItems(s.SortClause) {
if sb, ok := item.(*ast.SortBy); ok {
if _, err := a.typeExpr(sb.Node); err != nil {
return fmt.Errorf("order by: %w", err)
}
}
}
for _, n := range []ast.Node{s.LimitCount, s.LimitOffset} {
if err := a.typeLimit(n); err != nil {
return fmt.Errorf("limit: %w", err)
}
}
return nil
}
// typeLimit types a LIMIT or OFFSET count. A bare placeholder there holds
// whatever the dialect counts rows in.
func (a *analyzer) typeLimit(n ast.Node) error {
if n == nil {
return nil
}
if pr, ok := n.(*ast.ParamRef); ok {
oid, err := a.cat.LimitTypeOID()
if err != nil {
return err
}
a.inferParam(pr.Number, exprType{typeOID: oid})
return nil
}
_, err := a.typeExpr(n)
return err
}
func (a *analyzer) typeValuesLists(l *ast.List) error {
for _, row := range listItems(l) {
if _, err := a.typeExpr(row); err != nil {
return err
}
}
return nil
}
// analyzeSetOperation types both sides of UNION, INTERSECT or EXCEPT and
// reports the first branch's columns, the way the databases name the result.
func (a *analyzer) analyzeSetOperation(s *ast.SelectStmt) error {
left, err := a.subquery(s.Larg)
if err != nil {
return err
}
if _, err := a.subquery(s.Rarg); err != nil {
return err
}
a.columns = left.columns
a.scope = left.scope
return nil
}
// bindCTEs analyzes a WITH clause, making each common table expression
// available to the query as a relation.
func (a *analyzer) bindCTEs(with *ast.WithClause) error {
if with == nil {
return nil
}
for _, item := range listItems(with.Ctes) {
cte, ok := item.(*ast.CommonTableExpr)
if !ok || cte.Ctename == nil {
continue
}
sel, ok := cte.Ctequery.(*ast.SelectStmt)
if !ok {
continue
}
cols, err := a.subqueryColumns(sel)
if err != nil {
return fmt.Errorf("with %s: %w", *cte.Ctename, err)
}
rel := derivedRel(*cte.Ctename, cols)
renameColumns(&rel, cte.Aliascolnames)
if a.ctes == nil {
a.ctes = map[string]scopeRel{}
}
a.ctes[*cte.Ctename] = rel
}
return nil
}
// bindAliases records the output names the target list assigns, so a later
// clause can refer to a result column by name.
func (a *analyzer) bindAliases(targets *ast.List) {
for _, item := range listItems(targets) {
rt, ok := item.(*ast.ResTarget)
if !ok || rt.Name == nil || *rt.Name == "" {
continue
}
if a.aliases == nil {
a.aliases = map[string]ast.Node{}
}
a.aliases[*rt.Name] = rt.Val
}
}
// typeAlias types the expression an output name stands for, which is how a
// GROUP BY or HAVING clause refers to a result column.
func (a *analyzer) typeAlias(name string) (exprType, bool, error) {
val, ok := a.aliases[name]
if !ok || a.resolving[name] {
return exprType{}, false, nil
}
if a.resolving == nil {
a.resolving = map[string]bool{}
}
a.resolving[name] = true
defer delete(a.resolving, name)
t, err := a.typeExpr(val)
if err != nil {
return exprType{}, false, err
}
return t, true, nil
}
// renameColumns applies the column aliases a derived relation was declared
// with, as in "WITH t(a, b) AS (...)".
func renameColumns(rel *scopeRel, names *ast.List) {
for i, item := range listItems(names) {
if i >= len(rel.cols) {
break
}
if s, ok := item.(*ast.String); ok && s.Str != "" {
rel.cols[i].Name = s.Str
}
}
}
func listItems(l *ast.List) []ast.Node {
if l == nil {
return nil
}
return l.Items
}
func (a *analyzer) typeJoinConditions(item ast.Node) error {
je, ok := item.(*ast.JoinExpr)
if !ok {
return nil
}
if err := a.typeJoinConditions(je.Larg); err != nil {
return err
}
if err := a.typeJoinConditions(je.Rarg); err != nil {
return err
}
if je.Quals != nil {
if _, err := a.typeExpr(je.Quals); err != nil {
return fmt.Errorf("ON: %w", err)
}
}
return nil
}

View File

@@ -0,0 +1,110 @@
package analyzer_test
import (
"strings"
"testing"
"github.com/sqlc-dev/sqlc/internal/core"
"github.com/sqlc-dev/sqlc/internal/core/analyzer"
coreschema "github.com/sqlc-dev/sqlc/internal/core/schema"
"github.com/sqlc-dev/sqlc/internal/engine/googlesql"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
)
const benchSchema = `
CREATE TABLE users (
id INT64 NOT NULL,
name STRING NOT NULL,
bio STRING,
) PRIMARY KEY (id);
CREATE TABLE posts (
id INT64 NOT NULL,
user_id INT64 NOT NULL,
title STRING(255),
created TIMESTAMP NOT NULL,
) PRIMARY KEY (id);
`
const benchQueries = `
SELECT * FROM users;
SELECT COUNT(*) AS total FROM users;
SELECT u.name, p.* FROM users u JOIN posts p ON p.user_id = u.id WHERE u.name = @name;
SELECT id, name, bio FROM users WHERE id = @id AND name = @name;
SELECT p.title, p.created FROM posts p WHERE p.user_id = @uid AND p.title = @t;
`
func parseAll(t testing.TB, src string) []ast.Node {
p := googlesql.NewParser()
stmts, err := p.Parse(strings.NewReader(src))
if err != nil {
t.Fatal(err)
}
out := make([]ast.Node, 0, len(stmts))
for i := range stmts {
out = append(out, stmts[i].Raw)
}
return out
}
// parseQueries mirrors the compiler pipeline. GoogleSQL is not preprocessed —
// "@name" is native syntax the converter turns into a ParamRef — so parsing is
// all it takes.
func parseQueries(t testing.TB, src string) []ast.Node {
return parseAll(t, src)
}
func newBenchCatalog(t testing.TB) (*core.Catalog, []ast.Node) {
cat, err := core.New(googlesql.Dialect())
if err != nil {
t.Fatal(err)
}
for _, n := range parseAll(t, benchSchema) {
if err := coreschema.Apply(cat, n); err != nil {
t.Fatal(err)
}
}
return cat, parseQueries(t, benchQueries)
}
// BenchmarkCatalogNew measures per-compile catalog setup: open the SQLite
// catalog, install the schema, and run the dialect seed.
func BenchmarkCatalogNew(b *testing.B) {
for b.Loop() {
cat, err := core.New(googlesql.Dialect())
if err != nil {
b.Fatal(err)
}
cat.Close()
}
}
// BenchmarkApplySchema measures loading user DDL into the catalog.
func BenchmarkApplySchema(b *testing.B) {
stmts := parseAll(b, benchSchema)
for b.Loop() {
cat, err := core.New(googlesql.Dialect())
if err != nil {
b.Fatal(err)
}
for _, n := range stmts {
if err := coreschema.Apply(cat, n); err != nil {
b.Fatal(err)
}
}
cat.Close()
}
}
// BenchmarkPrepare measures steady-state query analysis against a warm catalog.
func BenchmarkPrepare(b *testing.B) {
cat, queries := newBenchCatalog(b)
defer cat.Close()
for b.Loop() {
for _, q := range queries {
if _, err := analyzer.Prepare(cat, q); err != nil {
b.Fatal(err)
}
}
}
}

View File

@@ -0,0 +1,211 @@
package analyzer
import (
"fmt"
"github.com/sqlc-dev/sqlc/internal/core"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
)
func (a *analyzer) analyzeInsert(s *ast.InsertStmt) error {
if s.Relation == nil {
return fmt.Errorf("insert: missing relation")
}
rel, err := a.bindRangeVar(s.Relation)
if err != nil {
return err
}
a.scope = &scope{rels: []scopeRel{rel}}
targets, err := insertTargets(rel, s.Cols)
if err != nil {
return err
}
if err := a.bindInsertValues(s.SelectStmt, rel, targets); err != nil {
return err
}
return a.projectReturning(s.ReturningList)
}
func (a *analyzer) analyzeUpdate(s *ast.UpdateStmt) error {
if err := a.bindCTEs(s.WithClause); err != nil {
return err
}
sc, err := a.relationScope(s.Relations, s.FromClause, nil)
if err != nil {
return err
}
a.scope = sc
target := sc.rels[0]
for _, item := range listItems(s.TargetList) {
rt, ok := item.(*ast.ResTarget)
if !ok || rt.Name == nil {
continue
}
col, ok := findColumn(target, *rt.Name)
if !ok {
return fmt.Errorf("unknown column %q", *rt.Name)
}
if err := a.bindValue(target, &col, rt.Val); err != nil {
return fmt.Errorf("set %s: %w", *rt.Name, err)
}
}
if s.WhereClause != nil {
if _, err := a.typeExpr(s.WhereClause); err != nil {
return fmt.Errorf("where: %w", err)
}
}
return a.projectReturning(s.ReturningList)
}
func (a *analyzer) analyzeDelete(s *ast.DeleteStmt) error {
if err := a.bindCTEs(s.WithClause); err != nil {
return err
}
sc, err := a.relationScope(s.Relations, s.UsingClause, s.FromClause)
if err != nil {
return err
}
a.scope = sc
if s.WhereClause != nil {
if _, err := a.typeExpr(s.WhereClause); err != nil {
return fmt.Errorf("where: %w", err)
}
}
return a.projectReturning(s.ReturningList)
}
// relationScope builds the scope a DML statement operates on: the relations it
// targets, whatever a USING or FROM clause joins in, and — for the engines that
// report a multi-table DELETE that way — a single FROM node.
func (a *analyzer) relationScope(relations, extra *ast.List, from ast.Node) (*scope, error) {
sc := &scope{}
defer a.binding(sc)()
for _, item := range listItems(relations) {
if err := a.appendFromItem(sc, item); err != nil {
return nil, err
}
}
for _, item := range listItems(extra) {
if err := a.appendFromItem(sc, item); err != nil {
return nil, err
}
}
if from != nil {
if err := a.appendFromItem(sc, from); err != nil {
return nil, err
}
}
if len(sc.rels) == 0 {
return nil, fmt.Errorf("missing target relation")
}
return sc, nil
}
func insertTargets(rel scopeRel, cols *ast.List) ([]core.ClassColumn, error) {
items := listItems(cols)
if len(items) == 0 {
out := make([]core.ClassColumn, 0, len(rel.cols))
for _, col := range rel.cols {
if !col.Hidden {
out = append(out, col)
}
}
return out, nil
}
out := make([]core.ClassColumn, 0, len(items))
for _, item := range items {
rt, ok := item.(*ast.ResTarget)
if !ok || rt.Name == nil {
return nil, fmt.Errorf("insert: unsupported column target %T", item)
}
col, ok := findColumn(rel, *rt.Name)
if !ok {
return nil, fmt.Errorf("unknown column %q", *rt.Name)
}
out = append(out, col)
}
return out, nil
}
func (a *analyzer) bindInsertValues(n ast.Node, rel scopeRel, targets []core.ClassColumn) error {
if n == nil {
return nil
}
sel, ok := n.(*ast.SelectStmt)
if !ok {
return fmt.Errorf("insert: unsupported source %T", n)
}
// INSERT ... SELECT inserts whatever the query returns. The rows are not
// the statement's result, but the query still holds placeholders.
if sel.ValuesLists == nil {
_, err := a.subqueryColumns(sel)
return err
}
for _, row := range listItems(sel.ValuesLists) {
values, ok := row.(*ast.List)
if !ok {
continue
}
for i, v := range values.Items {
var target *core.ClassColumn
if i < len(targets) {
target = &targets[i]
}
if err := a.bindValue(rel, target, v); err != nil {
return err
}
}
}
return nil
}
func (a *analyzer) bindValue(rel scopeRel, target *core.ClassColumn, v ast.Node) error {
if target != nil {
switch value := v.(type) {
case *ast.ParamRef:
a.inferParam(value.Number, columnType(rel, *target))
return nil
case *ast.A_Const:
return nil
}
}
_, err := a.typeExpr(v)
return err
}
func (a *analyzer) projectReturning(l *ast.List) error {
for _, item := range listItems(l) {
rt, ok := item.(*ast.ResTarget)
if !ok {
continue
}
if err := a.projectTarget(rt); err != nil {
return err
}
}
return nil
}
func findColumn(rel scopeRel, name string) (core.ClassColumn, bool) {
for _, col := range rel.cols {
if col.Name == name {
return col, true
}
}
return core.ClassColumn{}, false
}
func columnType(rel scopeRel, col core.ClassColumn) exprType {
return exprType{
typeOID: col.TypeOID,
expr: col.Type,
nullable: !col.NotNull,
sourceClassOID: rel.classOID,
sourceAttributeOID: col.AttOID,
sourceTableAlias: rel.alias,
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,169 @@
package analyzer
import (
"slices"
"strings"
"github.com/sqlc-dev/sqlc/internal/core"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
)
func (a *analyzer) projectTarget(rt *ast.ResTarget) error {
// A column reference's field list is flattened once here and threaded
// through the star check, the star expansion and the output name.
var fields []string
if cr, ok := rt.Val.(*ast.ColumnRef); ok {
fields = flattenFields(cr.Fields)
if isStar(fields) {
a.emitStar(rt, fields)
return nil
}
}
t, err := a.typeExpr(rt.Val)
if err != nil {
return err
}
// A placeholder selected directly is named by its alias and, when nothing
// constrains it, typed as the dialect types such a placeholder.
if pr, ok := rt.Val.(*ast.ParamRef); ok {
if rt.Name != nil && *rt.Name != "" {
if p := a.params[pr.Number]; p.Name == "" {
p.Name = *rt.Name
a.params[pr.Number] = p
}
}
if t.typeOID == 0 && t.expr == nil {
if oid, ok := a.cat.UntypedTypeOID(); ok {
t = exprType{typeOID: oid, nullable: true}
}
}
}
col := core.Column{
Name: targetName(rt, fields),
TypeOID: t.typeOID,
NotNull: !t.nullable,
SourceClassOID: t.sourceClassOID,
SourceAttributeOID: t.sourceAttributeOID,
}
col.DataType, col.IsArray = a.typeNameOf(t)
col.Type = a.typeExprOf(t)
a.decorateSource(&col, t.sourceAttributeOID, t.sourceTableAlias)
if rt.Name == nil || *rt.Name == "" {
// A column whose own name is dotted, as ClickHouse's n.a is, is
// reported under that name rather than its last part.
if col.Source != nil && strings.Contains(col.Source.Column, ".") {
col.Name = col.Source.Column
}
a.qualifyDuplicate(&col, t.sourceTableAlias)
}
a.columns = append(a.columns, col)
return nil
}
// qualifyDuplicate names a column after its relation when an earlier result
// column from another relation already has its name, in a dialect that
// does so.
func (a *analyzer) qualifyDuplicate(col *core.Column, alias string) {
if alias == "" || !a.cat.QualifiesDuplicateColumns() {
return
}
for _, prev := range a.columns {
if prev.Name != col.Name {
continue
}
prevAlias := ""
if prev.Source != nil {
prevAlias = prev.Source.TableAlias
}
if prevAlias != alias {
col.Name = alias + "." + col.Name
return
}
}
}
func (a *analyzer) decorateSource(col *core.Column, attOID int64, tableAlias string) {
if attOID == 0 {
return
}
ad, err := a.cat.LookupAttribute(attOID)
if err != nil {
return
}
col.Source = &core.ColumnSource{
Schema: ad.Schema,
Table: ad.Table,
TableAlias: tableAlias,
Column: ad.Column,
}
col.DeclType = ad.DeclType
col.IsPrimaryKey = ad.IsPrimaryKey
col.IsUnique = ad.IsUnique
col.IsAutoIncrement = ad.AutoIncrement
}
// targetName picks the output name for a target. fields is the already
// flattened field list when rt.Val is a column reference, and nil otherwise.
func targetName(rt *ast.ResTarget, fields []string) string {
if rt.Name != nil && *rt.Name != "" {
return *rt.Name
}
if len(fields) > 0 {
return fields[len(fields)-1]
}
if fc, ok := rt.Val.(*ast.FuncCall); ok {
if name := funcCallName(fc); name != "" {
return name
}
}
return "?column?"
}
func isStar(fields []string) bool {
return len(fields) > 0 && fields[len(fields)-1] == "*"
}
func (a *analyzer) emitStar(rt *ast.ResTarget, fields []string) {
relName := ""
if len(fields) > 1 {
relName = fields[0]
}
// The star is reported along with the columns it covers, so the query text
// can be rewritten to name them.
star := core.StarExpansion{Location: rt.Location, Fields: fields}
if rt.Name != nil {
star.Alias = *rt.Name
}
for _, rel := range a.scope.rels {
if relName != "" && rel.alias != relName {
continue
}
a.columns = slices.Grow(a.columns, len(rel.cols))
star.Columns = slices.Grow(star.Columns, len(rel.cols))
for _, c := range rel.cols {
if c.Hidden {
continue
}
col := core.Column{
Name: c.Name,
TypeOID: c.TypeOID,
NotNull: c.NotNull,
SourceClassOID: rel.classOID,
SourceAttributeOID: c.AttOID,
}
t := exprType{typeOID: c.TypeOID, expr: c.Type, nullable: !c.NotNull}
col.DataType, col.IsArray = a.typeNameOf(t)
col.Type = a.typeExprOf(t)
a.decorateSource(&col, c.AttOID, rel.alias)
a.qualifyDuplicate(&col, rel.alias)
a.columns = append(a.columns, col)
star.Columns = append(star.Columns, core.StarColumn{
Relation: rel.alias,
Name: c.Name,
DataType: col.DataType,
})
}
}
a.recordStar(star)
}

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