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
|
||||
}
|
||||
110
internal/core/analyzer/bench_test.go
Normal file
110
internal/core/analyzer/bench_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
211
internal/core/analyzer/dml.go
Normal file
211
internal/core/analyzer/dml.go
Normal 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,
|
||||
}
|
||||
}
|
||||
1038
internal/core/analyzer/expr.go
Normal file
1038
internal/core/analyzer/expr.go
Normal file
File diff suppressed because it is too large
Load Diff
169
internal/core/analyzer/projection.go
Normal file
169
internal/core/analyzer/projection.go
Normal 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)
|
||||
}
|
||||
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