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:
389
internal/core/analyzer/analyzer.go
Normal file
389
internal/core/analyzer/analyzer.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user