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
390 lines
9.3 KiB
Go
390 lines
9.3 KiB
Go
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
|
|
}
|