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:
214
internal/compiler/analyze.go
Normal file
214
internal/compiler/analyze.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
analyzer "github.com/sqlc-dev/sqlc/internal/analysis"
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
"github.com/sqlc-dev/sqlc/internal/source"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/named"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/preprocess"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/validate"
|
||||
)
|
||||
|
||||
type analysis struct {
|
||||
Table *ast.TableName
|
||||
Columns []*Column
|
||||
Parameters []Parameter
|
||||
Named *named.ParamSet
|
||||
Query string
|
||||
}
|
||||
|
||||
func convertTableName(id *analyzer.Identifier) *ast.TableName {
|
||||
if id == nil {
|
||||
return nil
|
||||
}
|
||||
return &ast.TableName{
|
||||
Catalog: id.Catalog,
|
||||
Schema: id.Schema,
|
||||
Name: id.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func convertTypeName(id *analyzer.Identifier) *ast.TypeName {
|
||||
if id == nil {
|
||||
return nil
|
||||
}
|
||||
return &ast.TypeName{
|
||||
Catalog: id.Catalog,
|
||||
Schema: id.Schema,
|
||||
Name: id.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func convertColumn(c *analyzer.Column) *Column {
|
||||
length := int(c.Length)
|
||||
return &Column{
|
||||
Name: c.Name,
|
||||
OriginalName: c.OriginalName,
|
||||
DataType: c.DataType,
|
||||
NotNull: c.NotNull,
|
||||
Unsigned: c.Unsigned,
|
||||
IsArray: c.IsArray,
|
||||
ArrayDims: int(c.ArrayDims),
|
||||
Comment: c.Comment,
|
||||
Length: &length,
|
||||
IsNamedParam: c.IsNamedParam,
|
||||
IsFuncCall: c.IsFuncCall,
|
||||
Scope: c.Scope,
|
||||
Table: convertTableName(c.Table),
|
||||
TableAlias: c.TableAlias,
|
||||
Type: convertTypeName(c.Type),
|
||||
EmbedTable: convertTableName(c.EmbedTable),
|
||||
IsSqlcSlice: c.IsSqlcSlice,
|
||||
}
|
||||
}
|
||||
|
||||
func combineAnalysis(prev *analysis, a *analyzer.Analysis) *analysis {
|
||||
var cols []*Column
|
||||
for _, c := range a.Columns {
|
||||
cols = append(cols, convertColumn(c))
|
||||
}
|
||||
var params []Parameter
|
||||
for _, p := range a.Params {
|
||||
params = append(params, Parameter{
|
||||
Number: int(p.Number),
|
||||
Column: convertColumn(p.Column),
|
||||
})
|
||||
}
|
||||
if len(prev.Columns) == len(cols) {
|
||||
for i := range prev.Columns {
|
||||
// Only override column types if the analyzer provides a specific type
|
||||
// (not "any"), since the catalog-based inference may have better info
|
||||
if cols[i].DataType != "any" {
|
||||
prev.Columns[i].DataType = cols[i].DataType
|
||||
prev.Columns[i].IsArray = cols[i].IsArray
|
||||
prev.Columns[i].ArrayDims = cols[i].ArrayDims
|
||||
}
|
||||
}
|
||||
} else {
|
||||
embedding := false
|
||||
for i := range prev.Columns {
|
||||
if prev.Columns[i].EmbedTable != nil {
|
||||
embedding = true
|
||||
}
|
||||
}
|
||||
if !embedding {
|
||||
prev.Columns = cols
|
||||
}
|
||||
}
|
||||
if len(prev.Parameters) == len(params) {
|
||||
for i := range prev.Parameters {
|
||||
// Only override parameter types if the analyzer provides a specific type
|
||||
// (not "any"), since the catalog-based inference may have better info
|
||||
if params[i].Column.DataType != "any" {
|
||||
prev.Parameters[i].Column.DataType = params[i].Column.DataType
|
||||
prev.Parameters[i].Column.IsArray = params[i].Column.IsArray
|
||||
prev.Parameters[i].Column.ArrayDims = params[i].Column.ArrayDims
|
||||
}
|
||||
}
|
||||
} else {
|
||||
prev.Parameters = params
|
||||
}
|
||||
return prev
|
||||
}
|
||||
|
||||
func (c *Compiler) analyzeQuery(raw *ast.RawStmt, query string, pre *preprocess.Statement) (*analysis, error) {
|
||||
return c._analyzeQuery(raw, query, pre, true)
|
||||
}
|
||||
|
||||
func (c *Compiler) inferQuery(raw *ast.RawStmt, query string, pre *preprocess.Statement) (*analysis, error) {
|
||||
return c._analyzeQuery(raw, query, pre, false)
|
||||
}
|
||||
|
||||
func (c *Compiler) _analyzeQuery(raw *ast.RawStmt, query string, pre *preprocess.Statement, failfast bool) (*analysis, error) {
|
||||
errors := make([]error, 0)
|
||||
check := func(err error) error {
|
||||
if failfast {
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
dollar := pre.Dollar
|
||||
if err := check(pre.ParamErr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
namedParams := pre.Params
|
||||
|
||||
var table *ast.TableName
|
||||
switch n := raw.Stmt.(type) {
|
||||
case *ast.InsertStmt:
|
||||
var err error
|
||||
table, err = ParseTableName(n.Relation)
|
||||
if err := check(err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := check(validate.InsertStmt(c.catalog, table, n)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := check(validate.FuncCall(c.catalog, c.combo, raw)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := check(validate.Slice(raw, pre.Slices)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rvs := rangeVars(raw.Stmt)
|
||||
refs, errs := findParameters(raw.Stmt)
|
||||
if len(errs) > 0 {
|
||||
if failfast {
|
||||
return nil, errs[0]
|
||||
}
|
||||
errors = append(errors, errs...)
|
||||
}
|
||||
refs = uniqueParamRefs(refs, dollar)
|
||||
if c.conf.Engine == config.EngineMySQL || !dollar {
|
||||
sort.Slice(refs, func(i, j int) bool { return refs[i].ref.Location < refs[j].ref.Location })
|
||||
} else {
|
||||
sort.Slice(refs, func(i, j int) bool { return refs[i].ref.Number < refs[j].ref.Number })
|
||||
}
|
||||
embeds := pre.Embeds
|
||||
qc, err := c.buildQueryCatalog(c.catalog, raw.Stmt, embeds)
|
||||
if err := check(err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
params, err := c.resolveCatalogRefs(qc, rvs, refs, namedParams, embeds)
|
||||
if err := check(err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cols, err := c.outputColumns(qc, raw.Stmt)
|
||||
if err := check(err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
edits, err := c.expand(qc, raw)
|
||||
if check(err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expanded, err := source.Mutate(query, edits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rerr error
|
||||
if len(errors) > 0 {
|
||||
rerr = errors[0]
|
||||
}
|
||||
|
||||
return &analysis{
|
||||
Table: table,
|
||||
Columns: cols,
|
||||
Parameters: params,
|
||||
Query: expanded,
|
||||
Named: namedParams,
|
||||
}, rerr
|
||||
}
|
||||
62
internal/compiler/catalog_core.go
Normal file
62
internal/compiler/catalog_core.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/core"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
|
||||
)
|
||||
|
||||
// coreResultCatalog dumps the core catalog into the legacy catalog shape a
|
||||
// Result carries, so codegen sees the same table models either way a query
|
||||
// set was analyzed. Only relations make the trip: codegen reads tables and
|
||||
// their columns to build models, and none of the types, functions or
|
||||
// operators the core catalog also holds.
|
||||
func coreResultCatalog(c *core.Catalog) (*catalog.Catalog, error) {
|
||||
cat := catalog.New("public")
|
||||
namespaces, err := c.Namespaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, ns := range namespaces {
|
||||
schema := &catalog.Schema{Name: ns.Name}
|
||||
tables, err := c.TablesInNamespace(ns.OID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, table := range tables {
|
||||
cols, err := c.ClassCodegenColumns(table.OID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t := &catalog.Table{Rel: &ast.TableName{Schema: ns.Name, Name: table.Name}}
|
||||
for _, col := range cols {
|
||||
// Codegen reads a data type and an array flag, and renders
|
||||
// a "[]" per dimension, so an array of arrays of integers
|
||||
// is the type integer with two dimensions.
|
||||
expr, err := c.TypeExprOf(col.TypeOID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inner := expr.Innermost()
|
||||
column := &catalog.Column{
|
||||
Name: col.Name,
|
||||
Type: ast.TypeName{Name: strings.TrimSuffix(inner.Name, " unsigned")},
|
||||
IsNotNull: col.NotNull,
|
||||
IsArray: expr.IsArray(),
|
||||
ArrayDims: expr.ArrayDims(),
|
||||
IsUnsigned: strings.HasSuffix(inner.Name, " unsigned"),
|
||||
}
|
||||
if len(inner.Args) > 0 && inner.Args[0].Int != nil {
|
||||
l := int(*inner.Args[0].Int)
|
||||
column.Length = &l
|
||||
}
|
||||
t.Columns = append(t.Columns, column)
|
||||
}
|
||||
schema.Tables = append(schema.Tables, t)
|
||||
}
|
||||
cat.Schemas = append(cat.Schemas, schema)
|
||||
}
|
||||
return cat, nil
|
||||
}
|
||||
137
internal/compiler/compat.go
Normal file
137
internal/compiler/compat.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
|
||||
)
|
||||
|
||||
// This is mainly copy-pasted from internal/postgresql/parse.go
|
||||
func stringSlice(list *ast.List) []string {
|
||||
items := []string{}
|
||||
for _, item := range list.Items {
|
||||
if n, ok := item.(*ast.String); ok {
|
||||
items = append(items, n.Str)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
type Relation struct {
|
||||
Catalog string
|
||||
Schema string
|
||||
Name string
|
||||
}
|
||||
|
||||
func parseRelation(node ast.Node) (*Relation, error) {
|
||||
switch n := node.(type) {
|
||||
case *ast.Boolean:
|
||||
if n == nil {
|
||||
return nil, fmt.Errorf("unexpected nil in %T node", n)
|
||||
}
|
||||
return &Relation{Name: "bool"}, nil
|
||||
|
||||
case *ast.List:
|
||||
if n == nil {
|
||||
return nil, fmt.Errorf("unexpected nil in %T node", n)
|
||||
}
|
||||
parts := stringSlice(n)
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
return &Relation{
|
||||
Name: parts[0],
|
||||
}, nil
|
||||
case 2:
|
||||
return &Relation{
|
||||
Schema: parts[0],
|
||||
Name: parts[1],
|
||||
}, nil
|
||||
case 3:
|
||||
return &Relation{
|
||||
Catalog: parts[0],
|
||||
Schema: parts[1],
|
||||
Name: parts[2],
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid name: %s", astutils.Join(n, "."))
|
||||
}
|
||||
|
||||
case *ast.RangeVar:
|
||||
if n == nil {
|
||||
return nil, fmt.Errorf("unexpected nil in %T node", n)
|
||||
}
|
||||
name := Relation{}
|
||||
if n.Catalogname != nil {
|
||||
name.Catalog = *n.Catalogname
|
||||
}
|
||||
if n.Schemaname != nil {
|
||||
name.Schema = *n.Schemaname
|
||||
}
|
||||
if n.Relname != nil {
|
||||
name.Name = *n.Relname
|
||||
}
|
||||
return &name, nil
|
||||
|
||||
case *ast.TypeName:
|
||||
if n == nil {
|
||||
return nil, fmt.Errorf("unexpected nil in %T node", n)
|
||||
}
|
||||
if n.Names != nil {
|
||||
return parseRelation(n.Names)
|
||||
} else {
|
||||
return &Relation{Name: n.Name}, nil
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected node type: %T", node)
|
||||
}
|
||||
}
|
||||
|
||||
func ParseTableName(node ast.Node) (*ast.TableName, error) {
|
||||
rel, err := parseRelation(node)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse table name: %w", err)
|
||||
}
|
||||
return &ast.TableName{
|
||||
Catalog: rel.Catalog,
|
||||
Schema: rel.Schema,
|
||||
Name: rel.Name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ParseTypeName(node ast.Node) (*ast.TypeName, error) {
|
||||
rel, err := parseRelation(node)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse type name: %w", err)
|
||||
}
|
||||
return &ast.TypeName{
|
||||
Catalog: rel.Catalog,
|
||||
Schema: rel.Schema,
|
||||
Name: rel.Name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ParseRelationString(name string) (*Relation, error) {
|
||||
parts := strings.Split(name, ".")
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
return &Relation{
|
||||
Name: parts[0],
|
||||
}, nil
|
||||
case 2:
|
||||
return &Relation{
|
||||
Schema: parts[0],
|
||||
Name: parts[1],
|
||||
}, nil
|
||||
case 3:
|
||||
return &Relation{
|
||||
Catalog: parts[0],
|
||||
Schema: parts[1],
|
||||
Name: parts[2],
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid name: %s", name)
|
||||
}
|
||||
}
|
||||
300
internal/compiler/compile.go
Normal file
300
internal/compiler/compile.go
Normal file
@@ -0,0 +1,300 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/core"
|
||||
coreschema "github.com/sqlc-dev/sqlc/internal/core/schema"
|
||||
"github.com/sqlc-dev/sqlc/internal/migrations"
|
||||
"github.com/sqlc-dev/sqlc/internal/multierr"
|
||||
"github.com/sqlc-dev/sqlc/internal/opts"
|
||||
"github.com/sqlc-dev/sqlc/internal/rpc"
|
||||
"github.com/sqlc-dev/sqlc/internal/source"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/preprocess"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/sqlerr"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/sqlpath"
|
||||
)
|
||||
|
||||
// TODO: Rename this interface Engine
|
||||
type Parser interface {
|
||||
Parse(io.Reader) ([]ast.Statement, error)
|
||||
CommentSyntax() source.CommentSyntax
|
||||
IsReservedKeyword(string) bool
|
||||
}
|
||||
|
||||
// schemaFile is a schema file's contents, after the rewrites that are applied
|
||||
// before any of it is parsed.
|
||||
type schemaFile struct {
|
||||
name string
|
||||
contents string
|
||||
}
|
||||
|
||||
func (c *Compiler) parseCatalog(schemas []string) error {
|
||||
paths, err := sqlpath.Glob(schemas)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
merr := multierr.New()
|
||||
files := make([]schemaFile, 0, len(paths))
|
||||
for _, path := range paths {
|
||||
blob, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
merr.Add(path, "", 0, err)
|
||||
continue
|
||||
}
|
||||
contents := migrations.RemoveRollbackStatements(string(blob))
|
||||
contents = migrations.RemovePsqlMetaCommands(contents)
|
||||
files = append(files, schemaFile{name: path, contents: contents})
|
||||
c.schema = append(c.schema, contents)
|
||||
}
|
||||
|
||||
if c.coreAnalysis {
|
||||
// A schema file that could not be read is not part of the schema the
|
||||
// action was keyed on, so there is no catalog to restore and nothing
|
||||
// further to report.
|
||||
if len(merr.Errs()) > 0 {
|
||||
return merr
|
||||
}
|
||||
if err := c.parseCatalogCore(files, merr); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
c.parseCatalogLegacy(files, merr)
|
||||
}
|
||||
|
||||
if len(merr.Errs()) > 0 {
|
||||
return merr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Compiler) parseCatalogLegacy(files []schemaFile, merr *multierr.Error) {
|
||||
for _, file := range files {
|
||||
stmts, err := c.parser.Parse(strings.NewReader(file.contents))
|
||||
if err != nil {
|
||||
// A schema file and a query file are often the same file, so a
|
||||
// query's sqlc syntax can fail here. Look for an explanation
|
||||
// before reporting the syntax error it caused.
|
||||
if reported := addSyntaxErrors(merr, file.name, file.contents, preprocess.File(c.conf.Engine, file.contents)); !reported {
|
||||
merr.Add(file.name, file.contents, 0, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
for i := range stmts {
|
||||
if err := c.catalog.Update(stmts[i], c); err != nil {
|
||||
merr.Add(file.name, file.contents, stmts[i].Pos(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseCatalogCore builds the catalog the analysis core works against: the
|
||||
// dialect's seed with the schema's DDL applied. The two are one cached action,
|
||||
// so a run that has built this catalog before restores it and never parses the
|
||||
// schema at all.
|
||||
func (c *Compiler) parseCatalogCore(files []schemaFile, merr *multierr.Error) error {
|
||||
contents := make([]string, 0, len(files))
|
||||
for _, file := range files {
|
||||
contents = append(contents, file.contents)
|
||||
}
|
||||
|
||||
cat, err := core.NewCached(string(c.conf.Engine), contents, func(cat *core.Catalog) error {
|
||||
for _, file := range files {
|
||||
stmts, err := c.parser.Parse(strings.NewReader(file.contents))
|
||||
if err != nil {
|
||||
// A schema file and a query file are often the same file, so a
|
||||
// query's sqlc syntax can fail here. Look for an explanation
|
||||
// before reporting the syntax error it caused.
|
||||
if reported := addSyntaxErrors(merr, file.name, file.contents, preprocess.File(c.conf.Engine, file.contents)); !reported {
|
||||
merr.Add(file.name, file.contents, 0, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
for i := range stmts {
|
||||
if err := coreschema.Apply(cat, stmts[i].Raw); err != nil {
|
||||
merr.Add(file.name, file.contents, stmts[i].Pos(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
// A catalog the whole schema did not make it into is not the catalog
|
||||
// this action describes, so it is not one to keep.
|
||||
if len(merr.Errs()) > 0 {
|
||||
return merr
|
||||
}
|
||||
return nil
|
||||
}, c.coreDialect)
|
||||
|
||||
if cat == nil {
|
||||
return fmt.Errorf("%s: init catalog: %w", c.conf.Engine, err)
|
||||
}
|
||||
// Whatever apply reported is already in merr, which the caller returns.
|
||||
c.coreCatalog = cat
|
||||
|
||||
// Codegen consumes the catalog through the Result, in the legacy shape.
|
||||
legacy, err := coreResultCatalog(cat)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: dump catalog: %w", c.conf.Engine, err)
|
||||
}
|
||||
c.catalog = legacy
|
||||
return nil
|
||||
}
|
||||
|
||||
// statement is a parsed query awaiting analysis, with what error reporting
|
||||
// needs to place it back in the file it came from.
|
||||
type statement struct {
|
||||
filename string
|
||||
src string
|
||||
pp *preprocess.Result
|
||||
raw *ast.RawStmt
|
||||
}
|
||||
|
||||
func (c *Compiler) parseQueries(o opts.Parser) (*Result, error) {
|
||||
merr := multierr.New()
|
||||
files, err := sqlpath.Glob(c.conf.Queries)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var stmts []statement
|
||||
for _, filename := range files {
|
||||
blob, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
merr.Add(filename, "", 0, err)
|
||||
continue
|
||||
}
|
||||
src := string(blob)
|
||||
|
||||
// sqlc syntax is not SQL. Rewrite it to native placeholders before the
|
||||
// engine parser ever sees the query, so parsers only handle SQL.
|
||||
pp := preprocess.File(c.conf.Engine, src)
|
||||
|
||||
parsed, err := c.parser.Parse(strings.NewReader(pp.Text))
|
||||
if err != nil {
|
||||
if reported := addSyntaxErrors(merr, filename, src, pp); !reported {
|
||||
merr.Add(filename, src, 0, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
for _, stmt := range parsed {
|
||||
stmts = append(stmts, statement{filename: filename, src: src, pp: pp, raw: stmt.Raw})
|
||||
}
|
||||
}
|
||||
|
||||
queries, errs := c.analyzeStatements(stmts, o)
|
||||
|
||||
var q []*Query
|
||||
set := map[string]struct{}{}
|
||||
for i, stmt := range stmts {
|
||||
if err := errs[i]; err != nil {
|
||||
var e *sqlerr.Error
|
||||
loc := stmt.raw.Pos()
|
||||
if errors.As(err, &e) && e.Location != 0 {
|
||||
loc = e.Location
|
||||
}
|
||||
// Locations are reported against the rewritten query; map them
|
||||
// back so errors point at what the user wrote.
|
||||
loc = stmt.pp.Origin(loc)
|
||||
if e != nil && e.Location != 0 {
|
||||
e.Location = loc
|
||||
}
|
||||
merr.Add(stmt.filename, stmt.src, loc, err)
|
||||
// If this rpc unauthenticated error bubbles up, then all future parsing/analysis will fail
|
||||
if errors.Is(err, rpc.ErrUnauthenticated) {
|
||||
return nil, merr
|
||||
}
|
||||
continue
|
||||
}
|
||||
query := queries[i]
|
||||
if query == nil {
|
||||
continue
|
||||
}
|
||||
query.Metadata.Filename = filepath.Base(stmt.filename)
|
||||
queryName := query.Metadata.Name
|
||||
if queryName != "" {
|
||||
if _, exists := set[queryName]; exists {
|
||||
merr.Add(stmt.filename, stmt.src, stmt.pp.Origin(stmt.raw.Pos()), fmt.Errorf("duplicate query name: %s", queryName))
|
||||
continue
|
||||
}
|
||||
set[queryName] = struct{}{}
|
||||
}
|
||||
q = append(q, query)
|
||||
}
|
||||
|
||||
if len(merr.Errs()) > 0 {
|
||||
return nil, merr
|
||||
}
|
||||
if len(q) == 0 {
|
||||
return nil, fmt.Errorf("no queries contained in paths %s", strings.Join(c.conf.Queries, ","))
|
||||
}
|
||||
|
||||
return &Result{
|
||||
Catalog: c.catalog,
|
||||
Queries: q,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// analyzeStatements analyzes each statement, returning the results in the
|
||||
// order the statements were given so that queries and errors come out in
|
||||
// source order however they were produced.
|
||||
//
|
||||
// The analysis core reads the catalog and nothing else, so its statements are
|
||||
// analyzed concurrently. Every other path holds state that a second goroutine
|
||||
// would race — a database connection, the legacy catalog — and stays serial.
|
||||
func (c *Compiler) analyzeStatements(stmts []statement, o opts.Parser) ([]*Query, []error) {
|
||||
queries := make([]*Query, len(stmts))
|
||||
errs := make([]error, len(stmts))
|
||||
|
||||
analyze := func(i int) {
|
||||
queries[i], errs[i] = c.parseQuery(stmts[i].raw, stmts[i].pp, o)
|
||||
}
|
||||
|
||||
workers := runtime.GOMAXPROCS(0)
|
||||
if !c.coreAnalysis || workers < 2 || len(stmts) < 2 {
|
||||
for i := range stmts {
|
||||
analyze(i)
|
||||
}
|
||||
return queries, errs
|
||||
}
|
||||
|
||||
var g errgroup.Group
|
||||
g.SetLimit(workers)
|
||||
for i := range stmts {
|
||||
g.Go(func() error {
|
||||
analyze(i)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
g.Wait()
|
||||
return queries, errs
|
||||
}
|
||||
|
||||
func addSyntaxErrors(merr *multierr.Error, filename, src string, pp *preprocess.Result) bool {
|
||||
var found bool
|
||||
for _, stmt := range pp.Statements() {
|
||||
if stmt.Err == nil {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
loc := pp.Origin(stmt.Start)
|
||||
var e *sqlerr.Error
|
||||
if errors.As(stmt.Err, &e) && e.Location != 0 {
|
||||
loc = pp.Origin(e.Location)
|
||||
e.Location = loc
|
||||
}
|
||||
merr.Add(filename, src, loc, stmt.Err)
|
||||
}
|
||||
return found
|
||||
}
|
||||
199
internal/compiler/engine.go
Normal file
199
internal/compiler/engine.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/analyzer"
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
"github.com/sqlc-dev/sqlc/internal/core"
|
||||
"github.com/sqlc-dev/sqlc/internal/dbmanager"
|
||||
"github.com/sqlc-dev/sqlc/internal/engine/clickhouse"
|
||||
"github.com/sqlc-dev/sqlc/internal/engine/dolphin"
|
||||
"github.com/sqlc-dev/sqlc/internal/engine/duckdb"
|
||||
"github.com/sqlc-dev/sqlc/internal/engine/googlesql"
|
||||
"github.com/sqlc-dev/sqlc/internal/engine/mssql"
|
||||
"github.com/sqlc-dev/sqlc/internal/engine/postgresql"
|
||||
pganalyze "github.com/sqlc-dev/sqlc/internal/engine/postgresql/analyzer"
|
||||
"github.com/sqlc-dev/sqlc/internal/engine/sqlite"
|
||||
sqliteanalyze "github.com/sqlc-dev/sqlc/internal/engine/sqlite/analyzer"
|
||||
"github.com/sqlc-dev/sqlc/internal/opts"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
|
||||
)
|
||||
|
||||
type Compiler struct {
|
||||
conf config.SQL
|
||||
combo config.CombinedSettings
|
||||
catalog *catalog.Catalog
|
||||
parser Parser
|
||||
result *Result
|
||||
analyzer analyzer.Analyzer
|
||||
client dbmanager.Client
|
||||
selector selector
|
||||
|
||||
coreCatalog *core.Catalog
|
||||
|
||||
// coreAnalysis routes every engine through the core catalog and analyzer,
|
||||
// and coreDialect seeds the catalog it does so with. The catalog itself is
|
||||
// not built until the schema is loaded, because the two are cached
|
||||
// together.
|
||||
coreAnalysis bool
|
||||
coreDialect core.Option
|
||||
|
||||
// newParser builds a parser for the configured engine, and is set for
|
||||
// every engine either path supports. The core path analyzes statements
|
||||
// concurrently, and a parser holds enough state that two goroutines cannot
|
||||
// share one, so a goroutine that has to parse something of its own — the
|
||||
// query text it just rewrote — builds its own rather than taking c.parser.
|
||||
newParser func() Parser
|
||||
|
||||
schema []string
|
||||
}
|
||||
|
||||
// Option configures a Compiler.
|
||||
type Option func(*Compiler)
|
||||
|
||||
// WithCoreAnalysis analyzes queries with the core catalog and analyzer rather
|
||||
// than each engine's own analysis path. ClickHouse and GoogleSQL always do;
|
||||
// this opts the remaining engines in.
|
||||
func WithCoreAnalysis() Option {
|
||||
return func(c *Compiler) { c.coreAnalysis = true }
|
||||
}
|
||||
|
||||
func NewCompiler(conf config.SQL, combo config.CombinedSettings, parserOpts opts.Parser, options ...Option) (*Compiler, error) {
|
||||
c := &Compiler{conf: conf, combo: combo}
|
||||
for _, o := range options {
|
||||
o(c)
|
||||
}
|
||||
|
||||
// ClickHouse, GoogleSQL, SQL Server and DuckDB have no legacy analysis
|
||||
// path to fall back to.
|
||||
switch conf.Engine {
|
||||
case config.EngineClickHouse, config.EngineGoogleSQL, config.EngineMSSQL, config.EngineDuckDB:
|
||||
c.coreAnalysis = true
|
||||
}
|
||||
if c.coreAnalysis {
|
||||
if err := c.initCore(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
if conf.Database != nil && conf.Database.Managed {
|
||||
client := dbmanager.NewClient(combo.Global.Servers)
|
||||
c.client = client
|
||||
}
|
||||
|
||||
switch conf.Engine {
|
||||
case config.EngineSQLite:
|
||||
c.newParser = func() Parser { return sqlite.NewParser() }
|
||||
c.catalog = sqlite.NewCatalog()
|
||||
c.selector = newSQLiteSelector()
|
||||
|
||||
if conf.Database != nil {
|
||||
if conf.Analyzer.Database == nil || *conf.Analyzer.Database {
|
||||
c.analyzer = analyzer.Cached(
|
||||
sqliteanalyze.New(*conf.Database),
|
||||
combo.Global,
|
||||
*conf.Database,
|
||||
)
|
||||
}
|
||||
}
|
||||
case config.EngineMySQL:
|
||||
c.newParser = func() Parser { return dolphin.NewParser() }
|
||||
c.catalog = dolphin.NewCatalog()
|
||||
c.selector = newDefaultSelector()
|
||||
case config.EnginePostgreSQL:
|
||||
c.newParser = func() Parser { return postgresql.NewParser() }
|
||||
c.catalog = postgresql.NewCatalog()
|
||||
c.selector = newDefaultSelector()
|
||||
|
||||
if conf.Database != nil {
|
||||
if conf.Analyzer.Database == nil || *conf.Analyzer.Database {
|
||||
c.analyzer = analyzer.Cached(
|
||||
pganalyze.New(c.client, *conf.Database),
|
||||
combo.Global,
|
||||
*conf.Database,
|
||||
)
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown engine: %s", conf.Engine)
|
||||
}
|
||||
c.parser = c.newParser()
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// initCore wires up the engine's parser and a core catalog seeded with its
|
||||
// dialect. No legacy catalog and no analyzer connection are involved.
|
||||
func (c *Compiler) initCore() error {
|
||||
var dialect core.Option
|
||||
switch c.conf.Engine {
|
||||
case config.EngineSQLite:
|
||||
c.newParser = func() Parser { return sqlite.NewParser() }
|
||||
c.selector = newSQLiteSelector()
|
||||
dialect = sqlite.Dialect()
|
||||
case config.EngineMySQL:
|
||||
c.newParser = func() Parser { return dolphin.NewParser() }
|
||||
c.selector = newDefaultSelector()
|
||||
dialect = dolphin.Dialect()
|
||||
case config.EnginePostgreSQL:
|
||||
c.newParser = func() Parser { return postgresql.NewParser() }
|
||||
c.selector = newDefaultSelector()
|
||||
dialect = postgresql.Dialect()
|
||||
case config.EngineClickHouse:
|
||||
c.newParser = func() Parser { return clickhouse.NewParser() }
|
||||
c.selector = newDefaultSelector()
|
||||
dialect = clickhouse.Dialect()
|
||||
case config.EngineGoogleSQL:
|
||||
c.newParser = func() Parser { return googlesql.NewParser() }
|
||||
c.selector = newDefaultSelector()
|
||||
dialect = googlesql.Dialect()
|
||||
case config.EngineMSSQL:
|
||||
c.newParser = func() Parser { return mssql.NewParser() }
|
||||
c.selector = newDefaultSelector()
|
||||
dialect = mssql.Dialect()
|
||||
case config.EngineDuckDB:
|
||||
c.newParser = func() Parser { return duckdb.NewParser() }
|
||||
c.selector = newDefaultSelector()
|
||||
dialect = duckdb.Dialect()
|
||||
default:
|
||||
return fmt.Errorf("unknown engine: %s", c.conf.Engine)
|
||||
}
|
||||
c.parser = c.newParser()
|
||||
c.coreDialect = dialect
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Compiler) Catalog() *catalog.Catalog {
|
||||
return c.catalog
|
||||
}
|
||||
|
||||
func (c *Compiler) ParseCatalog(schema []string) error {
|
||||
return c.parseCatalog(schema)
|
||||
}
|
||||
|
||||
func (c *Compiler) ParseQueries(queries []string, o opts.Parser) error {
|
||||
r, err := c.parseQueries(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.result = r
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Compiler) Result() *Result {
|
||||
return c.result
|
||||
}
|
||||
|
||||
func (c *Compiler) Close(ctx context.Context) {
|
||||
if c.analyzer != nil {
|
||||
c.analyzer.Close(ctx)
|
||||
}
|
||||
if c.client != nil {
|
||||
c.client.Close(ctx)
|
||||
}
|
||||
if c.coreCatalog != nil {
|
||||
c.coreCatalog.Close()
|
||||
}
|
||||
}
|
||||
211
internal/compiler/expand.go
Normal file
211
internal/compiler/expand.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
"github.com/sqlc-dev/sqlc/internal/source"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
|
||||
)
|
||||
|
||||
func (c *Compiler) expand(qc *QueryCatalog, raw *ast.RawStmt) ([]source.Edit, error) {
|
||||
// Return early if there are no A_Star nodes to expand
|
||||
stars := astutils.Search(raw, func(node ast.Node) bool {
|
||||
_, ok := node.(*ast.A_Star)
|
||||
return ok
|
||||
})
|
||||
if len(stars.Items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
list := astutils.Search(raw, func(node ast.Node) bool {
|
||||
switch node.(type) {
|
||||
case *ast.DeleteStmt:
|
||||
case *ast.InsertStmt:
|
||||
case *ast.SelectStmt:
|
||||
case *ast.UpdateStmt:
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if len(list.Items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var edits []source.Edit
|
||||
for _, item := range list.Items {
|
||||
edit, err := c.expandStmt(qc, raw, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
edits = append(edits, edit...)
|
||||
}
|
||||
return edits, nil
|
||||
}
|
||||
|
||||
var validPostgresIdent = regexp.MustCompile(`^[a-z_][a-z0-9_$]*$`)
|
||||
|
||||
func (c *Compiler) quoteIdent(ident string) string {
|
||||
if c.parser.IsReservedKeyword(ident) {
|
||||
return c.quote(ident)
|
||||
}
|
||||
// SQL identifiers and key words must begin with a letter (a-z, but also
|
||||
// letters with diacritical marks and non-Latin letters) or an underscore
|
||||
// (_). Subsequent characters in an identifier or key word can be letters,
|
||||
// underscores, digits (0-9), or dollar signs ($).
|
||||
//
|
||||
// https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
|
||||
if c.conf.Engine == config.EnginePostgreSQL {
|
||||
// camelCase means the column is also camelCase
|
||||
if strings.ToLower(ident) != ident {
|
||||
return c.quote(ident)
|
||||
}
|
||||
if !validPostgresIdent.MatchString(strings.ToLower(ident)) {
|
||||
return c.quote(ident)
|
||||
}
|
||||
}
|
||||
return ident
|
||||
}
|
||||
|
||||
func (c *Compiler) quote(x string) string {
|
||||
switch c.conf.Engine {
|
||||
case config.EngineMySQL:
|
||||
return "`" + x + "`"
|
||||
default:
|
||||
return "\"" + x + "\""
|
||||
}
|
||||
}
|
||||
|
||||
// starOldFunc measures how much of the query text a star reference occupies,
|
||||
// so an edit replaces the reference and nothing else. Each part is measured
|
||||
// both bare and quoted: an embed was rewritten to "table.*" in the query text,
|
||||
// preserving the way the user quoted the table, so it is measured the same way
|
||||
// as a star reference the user wrote.
|
||||
func (c *Compiler) starOldFunc(parts []string) func(string) int {
|
||||
old := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
if p == "*" {
|
||||
old = append(old, p)
|
||||
} else {
|
||||
old = append(old, c.quoteIdent(p))
|
||||
}
|
||||
}
|
||||
return func(s string) int {
|
||||
length := 0
|
||||
for i, o := range old {
|
||||
if hasSeparator := i > 0; hasSeparator {
|
||||
length++
|
||||
}
|
||||
if strings.HasPrefix(s[length:], o) {
|
||||
length += len(o)
|
||||
} else if quoted := c.quote(o); strings.HasPrefix(s[length:], quoted) {
|
||||
length += len(quoted)
|
||||
} else {
|
||||
length += len(o)
|
||||
}
|
||||
}
|
||||
return length
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Compiler) expandStmt(qc *QueryCatalog, raw *ast.RawStmt, node ast.Node) ([]source.Edit, error) {
|
||||
tables, err := c.sourceTables(qc, node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Virtual tables for the OLD and NEW aliases available in a RETURNING
|
||||
// clause (PostgreSQL 18)
|
||||
rtables, err := c.returningTables(qc, node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var targets *ast.List
|
||||
switch n := node.(type) {
|
||||
case *ast.DeleteStmt:
|
||||
targets = n.ReturningList
|
||||
case *ast.InsertStmt:
|
||||
targets = n.ReturningList
|
||||
case *ast.SelectStmt:
|
||||
targets = n.TargetList
|
||||
case *ast.UpdateStmt:
|
||||
targets = n.ReturningList
|
||||
default:
|
||||
return nil, fmt.Errorf("outputColumns: unsupported node type: %T", n)
|
||||
}
|
||||
|
||||
var edits []source.Edit
|
||||
for _, target := range targets.Items {
|
||||
res, ok := target.(*ast.ResTarget)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ref, ok := res.Val.(*ast.ColumnRef)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !hasStarRef(ref) {
|
||||
continue
|
||||
}
|
||||
var parts, cols []string
|
||||
for _, f := range ref.Fields.Items {
|
||||
switch field := f.(type) {
|
||||
case *ast.String:
|
||||
parts = append(parts, field.Str)
|
||||
case *ast.A_Star:
|
||||
parts = append(parts, "*")
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown field in ColumnRef: %T", f)
|
||||
}
|
||||
}
|
||||
scope := astutils.Join(ref.Fields, ".")
|
||||
counts := map[string]int{}
|
||||
if scope == "" {
|
||||
for _, t := range tables {
|
||||
for _, c := range t.Columns {
|
||||
counts[c.Name] += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
starTables := tables
|
||||
if vt := returningTableForScope(tables, rtables, scope); vt != nil {
|
||||
starTables = []*Table{vt}
|
||||
}
|
||||
for _, t := range starTables {
|
||||
if scope != "" && scope != t.Rel.Name {
|
||||
continue
|
||||
}
|
||||
tableName := c.quoteIdent(t.Rel.Name)
|
||||
scopeName := c.quoteIdent(scope)
|
||||
for _, column := range t.Columns {
|
||||
cname := column.Name
|
||||
if res.Name != nil {
|
||||
cname = *res.Name
|
||||
}
|
||||
cname = c.quoteIdent(cname)
|
||||
if scope != "" {
|
||||
cname = scopeName + "." + cname
|
||||
}
|
||||
if counts[cname] > 1 {
|
||||
cname = tableName + "." + cname
|
||||
}
|
||||
|
||||
// This is important for SQLite in particular which needs to
|
||||
// wrap jsonb column values with `json(colname)` so they're in a
|
||||
// publicly usable format (i.e. not jsonb).
|
||||
cname = c.selector.ColumnExpr(cname, column)
|
||||
cols = append(cols, cname)
|
||||
}
|
||||
}
|
||||
edits = append(edits, source.Edit{
|
||||
Location: res.Location - raw.StmtLocation,
|
||||
OldFunc: c.starOldFunc(parts),
|
||||
New: strings.Join(cols, ", "),
|
||||
})
|
||||
}
|
||||
|
||||
return edits, nil
|
||||
}
|
||||
76
internal/compiler/expand_core.go
Normal file
76
internal/compiler/expand_core.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/core"
|
||||
"github.com/sqlc-dev/sqlc/internal/source"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
)
|
||||
|
||||
// expandCore rewrites the stars in a query's text with the columns the core
|
||||
// analyzer resolved them to. The analyzer has already walked the statement and
|
||||
// its subqueries, so there is nothing left to look up here: what remains is
|
||||
// deciding how each name is written, which is the engine's business and not
|
||||
// the core's.
|
||||
func (c *Compiler) expandCore(raw *ast.RawStmt, stars []core.StarExpansion) []source.Edit {
|
||||
if len(stars) == 0 {
|
||||
return nil
|
||||
}
|
||||
edits := make([]source.Edit, 0, len(stars))
|
||||
seen := make(map[int]bool, len(stars))
|
||||
for _, star := range stars {
|
||||
// The analyzer types an expression again when a later clause refers to
|
||||
// the output name it was given, so "SELECT (SELECT * FROM t) AS x ...
|
||||
// GROUP BY x" reports the star in the subquery once per pass. The
|
||||
// passes agree on what the star covers, and editing the same reference
|
||||
// twice would overlap, so only the first is kept.
|
||||
if seen[star.Location] {
|
||||
continue
|
||||
}
|
||||
seen[star.Location] = true
|
||||
|
||||
// Everything before the star qualifies it: "foo.*" is scoped to foo,
|
||||
// while a bare "*" covers every relation in the FROM clause.
|
||||
scope := strings.Join(star.Fields[:len(star.Fields)-1], ".")
|
||||
|
||||
// An unqualified star that covers more than one relation may name the
|
||||
// same column twice, so those are written with their relation.
|
||||
counts := map[string]int{}
|
||||
if scope == "" {
|
||||
for _, col := range star.Columns {
|
||||
counts[col.Name]++
|
||||
}
|
||||
}
|
||||
|
||||
cols := make([]string, 0, len(star.Columns))
|
||||
for _, col := range star.Columns {
|
||||
cname := col.Name
|
||||
if star.Alias != "" {
|
||||
cname = star.Alias
|
||||
}
|
||||
cname = c.quoteIdent(cname)
|
||||
if scope != "" {
|
||||
cname = c.quoteIdent(scope) + "." + cname
|
||||
}
|
||||
if counts[cname] > 1 {
|
||||
cname = c.quoteIdent(col.Relation) + "." + cname
|
||||
}
|
||||
|
||||
// This is important for SQLite in particular which needs to wrap
|
||||
// jsonb column values with `json(colname)` so they're in a publicly
|
||||
// usable format (i.e. not jsonb).
|
||||
cols = append(cols, c.selector.ColumnExpr(cname, &Column{
|
||||
Name: col.Name,
|
||||
DataType: col.DataType,
|
||||
}))
|
||||
}
|
||||
|
||||
edits = append(edits, source.Edit{
|
||||
Location: star.Location - raw.StmtLocation,
|
||||
OldFunc: c.starOldFunc(star.Fields),
|
||||
New: strings.Join(cols, ", "),
|
||||
})
|
||||
}
|
||||
return edits
|
||||
}
|
||||
214
internal/compiler/find_params.go
Normal file
214
internal/compiler/find_params.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
|
||||
)
|
||||
|
||||
func findParameters(root ast.Node) ([]paramRef, []error) {
|
||||
refs := make([]paramRef, 0)
|
||||
errors := make([]error, 0)
|
||||
v := paramSearch{seen: make(map[int]struct{}), refs: &refs, errs: &errors}
|
||||
astutils.Walk(v, root)
|
||||
if len(*v.errs) > 0 {
|
||||
return refs, *v.errs
|
||||
} else {
|
||||
return refs, nil
|
||||
}
|
||||
}
|
||||
|
||||
type paramRef struct {
|
||||
parent ast.Node
|
||||
rv *ast.RangeVar
|
||||
ref *ast.ParamRef
|
||||
name string // Named parameter support
|
||||
}
|
||||
|
||||
type paramSearch struct {
|
||||
parent ast.Node
|
||||
rangeVar *ast.RangeVar
|
||||
refs *[]paramRef
|
||||
seen map[int]struct{}
|
||||
errs *[]error
|
||||
|
||||
// XXX: Gross state hack for limit
|
||||
limitCount ast.Node
|
||||
limitOffset ast.Node
|
||||
}
|
||||
|
||||
type limitCount struct {
|
||||
}
|
||||
|
||||
func (l *limitCount) Pos() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
type limitOffset struct {
|
||||
}
|
||||
|
||||
func (l *limitOffset) Pos() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (p paramSearch) Visit(node ast.Node) astutils.Visitor {
|
||||
switch n := node.(type) {
|
||||
|
||||
case *ast.A_Expr:
|
||||
p.parent = node
|
||||
|
||||
case *ast.BetweenExpr:
|
||||
p.parent = node
|
||||
|
||||
case *ast.CallStmt:
|
||||
p.parent = n.FuncCall
|
||||
|
||||
case *ast.DeleteStmt:
|
||||
if n.LimitCount != nil {
|
||||
p.limitCount = n.LimitCount
|
||||
}
|
||||
|
||||
case *ast.FuncCall:
|
||||
p.parent = node
|
||||
|
||||
case *ast.InsertStmt:
|
||||
if s, ok := n.SelectStmt.(*ast.SelectStmt); ok {
|
||||
for i, item := range s.TargetList.Items {
|
||||
target, ok := item.(*ast.ResTarget)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ref, ok := target.Val.(*ast.ParamRef)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if len(n.Cols.Items) <= i {
|
||||
*p.errs = append(*p.errs, fmt.Errorf("INSERT has more expressions than target columns"))
|
||||
return p
|
||||
}
|
||||
*p.refs = append(*p.refs, paramRef{parent: n.Cols.Items[i], ref: ref, rv: n.Relation})
|
||||
p.seen[ref.Location] = struct{}{}
|
||||
}
|
||||
for _, item := range s.ValuesLists.Items {
|
||||
vl, ok := item.(*ast.List)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for i, v := range vl.Items {
|
||||
ref, ok := v.(*ast.ParamRef)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if len(n.Cols.Items) <= i {
|
||||
*p.errs = append(*p.errs, fmt.Errorf("INSERT has more expressions than target columns"))
|
||||
return p
|
||||
}
|
||||
*p.refs = append(*p.refs, paramRef{parent: n.Cols.Items[i], ref: ref, rv: n.Relation})
|
||||
p.seen[ref.Location] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case *ast.UpdateStmt:
|
||||
for _, item := range n.TargetList.Items {
|
||||
target, ok := item.(*ast.ResTarget)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ref, ok := target.Val.(*ast.ParamRef)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, relation := range n.Relations.Items {
|
||||
rv, ok := relation.(*ast.RangeVar)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
*p.refs = append(*p.refs, paramRef{parent: target, ref: ref, rv: rv})
|
||||
}
|
||||
p.seen[ref.Location] = struct{}{}
|
||||
}
|
||||
if n.LimitCount != nil {
|
||||
p.limitCount = n.LimitCount
|
||||
}
|
||||
|
||||
case *ast.RangeVar:
|
||||
p.rangeVar = n
|
||||
|
||||
case *ast.ResTarget:
|
||||
p.parent = node
|
||||
|
||||
case *ast.SelectStmt:
|
||||
if n.LimitCount != nil {
|
||||
p.limitCount = n.LimitCount
|
||||
}
|
||||
if n.LimitOffset != nil {
|
||||
p.limitOffset = n.LimitOffset
|
||||
}
|
||||
|
||||
case *ast.TypeCast:
|
||||
p.parent = node
|
||||
|
||||
case *ast.ParamRef:
|
||||
parent := p.parent
|
||||
|
||||
if count, ok := p.limitCount.(*ast.ParamRef); ok {
|
||||
if n.Number == count.Number {
|
||||
parent = &limitCount{}
|
||||
}
|
||||
}
|
||||
|
||||
if offset, ok := p.limitOffset.(*ast.ParamRef); ok {
|
||||
if n.Number == offset.Number {
|
||||
parent = &limitOffset{}
|
||||
}
|
||||
}
|
||||
if _, found := p.seen[n.Location]; found {
|
||||
break
|
||||
}
|
||||
|
||||
// Special, terrible case for *ast.MultiAssignRef
|
||||
set := true
|
||||
if res, ok := parent.(*ast.ResTarget); ok {
|
||||
if multi, ok := res.Val.(*ast.MultiAssignRef); ok {
|
||||
set = false
|
||||
if row, ok := multi.Source.(*ast.RowExpr); ok {
|
||||
for i, arg := range row.Args.Items {
|
||||
if ref, ok := arg.(*ast.ParamRef); ok {
|
||||
if multi.Colno == i+1 && ref.Number == n.Number {
|
||||
set = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if set {
|
||||
*p.refs = append(*p.refs, paramRef{parent: parent, ref: n, rv: p.rangeVar})
|
||||
p.seen[n.Location] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
|
||||
case *ast.In:
|
||||
if n.Sel == nil {
|
||||
p.parent = node
|
||||
} else {
|
||||
if sel, ok := n.Sel.(*ast.SelectStmt); ok && sel.FromClause != nil && len(sel.FromClause.Items) > 0 {
|
||||
from := sel.FromClause
|
||||
if schema, ok := from.Items[0].(*ast.RangeVar); ok && schema != nil {
|
||||
p.rangeVar = &ast.RangeVar{
|
||||
Catalogname: schema.Catalogname,
|
||||
Schemaname: schema.Schemaname,
|
||||
Relname: schema.Relname,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, ok := n.Expr.(*ast.ParamRef); ok {
|
||||
p.Visit(n.Expr)
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
812
internal/compiler/output_columns.go
Normal file
812
internal/compiler/output_columns.go
Normal file
@@ -0,0 +1,812 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/lang"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/sqlerr"
|
||||
)
|
||||
|
||||
// OutputColumns determines which columns a statement will output
|
||||
func (c *Compiler) OutputColumns(stmt ast.Node) ([]*catalog.Column, error) {
|
||||
qc, err := c.buildQueryCatalog(c.catalog, stmt, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cols, err := c.outputColumns(qc, stmt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
catCols := make([]*catalog.Column, 0, len(cols))
|
||||
for _, col := range cols {
|
||||
catCols = append(catCols, &catalog.Column{
|
||||
Name: col.Name,
|
||||
Type: ast.TypeName{Name: col.DataType},
|
||||
IsNotNull: col.NotNull,
|
||||
IsUnsigned: col.Unsigned,
|
||||
IsArray: col.IsArray,
|
||||
ArrayDims: col.ArrayDims,
|
||||
Comment: col.Comment,
|
||||
Length: col.Length,
|
||||
})
|
||||
}
|
||||
return catCols, nil
|
||||
}
|
||||
|
||||
func hasStarRef(cf *ast.ColumnRef) bool {
|
||||
for _, item := range cf.Fields.Items {
|
||||
if _, ok := item.(*ast.A_Star); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Compute the output columns for a statement.
|
||||
//
|
||||
// Return an error if column references are ambiguous
|
||||
// Return an error if column references don't exist
|
||||
func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, error) {
|
||||
tables, err := c.sourceTables(qc, node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Virtual tables for the OLD and NEW aliases available in a RETURNING
|
||||
// clause (PostgreSQL 18)
|
||||
rtables, err := c.returningTables(qc, node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
targets := &ast.List{}
|
||||
switch n := node.(type) {
|
||||
case *ast.DeleteStmt:
|
||||
targets = n.ReturningList
|
||||
case *ast.InsertStmt:
|
||||
targets = n.ReturningList
|
||||
case *ast.SelectStmt:
|
||||
targets = n.TargetList
|
||||
isUnion := len(targets.Items) == 0 && n.Larg != nil
|
||||
|
||||
if n.GroupClause != nil {
|
||||
for _, item := range n.GroupClause.Items {
|
||||
if err := c.findColumnForNode(item, tables, targets); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
validateOrderBy := true
|
||||
if c.conf.StrictOrderBy != nil {
|
||||
validateOrderBy = *c.conf.StrictOrderBy
|
||||
}
|
||||
if !isUnion && validateOrderBy {
|
||||
if n.SortClause != nil {
|
||||
for _, item := range n.SortClause.Items {
|
||||
sb, ok := item.(*ast.SortBy)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if err := c.findColumnForNode(sb.Node, tables, targets); err != nil {
|
||||
return nil, fmt.Errorf("%v: if you want to skip this validation, set 'strict_order_by' to false", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if n.WindowClause != nil {
|
||||
for _, item := range n.WindowClause.Items {
|
||||
sb, ok := item.(*ast.List)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, single := range sb.Items {
|
||||
caseExpr, ok := single.(*ast.CaseExpr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if err := c.findColumnForNode(caseExpr.Xpr, tables, targets); err != nil {
|
||||
return nil, fmt.Errorf("%v: if you want to skip this validation, set 'strict_order_by' to false", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For UNION queries, targets is empty and we need to look for the
|
||||
// columns in Largs.
|
||||
if isUnion {
|
||||
return c.outputColumns(qc, n.Larg)
|
||||
}
|
||||
case *ast.UpdateStmt:
|
||||
targets = n.ReturningList
|
||||
}
|
||||
|
||||
var cols []*Column
|
||||
|
||||
for _, target := range targets.Items {
|
||||
res, ok := target.(*ast.ResTarget)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch n := res.Val.(type) {
|
||||
|
||||
case *ast.A_Const:
|
||||
name := ""
|
||||
if res.Name != nil {
|
||||
name = *res.Name
|
||||
}
|
||||
switch n.Val.(type) {
|
||||
case *ast.String:
|
||||
cols = append(cols, &Column{Name: name, DataType: "text", NotNull: true})
|
||||
case *ast.Integer:
|
||||
cols = append(cols, &Column{Name: name, DataType: "int", NotNull: true})
|
||||
case *ast.Float:
|
||||
cols = append(cols, &Column{Name: name, DataType: "float", NotNull: true})
|
||||
case *ast.Boolean:
|
||||
cols = append(cols, &Column{Name: name, DataType: "bool", NotNull: true})
|
||||
default:
|
||||
cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false})
|
||||
}
|
||||
|
||||
case *ast.A_Expr:
|
||||
name := ""
|
||||
if res.Name != nil {
|
||||
name = *res.Name
|
||||
}
|
||||
switch op := astutils.Join(n.Name, ""); {
|
||||
case lang.IsComparisonOperator(op):
|
||||
// TODO: Generate a name for these operations
|
||||
cols = append(cols, &Column{Name: name, DataType: "bool", NotNull: true})
|
||||
case lang.IsMathematicalOperator(op):
|
||||
cols = append(cols, &Column{Name: name, DataType: "int", NotNull: true})
|
||||
default:
|
||||
cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false})
|
||||
}
|
||||
|
||||
case *ast.BoolExpr:
|
||||
name := ""
|
||||
if res.Name != nil {
|
||||
name = *res.Name
|
||||
}
|
||||
notNull := false
|
||||
if len(n.Args.Items) == 1 {
|
||||
switch n.Boolop {
|
||||
case ast.BoolExprTypeIsNull, ast.BoolExprTypeIsNotNull:
|
||||
notNull = true
|
||||
case ast.BoolExprTypeNot:
|
||||
sublink, ok := n.Args.Items[0].(*ast.SubLink)
|
||||
if ok && sublink.SubLinkType == ast.EXISTS_SUBLINK {
|
||||
notNull = true
|
||||
if name == "" {
|
||||
name = "not_exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cols = append(cols, &Column{Name: name, DataType: "bool", NotNull: notNull})
|
||||
|
||||
case *ast.CaseExpr:
|
||||
name := ""
|
||||
if res.Name != nil {
|
||||
name = *res.Name
|
||||
}
|
||||
// TODO: The TypeCase and A_Const code has been copied from below. Instead, we
|
||||
// need a recurse function to get the type of a node.
|
||||
if tc, ok := n.Defresult.(*ast.TypeCast); ok {
|
||||
if tc.TypeName == nil {
|
||||
return nil, errors.New("no type name type cast")
|
||||
}
|
||||
name := ""
|
||||
if ref, ok := tc.Arg.(*ast.ColumnRef); ok {
|
||||
name = astutils.Join(ref.Fields, "_")
|
||||
}
|
||||
if res.Name != nil {
|
||||
name = *res.Name
|
||||
}
|
||||
// TODO Validate column names
|
||||
col := toColumn(tc.TypeName)
|
||||
col.Name = name
|
||||
cols = append(cols, col)
|
||||
} else if aconst, ok := n.Defresult.(*ast.A_Const); ok {
|
||||
switch aconst.Val.(type) {
|
||||
case *ast.String:
|
||||
cols = append(cols, &Column{Name: name, DataType: "text", NotNull: true})
|
||||
case *ast.Integer:
|
||||
cols = append(cols, &Column{Name: name, DataType: "int", NotNull: true})
|
||||
case *ast.Float:
|
||||
cols = append(cols, &Column{Name: name, DataType: "float", NotNull: true})
|
||||
case *ast.Boolean:
|
||||
cols = append(cols, &Column{Name: name, DataType: "bool", NotNull: true})
|
||||
default:
|
||||
cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false})
|
||||
}
|
||||
} else {
|
||||
cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false})
|
||||
}
|
||||
|
||||
case *ast.CoalesceExpr:
|
||||
name := "coalesce"
|
||||
if res.Name != nil {
|
||||
name = *res.Name
|
||||
}
|
||||
var firstColumn *Column
|
||||
var shouldNotBeNull bool
|
||||
for _, arg := range n.Args.Items {
|
||||
if _, ok := arg.(*ast.A_Const); ok {
|
||||
shouldNotBeNull = true
|
||||
continue
|
||||
}
|
||||
if ref, ok := arg.(*ast.ColumnRef); ok {
|
||||
columns, err := outputColumnRefs(res, tablesForRef(ref, tables, rtables), ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, c := range columns {
|
||||
if firstColumn == nil {
|
||||
firstColumn = c
|
||||
}
|
||||
shouldNotBeNull = shouldNotBeNull || c.NotNull
|
||||
}
|
||||
}
|
||||
}
|
||||
if firstColumn != nil {
|
||||
firstColumn.NotNull = shouldNotBeNull
|
||||
firstColumn.skipTableRequiredCheck = true
|
||||
cols = append(cols, firstColumn)
|
||||
} else {
|
||||
cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false})
|
||||
}
|
||||
|
||||
case *ast.ColumnRef:
|
||||
if hasStarRef(n) {
|
||||
|
||||
// add a column with a reference to an embedded table
|
||||
if embed, ok := qc.embeds.Find(n.Location, res.Location); ok {
|
||||
cols = append(cols, &Column{
|
||||
Name: embed.Table.Name,
|
||||
EmbedTable: embed.Table,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO: This code is copied in func expand()
|
||||
scope := astutils.Join(n.Fields, ".")
|
||||
starTables := tables
|
||||
if vt := returningTableForScope(tables, rtables, scope); vt != nil {
|
||||
starTables = []*Table{vt}
|
||||
}
|
||||
for _, t := range starTables {
|
||||
if scope != "" && scope != t.Rel.Name {
|
||||
continue
|
||||
}
|
||||
for _, c := range t.Columns {
|
||||
cname := c.Name
|
||||
if res.Name != nil {
|
||||
cname = *res.Name
|
||||
}
|
||||
cols = append(cols, &Column{
|
||||
Name: cname,
|
||||
OriginalName: c.Name,
|
||||
Type: c.Type,
|
||||
Scope: scope,
|
||||
Table: c.Table,
|
||||
TableAlias: t.Rel.Name,
|
||||
DataType: c.DataType,
|
||||
NotNull: c.NotNull,
|
||||
Unsigned: c.Unsigned,
|
||||
IsArray: c.IsArray,
|
||||
ArrayDims: c.ArrayDims,
|
||||
Length: c.Length,
|
||||
})
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
columns, err := outputColumnRefs(res, tablesForRef(n, tables, rtables), n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cols = append(cols, columns...)
|
||||
|
||||
case *ast.FuncCall:
|
||||
rel := n.Func
|
||||
name := rel.Name
|
||||
if res.Name != nil {
|
||||
name = *res.Name
|
||||
}
|
||||
fun, err := qc.catalog.ResolveFuncCall(n)
|
||||
if err == nil {
|
||||
cols = append(cols, &Column{
|
||||
Name: name,
|
||||
DataType: dataType(fun.ReturnType),
|
||||
NotNull: !fun.ReturnTypeNullable,
|
||||
IsFuncCall: true,
|
||||
})
|
||||
} else {
|
||||
cols = append(cols, &Column{
|
||||
Name: name,
|
||||
DataType: "any",
|
||||
IsFuncCall: true,
|
||||
})
|
||||
}
|
||||
|
||||
case *ast.SubLink:
|
||||
name := "exists"
|
||||
if res.Name != nil {
|
||||
name = *res.Name
|
||||
}
|
||||
switch n.SubLinkType {
|
||||
case ast.EXISTS_SUBLINK:
|
||||
cols = append(cols, &Column{Name: name, DataType: "bool", NotNull: true})
|
||||
case ast.EXPR_SUBLINK:
|
||||
subcols, err := c.outputColumns(qc, n.Subselect)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
first := subcols[0]
|
||||
if res.Name != nil {
|
||||
first.Name = *res.Name
|
||||
}
|
||||
cols = append(cols, first)
|
||||
default:
|
||||
cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false})
|
||||
}
|
||||
|
||||
case *ast.TypeCast:
|
||||
if n.TypeName == nil {
|
||||
return nil, errors.New("no type name type cast")
|
||||
}
|
||||
name := ""
|
||||
if ref, ok := n.Arg.(*ast.ColumnRef); ok {
|
||||
name = astutils.Join(ref.Fields, "_")
|
||||
}
|
||||
if res.Name != nil {
|
||||
name = *res.Name
|
||||
}
|
||||
// TODO Validate column names
|
||||
col := toColumn(n.TypeName)
|
||||
col.Name = name
|
||||
// TODO Add correct, real type inference
|
||||
if constant, ok := n.Arg.(*ast.A_Const); ok {
|
||||
if _, ok := constant.Val.(*ast.Null); ok {
|
||||
col.NotNull = false
|
||||
}
|
||||
}
|
||||
cols = append(cols, col)
|
||||
|
||||
case *ast.SelectStmt:
|
||||
subcols, err := c.outputColumns(qc, n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
first := subcols[0]
|
||||
if res.Name != nil {
|
||||
first.Name = *res.Name
|
||||
}
|
||||
cols = append(cols, first)
|
||||
|
||||
default:
|
||||
name := ""
|
||||
if res.Name != nil {
|
||||
name = *res.Name
|
||||
}
|
||||
cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if n, ok := node.(*ast.SelectStmt); ok {
|
||||
for _, col := range cols {
|
||||
if !col.NotNull || col.Table == nil || col.skipTableRequiredCheck {
|
||||
continue
|
||||
}
|
||||
for _, f := range n.FromClause.Items {
|
||||
res := isTableRequired(f, col, tableRequired)
|
||||
if res != tableNotFound {
|
||||
col.NotNull = res == tableRequired
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cols, nil
|
||||
}
|
||||
|
||||
const (
|
||||
tableNotFound = iota
|
||||
tableRequired
|
||||
tableOptional
|
||||
)
|
||||
|
||||
func isTableRequired(n ast.Node, col *Column, prior int) int {
|
||||
switch n := n.(type) {
|
||||
case *ast.RangeVar:
|
||||
tableMatch := *n.Relname == col.Table.Name
|
||||
aliasMatch := true
|
||||
if n.Alias != nil && col.TableAlias != "" {
|
||||
aliasMatch = *n.Alias.Aliasname == col.TableAlias
|
||||
}
|
||||
if aliasMatch && tableMatch {
|
||||
return prior
|
||||
}
|
||||
case *ast.JoinExpr:
|
||||
helper := func(l, r int) int {
|
||||
if res := isTableRequired(n.Larg, col, l); res != tableNotFound {
|
||||
return res
|
||||
}
|
||||
if res := isTableRequired(n.Rarg, col, r); res != tableNotFound {
|
||||
return res
|
||||
}
|
||||
return tableNotFound
|
||||
}
|
||||
switch n.Jointype {
|
||||
case ast.JoinTypeLeft:
|
||||
return helper(tableRequired, tableOptional)
|
||||
case ast.JoinTypeRight:
|
||||
return helper(tableOptional, tableRequired)
|
||||
case ast.JoinTypeFull:
|
||||
return helper(tableOptional, tableOptional)
|
||||
case ast.JoinTypeInner, ast.JoinTypeCross, ast.JoinTypeComma:
|
||||
return helper(tableRequired, tableRequired)
|
||||
}
|
||||
case *ast.List:
|
||||
for _, item := range n.Items {
|
||||
if res := isTableRequired(item, col, prior); res != tableNotFound {
|
||||
return res
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tableNotFound
|
||||
}
|
||||
|
||||
type tableVisitor struct {
|
||||
list ast.List
|
||||
}
|
||||
|
||||
func (r *tableVisitor) Visit(n ast.Node) astutils.Visitor {
|
||||
switch n.(type) {
|
||||
case *ast.RangeVar, *ast.RangeFunction:
|
||||
r.list.Items = append(r.list.Items, n)
|
||||
return r
|
||||
case *ast.RangeSubselect:
|
||||
r.list.Items = append(r.list.Items, n)
|
||||
return nil
|
||||
default:
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
// Compute the output columns for a statement.
|
||||
//
|
||||
// Return an error if column references are ambiguous
|
||||
// Return an error if column references don't exist
|
||||
// Return an error if a table is referenced twice
|
||||
// Return an error if an unknown column is referenced
|
||||
func (c *Compiler) sourceTables(qc *QueryCatalog, node ast.Node) ([]*Table, error) {
|
||||
list := &ast.List{}
|
||||
switch n := node.(type) {
|
||||
case *ast.DeleteStmt:
|
||||
if n.Relations != nil {
|
||||
list = n.Relations
|
||||
} else if n.FromClause != nil {
|
||||
// Multi-table DELETE: walk FromClause to find tables
|
||||
var tv tableVisitor
|
||||
astutils.Walk(&tv, n.FromClause)
|
||||
list = &tv.list
|
||||
}
|
||||
case *ast.InsertStmt:
|
||||
list = &ast.List{
|
||||
Items: []ast.Node{n.Relation},
|
||||
}
|
||||
case *ast.SelectStmt:
|
||||
var tv tableVisitor
|
||||
astutils.Walk(&tv, n.FromClause)
|
||||
list = &tv.list
|
||||
case *ast.TruncateStmt:
|
||||
list = astutils.Search(n.Relations, func(node ast.Node) bool {
|
||||
_, ok := node.(*ast.RangeVar)
|
||||
return ok
|
||||
})
|
||||
case *ast.RefreshMatViewStmt:
|
||||
list = astutils.Search(n.Relation, func(node ast.Node) bool {
|
||||
_, ok := node.(*ast.RangeVar)
|
||||
return ok
|
||||
})
|
||||
case *ast.UpdateStmt:
|
||||
var tv tableVisitor
|
||||
astutils.Walk(&tv, n.FromClause)
|
||||
astutils.Walk(&tv, n.Relations)
|
||||
list = &tv.list
|
||||
}
|
||||
|
||||
var tables []*Table
|
||||
for _, item := range list.Items {
|
||||
switch n := item.(type) {
|
||||
|
||||
case *ast.RangeFunction:
|
||||
var funcCall *ast.FuncCall
|
||||
switch f := n.Functions.Items[0].(type) {
|
||||
case *ast.List:
|
||||
switch fi := f.Items[0].(type) {
|
||||
case *ast.FuncCall:
|
||||
funcCall = fi
|
||||
case *ast.SQLValueFunction:
|
||||
continue // TODO handle this correctly
|
||||
default:
|
||||
continue
|
||||
}
|
||||
case *ast.FuncCall:
|
||||
funcCall = f
|
||||
default:
|
||||
return nil, fmt.Errorf("sourceTables: unsupported function call type %T", n.Functions.Items[0])
|
||||
}
|
||||
|
||||
// If the function or table can't be found, don't error out. There
|
||||
// are many queries that depend on functions unknown to sqlc.
|
||||
fn, err := qc.GetFunc(funcCall.Func)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var table *Table
|
||||
if fn.ReturnType != nil {
|
||||
table, err = qc.GetTable(&ast.TableName{
|
||||
Catalog: fn.ReturnType.Catalog,
|
||||
Schema: fn.ReturnType.Schema,
|
||||
Name: fn.ReturnType.Name,
|
||||
})
|
||||
}
|
||||
if table == nil || err != nil {
|
||||
if n.Alias != nil && len(n.Alias.Colnames.Items) > 0 {
|
||||
table = &Table{}
|
||||
for _, colName := range n.Alias.Colnames.Items {
|
||||
table.Columns = append(table.Columns, &Column{
|
||||
Name: colName.(*ast.String).Str,
|
||||
DataType: "any",
|
||||
})
|
||||
}
|
||||
} else {
|
||||
colName := fn.Rel.Name
|
||||
if n.Alias != nil {
|
||||
colName = *n.Alias.Aliasname
|
||||
}
|
||||
table = &Table{
|
||||
Rel: &ast.TableName{
|
||||
Catalog: fn.Rel.Catalog,
|
||||
Schema: fn.Rel.Schema,
|
||||
Name: fn.Rel.Name,
|
||||
},
|
||||
}
|
||||
if len(fn.Outs) > 0 {
|
||||
for _, arg := range fn.Outs {
|
||||
table.Columns = append(table.Columns, &Column{
|
||||
Name: arg.Name,
|
||||
DataType: arg.Type.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
if fn.ReturnType != nil {
|
||||
table.Columns = []*Column{
|
||||
{
|
||||
Name: colName,
|
||||
DataType: fn.ReturnType.Name,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if n.Alias != nil {
|
||||
table.Rel = &ast.TableName{
|
||||
Name: *n.Alias.Aliasname,
|
||||
}
|
||||
}
|
||||
tables = append(tables, table)
|
||||
|
||||
case *ast.RangeSubselect:
|
||||
cols, err := c.outputColumns(qc, n.Subquery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tableName string
|
||||
if n.Alias != nil {
|
||||
tableName = *n.Alias.Aliasname
|
||||
}
|
||||
|
||||
tables = append(tables, &Table{
|
||||
Rel: &ast.TableName{
|
||||
Name: tableName,
|
||||
},
|
||||
Columns: cols,
|
||||
})
|
||||
|
||||
case *ast.RangeVar:
|
||||
fqn, err := ParseTableName(n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if qc == nil {
|
||||
return nil, fmt.Errorf("query catalog is empty")
|
||||
}
|
||||
table, cerr := qc.GetTable(fqn)
|
||||
if cerr != nil {
|
||||
// TODO: Update error location
|
||||
// cerr.Location = n.Location
|
||||
// return nil, *cerr
|
||||
return nil, cerr
|
||||
}
|
||||
if n.Alias != nil {
|
||||
table.Rel = &ast.TableName{
|
||||
Catalog: table.Rel.Catalog,
|
||||
Schema: table.Rel.Schema,
|
||||
Name: *n.Alias.Aliasname,
|
||||
}
|
||||
}
|
||||
tables = append(tables, table)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("sourceTable: unsupported list item type: %T", n)
|
||||
}
|
||||
}
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
func outputColumnRefs(res *ast.ResTarget, tables []*Table, node *ast.ColumnRef) ([]*Column, error) {
|
||||
parts := stringSlice(node.Fields)
|
||||
var schema, name, alias string
|
||||
switch {
|
||||
case len(parts) == 1:
|
||||
name = parts[0]
|
||||
case len(parts) == 2:
|
||||
alias = parts[0]
|
||||
name = parts[1]
|
||||
case len(parts) == 3:
|
||||
schema = parts[0]
|
||||
alias = parts[1]
|
||||
name = parts[2]
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown number of fields: %d", len(parts))
|
||||
}
|
||||
var cols []*Column
|
||||
var found int
|
||||
for _, t := range tables {
|
||||
if schema != "" && t.Rel.Schema != schema {
|
||||
continue
|
||||
}
|
||||
if alias != "" && t.Rel.Name != alias {
|
||||
continue
|
||||
}
|
||||
for _, c := range t.Columns {
|
||||
|
||||
if c.Name == name {
|
||||
found += 1
|
||||
cname := c.Name
|
||||
if res.Name != nil {
|
||||
cname = *res.Name
|
||||
}
|
||||
cols = append(cols, &Column{
|
||||
Name: cname,
|
||||
Type: c.Type,
|
||||
Table: c.Table,
|
||||
TableAlias: alias,
|
||||
DataType: c.DataType,
|
||||
NotNull: c.NotNull,
|
||||
Unsigned: c.Unsigned,
|
||||
IsArray: c.IsArray,
|
||||
ArrayDims: c.ArrayDims,
|
||||
Length: c.Length,
|
||||
EmbedTable: c.EmbedTable,
|
||||
OriginalName: c.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if found == 0 {
|
||||
return nil, &sqlerr.Error{
|
||||
Code: "42703",
|
||||
Message: fmt.Sprintf("column %q does not exist", name),
|
||||
Location: res.Location,
|
||||
}
|
||||
}
|
||||
if found > 1 {
|
||||
return nil, &sqlerr.Error{
|
||||
Code: "42703",
|
||||
Message: fmt.Sprintf("column reference %q is ambiguous", name),
|
||||
Location: res.Location,
|
||||
}
|
||||
}
|
||||
return cols, nil
|
||||
}
|
||||
|
||||
func (c *Compiler) findColumnForNode(item ast.Node, tables []*Table, targetList *ast.List) error {
|
||||
ref, ok := item.(*ast.ColumnRef)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if c.isImplicitColumnRef(ref) {
|
||||
return nil
|
||||
}
|
||||
return findColumnForRef(ref, tables, targetList)
|
||||
}
|
||||
|
||||
// isImplicitColumnRef reports whether ref names a column that every table in
|
||||
// the dialect has without declaring it, and which therefore never appears in
|
||||
// a catalog built from the schema. SQLite gives each ordinary table a rowid,
|
||||
// reachable under three spellings.
|
||||
func (c *Compiler) isImplicitColumnRef(ref *ast.ColumnRef) bool {
|
||||
if c.conf.Engine != config.EngineSQLite {
|
||||
return false
|
||||
}
|
||||
parts := stringSlice(ref.Fields)
|
||||
if len(parts) == 0 {
|
||||
return false
|
||||
}
|
||||
switch parts[len(parts)-1] {
|
||||
case "rowid", "_rowid_", "oid":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func findColumnForRef(ref *ast.ColumnRef, tables []*Table, targetList *ast.List) error {
|
||||
parts := stringSlice(ref.Fields)
|
||||
var alias, name string
|
||||
if len(parts) == 1 {
|
||||
name = parts[0]
|
||||
} else if len(parts) == 2 {
|
||||
alias = parts[0]
|
||||
name = parts[1]
|
||||
}
|
||||
|
||||
var found int
|
||||
for _, t := range tables {
|
||||
if alias != "" && t.Rel.Name != alias {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find matching column
|
||||
for _, c := range t.Columns {
|
||||
if c.Name == name {
|
||||
found++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find matching alias if necessary
|
||||
if found == 0 {
|
||||
for _, c := range targetList.Items {
|
||||
resTarget, ok := c.(*ast.ResTarget)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if resTarget.Name != nil && *resTarget.Name == name {
|
||||
found++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found == 0 {
|
||||
return &sqlerr.Error{
|
||||
Code: "42703",
|
||||
Message: fmt.Sprintf("column reference %q not found", name),
|
||||
Location: ref.Location,
|
||||
}
|
||||
}
|
||||
if found > 1 {
|
||||
return &sqlerr.Error{
|
||||
Code: "42703",
|
||||
Message: fmt.Sprintf("column reference %q is ambiguous", name),
|
||||
Location: ref.Location,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
201
internal/compiler/parse.go
Normal file
201
internal/compiler/parse.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/debug"
|
||||
"github.com/sqlc-dev/sqlc/internal/metadata"
|
||||
"github.com/sqlc-dev/sqlc/internal/opts"
|
||||
"github.com/sqlc-dev/sqlc/internal/source"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/preprocess"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/validate"
|
||||
"github.com/sqlc-dev/sqlc/internal/sqlcdebug"
|
||||
)
|
||||
|
||||
var debugDumpAST = sqlcdebug.New("dumpast")
|
||||
|
||||
func (c *Compiler) parseQuery(stmt ast.Node, pp *preprocess.Result, o opts.Parser) (*Query, error) {
|
||||
raw, ok := stmt.(*ast.RawStmt)
|
||||
if !ok {
|
||||
return nil, errors.New("node is not a statement")
|
||||
}
|
||||
|
||||
// The preprocessor already replaced every sqlc.* construct with native SQL
|
||||
// and recorded what it replaced.
|
||||
pre := pp.Statement(raw.StmtLocation)
|
||||
if pre.Err != nil {
|
||||
return nil, pre.Err
|
||||
}
|
||||
|
||||
// Engines number bind parameters in whatever order they convert the AST.
|
||||
// Restore the numbering the preprocessor assigned in source order.
|
||||
renumberParams(raw, pre.Numbers)
|
||||
|
||||
if c.coreCatalog != nil {
|
||||
return c.parseQueryCore(raw, pp.Text, pre)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
if debugDumpAST.Value() == "1" {
|
||||
debug.Dump(stmt)
|
||||
}
|
||||
|
||||
rawSQL, err := source.Pluck(pp.Text, raw.StmtLocation, raw.StmtLen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rawSQL == "" {
|
||||
return nil, errors.New("missing semicolon at end of file")
|
||||
}
|
||||
|
||||
name, cmd, err := metadata.ParseQueryNameAndType(rawSQL, metadata.CommentSyntax(c.parser.CommentSyntax()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := validate.Cmd(raw.Stmt, name, cmd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
md := metadata.Metadata{
|
||||
Name: name,
|
||||
Cmd: cmd,
|
||||
}
|
||||
|
||||
// TODO eventually can use this for name and type/cmd parsing too
|
||||
cleanedComments, err := source.CleanedComments(rawSQL, c.parser.CommentSyntax())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
md.Params, md.Flags, md.RuleSkiplist, err = metadata.ParseCommentFlags(cleanedComments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var anlys *analysis
|
||||
if c.analyzer != nil {
|
||||
inference, _ := c.inferQuery(raw, rawSQL, pre)
|
||||
if inference == nil {
|
||||
inference = &analysis{}
|
||||
}
|
||||
if inference.Query == "" {
|
||||
inference.Query = rawSQL
|
||||
}
|
||||
|
||||
result, err := c.analyzer.Analyze(ctx, raw, inference.Query, c.schema, inference.Named)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the query uses star expansion, verify that it was edited. If not,
|
||||
// return an error.
|
||||
stars := astutils.Search(raw, func(node ast.Node) bool {
|
||||
_, ok := node.(*ast.A_Star)
|
||||
return ok
|
||||
})
|
||||
hasStars := len(stars.Items) > 0
|
||||
unchanged := inference.Query == rawSQL
|
||||
if unchanged && hasStars {
|
||||
return nil, fmt.Errorf("star expansion failed for query")
|
||||
}
|
||||
|
||||
// FOOTGUN: combineAnalysis mutates inference
|
||||
anlys = combineAnalysis(inference, result)
|
||||
} else {
|
||||
anlys, err = c.analyzeQuery(raw, rawSQL, pre)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
expanded := anlys.Query
|
||||
|
||||
// If the query string was edited, make sure the syntax is valid
|
||||
if expanded != rawSQL {
|
||||
if _, err := c.parser.Parse(strings.NewReader(expanded)); err != nil {
|
||||
return nil, fmt.Errorf("edited query syntax is invalid: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
trimmed, comments, err := source.StripComments(expanded)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
md.Comments = comments
|
||||
|
||||
return &Query{
|
||||
RawStmt: raw,
|
||||
Metadata: md,
|
||||
Params: anlys.Parameters,
|
||||
Columns: anlys.Columns,
|
||||
SQL: trimmed,
|
||||
InsertIntoTable: anlys.Table,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// renumberParams applies the parameter numbers the preprocessor assigned,
|
||||
// matching each node by the location of its placeholder in the query text.
|
||||
func renumberParams(raw *ast.RawStmt, numbers map[int]int) {
|
||||
if len(numbers) == 0 {
|
||||
return
|
||||
}
|
||||
astutils.Walk(astutils.VisitorFunc(func(node ast.Node) {
|
||||
ref, ok := node.(*ast.ParamRef)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if n, ok := numbers[ref.Location]; ok {
|
||||
ref.Number = n
|
||||
}
|
||||
}), raw)
|
||||
}
|
||||
|
||||
func rangeVars(root ast.Node) []*ast.RangeVar {
|
||||
var vars []*ast.RangeVar
|
||||
find := astutils.VisitorFunc(func(node ast.Node) {
|
||||
switch n := node.(type) {
|
||||
case *ast.RangeVar:
|
||||
vars = append(vars, n)
|
||||
}
|
||||
})
|
||||
astutils.Walk(find, root)
|
||||
return vars
|
||||
}
|
||||
|
||||
func uniqueParamRefs(in []paramRef, dollar bool) []paramRef {
|
||||
m := make(map[int]bool, len(in))
|
||||
o := make([]paramRef, 0, len(in))
|
||||
for _, v := range in {
|
||||
if !m[v.ref.Number] {
|
||||
m[v.ref.Number] = true
|
||||
if v.ref.Number != 0 {
|
||||
o = append(o, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !dollar {
|
||||
start := 1
|
||||
for _, v := range in {
|
||||
if v.ref.Number == 0 {
|
||||
for m[start] {
|
||||
start++
|
||||
}
|
||||
v.ref.Number = start
|
||||
o = append(o, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
return o
|
||||
}
|
||||
165
internal/compiler/parse_core.go
Normal file
165
internal/compiler/parse_core.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/core"
|
||||
coreanalyzer "github.com/sqlc-dev/sqlc/internal/core/analyzer"
|
||||
"github.com/sqlc-dev/sqlc/internal/metadata"
|
||||
"github.com/sqlc-dev/sqlc/internal/source"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/named"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/preprocess"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/validate"
|
||||
)
|
||||
|
||||
func (c *Compiler) parseQueryCore(raw *ast.RawStmt, src string, pre *preprocess.Statement) (*Query, error) {
|
||||
rawSQL, err := source.Pluck(src, raw.StmtLocation, raw.StmtLen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(rawSQL) == "" {
|
||||
return nil, errors.New("missing semicolon at end of file")
|
||||
}
|
||||
|
||||
name, cmd, err := metadata.ParseQueryNameAndType(rawSQL, metadata.CommentSyntax(c.parser.CommentSyntax()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if name == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if err := validate.Cmd(raw.Stmt, name, cmd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
md := metadata.Metadata{Name: name, Cmd: cmd}
|
||||
cleanedComments, err := source.CleanedComments(rawSQL, c.parser.CommentSyntax())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
md.Params, md.Flags, md.RuleSkiplist, err = metadata.ParseCommentFlags(cleanedComments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pre.ParamErr != nil {
|
||||
return nil, pre.ParamErr
|
||||
}
|
||||
namedParams := pre.Params
|
||||
expanded := rawSQL
|
||||
|
||||
var cols []*Column
|
||||
var params []Parameter
|
||||
switch raw.Stmt.(type) {
|
||||
case *ast.SelectStmt, *ast.InsertStmt, *ast.UpdateStmt, *ast.DeleteStmt:
|
||||
res, err := coreanalyzer.Prepare(c.coreCatalog, raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, col := range res.Columns {
|
||||
cols = append(cols, coreColumn(col))
|
||||
}
|
||||
for _, p := range res.Parameters {
|
||||
params = append(params, Parameter{Number: p.Number, Column: coreParamColumn(p, namedParams)})
|
||||
}
|
||||
expanded, err = source.Mutate(rawSQL, c.expandCore(raw, res.Stars))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// If the query string was edited, make sure the syntax is valid
|
||||
if expanded != rawSQL {
|
||||
if _, err := c.newParser().Parse(strings.NewReader(expanded)); err != nil {
|
||||
return nil, fmt.Errorf("edited query syntax is invalid: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
trimmed, comments, err := source.StripComments(expanded)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
md.Comments = comments
|
||||
|
||||
var insertTable *ast.TableName
|
||||
if ins, ok := raw.Stmt.(*ast.InsertStmt); ok {
|
||||
insertTable, _ = ParseTableName(ins.Relation)
|
||||
}
|
||||
|
||||
return &Query{
|
||||
RawStmt: raw,
|
||||
Metadata: md,
|
||||
Params: params,
|
||||
Columns: cols,
|
||||
SQL: trimmed,
|
||||
InsertIntoTable: insertTable,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func coreColumn(c core.Column) *Column {
|
||||
col := &Column{
|
||||
Name: c.Name,
|
||||
DataType: c.DataType,
|
||||
NotNull: c.NotNull,
|
||||
IsArray: c.IsArray,
|
||||
TypeExpr: c.Type,
|
||||
}
|
||||
describeType(col, c.Type)
|
||||
if c.Source != nil && c.Source.Table != "" {
|
||||
col.Table = &ast.TableName{Schema: c.Source.Schema, Name: c.Source.Table}
|
||||
col.TableAlias = c.Source.TableAlias
|
||||
col.OriginalName = c.Source.Column
|
||||
}
|
||||
return col
|
||||
}
|
||||
|
||||
// describeType fills in what codegen reads about a type from its
|
||||
// expression: one array dimension per nesting, the length that is the
|
||||
// innermost type's first integer argument (which is how a MySQL tinyint(1)
|
||||
// is told from a tinyint), and whether the innermost type is unsigned.
|
||||
func describeType(col *Column, t *core.TypeExpr) {
|
||||
if t == nil {
|
||||
if col.IsArray {
|
||||
col.ArrayDims = 1
|
||||
}
|
||||
return
|
||||
}
|
||||
col.ArrayDims = t.ArrayDims()
|
||||
inner := t.Innermost()
|
||||
if len(inner.Args) > 0 && inner.Args[0].Int != nil {
|
||||
l := int(*inner.Args[0].Int)
|
||||
col.Length = &l
|
||||
}
|
||||
col.Unsigned = strings.HasSuffix(inner.Name, " unsigned")
|
||||
}
|
||||
|
||||
func coreParamColumn(p core.Parameter, params *named.ParamSet) *Column {
|
||||
col := &Column{
|
||||
Name: p.Name,
|
||||
DataType: p.DataType,
|
||||
NotNull: p.NotNull,
|
||||
IsArray: p.IsArray,
|
||||
TypeExpr: p.Type,
|
||||
}
|
||||
describeType(col, p.Type)
|
||||
if p.Source != nil && p.Source.Table != "" {
|
||||
col.Table = &ast.TableName{Schema: p.Source.Schema, Name: p.Source.Table}
|
||||
col.OriginalName = p.Source.Column
|
||||
}
|
||||
if col.Name == "" && p.Source != nil {
|
||||
col.Name = p.Source.Column
|
||||
}
|
||||
// Merge in what the user asked for: sqlc.narg() makes the parameter
|
||||
// nullable and sqlc.slice() marks it as a slice, whichever way the
|
||||
// analyzer typed it.
|
||||
if param, isNamed := params.FetchMerge(p.Number, named.NewInferredParam(col.Name, p.NotNull)); isNamed {
|
||||
col.Name = param.Name()
|
||||
col.NotNull = param.NotNull()
|
||||
col.IsSqlcSlice = param.IsSqlcSlice()
|
||||
col.IsNamedParam = true
|
||||
}
|
||||
return col
|
||||
}
|
||||
67
internal/compiler/query.go
Normal file
67
internal/compiler/query.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"github.com/sqlc-dev/sqlc/internal/core"
|
||||
"github.com/sqlc-dev/sqlc/internal/metadata"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
|
||||
)
|
||||
|
||||
type Function struct {
|
||||
Rel *ast.FuncName
|
||||
ReturnType *ast.TypeName
|
||||
Outs []*catalog.Argument
|
||||
}
|
||||
|
||||
type Table struct {
|
||||
Rel *ast.TableName
|
||||
Columns []*Column
|
||||
}
|
||||
|
||||
type Column struct {
|
||||
Name string
|
||||
OriginalName string
|
||||
DataType string
|
||||
NotNull bool
|
||||
Unsigned bool
|
||||
IsArray bool
|
||||
ArrayDims int
|
||||
Comment string
|
||||
Length *int
|
||||
IsNamedParam bool
|
||||
IsFuncCall bool
|
||||
|
||||
// XXX: Figure out what PostgreSQL calls `foo.id`
|
||||
Scope string
|
||||
Table *ast.TableName
|
||||
TableAlias string
|
||||
Type *ast.TypeName
|
||||
EmbedTable *ast.TableName
|
||||
|
||||
// TypeExpr is the type as the analysis core wrote it, with the
|
||||
// arguments and nesting DataType and IsArray flatten away. It is unset
|
||||
// on the legacy path.
|
||||
TypeExpr *core.TypeExpr
|
||||
|
||||
IsSqlcSlice bool // is this sqlc.slice()
|
||||
|
||||
skipTableRequiredCheck bool
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
SQL string
|
||||
Metadata metadata.Metadata
|
||||
Columns []*Column
|
||||
Params []Parameter
|
||||
|
||||
// Needed for CopyFrom
|
||||
InsertIntoTable *ast.TableName
|
||||
|
||||
// Needed for vet
|
||||
RawStmt *ast.RawStmt
|
||||
}
|
||||
|
||||
type Parameter struct {
|
||||
Number int
|
||||
Column *Column
|
||||
}
|
||||
109
internal/compiler/query_catalog.go
Normal file
109
internal/compiler/query_catalog.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/preprocess"
|
||||
)
|
||||
|
||||
type QueryCatalog struct {
|
||||
catalog *catalog.Catalog
|
||||
ctes map[string]*Table
|
||||
embeds preprocess.EmbedSet
|
||||
}
|
||||
|
||||
func (comp *Compiler) buildQueryCatalog(c *catalog.Catalog, node ast.Node, embeds preprocess.EmbedSet) (*QueryCatalog, error) {
|
||||
var with *ast.WithClause
|
||||
switch n := node.(type) {
|
||||
case *ast.DeleteStmt:
|
||||
with = n.WithClause
|
||||
case *ast.InsertStmt:
|
||||
with = n.WithClause
|
||||
case *ast.UpdateStmt:
|
||||
with = n.WithClause
|
||||
case *ast.SelectStmt:
|
||||
with = n.WithClause
|
||||
default:
|
||||
with = nil
|
||||
}
|
||||
qc := &QueryCatalog{catalog: c, ctes: map[string]*Table{}, embeds: embeds}
|
||||
if with != nil {
|
||||
for _, item := range with.Ctes.Items {
|
||||
if cte, ok := item.(*ast.CommonTableExpr); ok {
|
||||
cols, err := comp.outputColumns(qc, cte.Ctequery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var names []string
|
||||
if cte.Aliascolnames != nil {
|
||||
for _, item := range cte.Aliascolnames.Items {
|
||||
if val, ok := item.(*ast.String); ok {
|
||||
names = append(names, val.Str)
|
||||
} else {
|
||||
names = append(names, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
rel := &ast.TableName{Name: *cte.Ctename}
|
||||
for i := range cols {
|
||||
cols[i].Table = rel
|
||||
if len(names) > i {
|
||||
cols[i].Name = names[i]
|
||||
}
|
||||
}
|
||||
qc.ctes[*cte.Ctename] = &Table{
|
||||
Rel: rel,
|
||||
Columns: cols,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return qc, nil
|
||||
}
|
||||
|
||||
func ConvertColumn(rel *ast.TableName, c *catalog.Column) *Column {
|
||||
return &Column{
|
||||
Table: rel,
|
||||
Name: c.Name,
|
||||
DataType: dataType(&c.Type),
|
||||
NotNull: c.IsNotNull,
|
||||
Unsigned: c.IsUnsigned,
|
||||
IsArray: c.IsArray,
|
||||
ArrayDims: c.ArrayDims,
|
||||
Type: &c.Type,
|
||||
Length: c.Length,
|
||||
}
|
||||
}
|
||||
|
||||
func (qc QueryCatalog) GetTable(rel *ast.TableName) (*Table, error) {
|
||||
cte, exists := qc.ctes[rel.Name]
|
||||
if exists {
|
||||
return &Table{Rel: rel, Columns: cte.Columns}, nil
|
||||
}
|
||||
src, err := qc.catalog.GetTable(rel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var cols []*Column
|
||||
for _, c := range src.Columns {
|
||||
cols = append(cols, ConvertColumn(rel, c))
|
||||
}
|
||||
return &Table{Rel: rel, Columns: cols}, nil
|
||||
}
|
||||
|
||||
func (qc QueryCatalog) GetFunc(rel *ast.FuncName) (*Function, error) {
|
||||
funcs, err := qc.catalog.ListFuncsByName(rel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(funcs) == 0 {
|
||||
return nil, fmt.Errorf("function not found: %s", rel.Name)
|
||||
}
|
||||
return &Function{
|
||||
Rel: rel,
|
||||
Outs: funcs[0].OutArgs(),
|
||||
ReturnType: funcs[0].ReturnType,
|
||||
}, nil
|
||||
}
|
||||
638
internal/compiler/resolve.go
Normal file
638
internal/compiler/resolve.go
Normal file
@@ -0,0 +1,638 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/named"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/preprocess"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/sqlerr"
|
||||
)
|
||||
|
||||
func dataType(n *ast.TypeName) string {
|
||||
if n.Schema != "" {
|
||||
return n.Schema + "." + n.Name
|
||||
} else {
|
||||
return n.Name
|
||||
}
|
||||
}
|
||||
|
||||
func (comp *Compiler) resolveCatalogRefs(qc *QueryCatalog, rvs []*ast.RangeVar, args []paramRef, params *named.ParamSet, embeds preprocess.EmbedSet) ([]Parameter, error) {
|
||||
c := comp.catalog
|
||||
|
||||
aliasMap := map[string]*ast.TableName{}
|
||||
// TODO: Deprecate defaultTable
|
||||
var defaultTable *ast.TableName
|
||||
var tables []*ast.TableName
|
||||
|
||||
typeMap := map[string]map[string]map[string]*catalog.Column{}
|
||||
indexTable := func(table catalog.Table) error {
|
||||
tables = append(tables, table.Rel)
|
||||
if defaultTable == nil {
|
||||
defaultTable = table.Rel
|
||||
}
|
||||
schema := table.Rel.Schema
|
||||
if schema == "" {
|
||||
schema = c.DefaultSchema
|
||||
}
|
||||
if _, exists := typeMap[schema]; !exists {
|
||||
typeMap[schema] = map[string]map[string]*catalog.Column{}
|
||||
}
|
||||
typeMap[schema][table.Rel.Name] = map[string]*catalog.Column{}
|
||||
for _, c := range table.Columns {
|
||||
cc := c
|
||||
typeMap[schema][table.Rel.Name][c.Name] = cc
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, rv := range rvs {
|
||||
if rv.Relname == nil {
|
||||
continue
|
||||
}
|
||||
fqn, err := ParseTableName(rv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, found := aliasMap[fqn.Name]; found {
|
||||
continue
|
||||
}
|
||||
table, err := c.GetTable(fqn)
|
||||
if err != nil {
|
||||
if qc == nil {
|
||||
continue
|
||||
}
|
||||
// If the table name doesn't exist, first check if it's a CTE
|
||||
if _, qcerr := qc.GetTable(fqn); qcerr != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
err = indexTable(table)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rv.Alias != nil {
|
||||
aliasMap[*rv.Alias.Aliasname] = fqn
|
||||
}
|
||||
}
|
||||
|
||||
// resolve a table for an embed
|
||||
for _, embed := range embeds {
|
||||
table, err := c.GetTable(embed.Table)
|
||||
if err == nil {
|
||||
embed.Table = table.Rel
|
||||
continue
|
||||
}
|
||||
|
||||
if alias, ok := aliasMap[embed.Table.Name]; ok {
|
||||
embed.Table = alias
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unable to resolve table with %q: %w", embed.Orig(), err)
|
||||
}
|
||||
|
||||
var a []Parameter
|
||||
|
||||
addUnknownParam := func(ref paramRef) {
|
||||
defaultP := named.NewInferredParam(ref.name, false)
|
||||
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
|
||||
a = append(a, Parameter{
|
||||
Number: ref.ref.Number,
|
||||
Column: &Column{
|
||||
Name: p.Name(),
|
||||
DataType: "any",
|
||||
IsNamedParam: isNamed,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
for _, ref := range args {
|
||||
switch n := ref.parent.(type) {
|
||||
|
||||
case *limitOffset:
|
||||
defaultP := named.NewInferredParam("offset", true)
|
||||
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
|
||||
a = append(a, Parameter{
|
||||
Number: ref.ref.Number,
|
||||
Column: &Column{
|
||||
Name: p.Name(),
|
||||
DataType: "integer",
|
||||
NotNull: p.NotNull(),
|
||||
IsNamedParam: isNamed,
|
||||
},
|
||||
})
|
||||
|
||||
case *limitCount:
|
||||
defaultP := named.NewInferredParam("limit", true)
|
||||
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
|
||||
a = append(a, Parameter{
|
||||
Number: ref.ref.Number,
|
||||
Column: &Column{
|
||||
Name: p.Name(),
|
||||
DataType: "integer",
|
||||
NotNull: p.NotNull(),
|
||||
IsNamedParam: isNamed,
|
||||
},
|
||||
})
|
||||
|
||||
case *ast.A_Expr:
|
||||
// TODO: While this works for a wide range of simple expressions,
|
||||
// more complicated expressions will cause this logic to fail.
|
||||
list := astutils.Search(n.Lexpr, func(node ast.Node) bool {
|
||||
_, ok := node.(*ast.ColumnRef)
|
||||
return ok
|
||||
})
|
||||
if len(list.Items) == 0 {
|
||||
list = astutils.Search(n.Rexpr, func(node ast.Node) bool {
|
||||
_, ok := node.(*ast.ColumnRef)
|
||||
return ok
|
||||
})
|
||||
}
|
||||
|
||||
if len(list.Items) == 0 {
|
||||
// TODO: Move this to database-specific engine package
|
||||
dataType := "any"
|
||||
if astutils.Join(n.Name, ".") == "||" {
|
||||
dataType = "text"
|
||||
}
|
||||
|
||||
defaultP := named.NewParam("")
|
||||
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
|
||||
a = append(a, Parameter{
|
||||
Number: ref.ref.Number,
|
||||
Column: &Column{
|
||||
Name: p.Name(),
|
||||
DataType: dataType,
|
||||
IsNamedParam: isNamed,
|
||||
NotNull: p.NotNull(),
|
||||
IsSqlcSlice: p.IsSqlcSlice(),
|
||||
},
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
switch node := list.Items[0].(type) {
|
||||
case *ast.ColumnRef:
|
||||
items := stringSlice(node.Fields)
|
||||
var key, alias string
|
||||
switch len(items) {
|
||||
case 1:
|
||||
key = items[0]
|
||||
case 2:
|
||||
alias = items[0]
|
||||
key = items[1]
|
||||
case 3:
|
||||
// schema := items[0]
|
||||
alias = items[1]
|
||||
key = items[2]
|
||||
default:
|
||||
panic("too many field items: " + strconv.Itoa(len(items)))
|
||||
}
|
||||
|
||||
search := tables
|
||||
if alias != "" {
|
||||
if original, ok := aliasMap[alias]; ok {
|
||||
search = []*ast.TableName{original}
|
||||
} else {
|
||||
var located bool
|
||||
for _, fqn := range tables {
|
||||
if fqn.Name == alias {
|
||||
located = true
|
||||
search = []*ast.TableName{fqn}
|
||||
}
|
||||
}
|
||||
if !located {
|
||||
return nil, &sqlerr.Error{
|
||||
Code: "42703",
|
||||
Message: fmt.Sprintf("table alias %q does not exist", alias),
|
||||
Location: node.Location,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var found int
|
||||
for _, table := range search {
|
||||
schema := table.Schema
|
||||
if schema == "" {
|
||||
schema = c.DefaultSchema
|
||||
}
|
||||
if c, ok := typeMap[schema][table.Name][key]; ok {
|
||||
found += 1
|
||||
if ref.name != "" {
|
||||
key = ref.name
|
||||
}
|
||||
|
||||
defaultP := named.NewInferredParam(key, c.IsNotNull)
|
||||
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
|
||||
a = append(a, Parameter{
|
||||
Number: ref.ref.Number,
|
||||
Column: &Column{
|
||||
Name: p.Name(),
|
||||
OriginalName: c.Name,
|
||||
DataType: dataType(&c.Type),
|
||||
NotNull: p.NotNull(),
|
||||
Unsigned: c.IsUnsigned,
|
||||
IsArray: c.IsArray,
|
||||
ArrayDims: c.ArrayDims,
|
||||
Length: c.Length,
|
||||
Table: table,
|
||||
IsNamedParam: isNamed,
|
||||
IsSqlcSlice: p.IsSqlcSlice(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if found == 0 {
|
||||
return nil, &sqlerr.Error{
|
||||
Code: "42703",
|
||||
Message: fmt.Sprintf("column %q does not exist", key),
|
||||
Location: node.Location,
|
||||
}
|
||||
}
|
||||
if found > 1 {
|
||||
return nil, &sqlerr.Error{
|
||||
Code: "42703",
|
||||
Message: fmt.Sprintf("column reference %q is ambiguous", key),
|
||||
Location: node.Location,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case *ast.BetweenExpr:
|
||||
if n == nil || n.Expr == nil || n.Left == nil || n.Right == nil {
|
||||
fmt.Println("ast.BetweenExpr is nil")
|
||||
continue
|
||||
}
|
||||
|
||||
var key string
|
||||
if ref, ok := n.Expr.(*ast.ColumnRef); ok {
|
||||
itemsCount := len(ref.Fields.Items)
|
||||
if str, ok := ref.Fields.Items[itemsCount-1].(*ast.String); ok {
|
||||
key = str.Str
|
||||
}
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
schema := table.Schema
|
||||
if schema == "" {
|
||||
schema = c.DefaultSchema
|
||||
}
|
||||
|
||||
if c, ok := typeMap[schema][table.Name][key]; ok {
|
||||
defaultP := named.NewInferredParam(key, c.IsNotNull)
|
||||
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
|
||||
var namePrefix string
|
||||
if !isNamed {
|
||||
if ref.ref == n.Left {
|
||||
namePrefix = "from_"
|
||||
} else if ref.ref == n.Right {
|
||||
namePrefix = "to_"
|
||||
}
|
||||
}
|
||||
|
||||
a = append(a, Parameter{
|
||||
Number: ref.ref.Number,
|
||||
Column: &Column{
|
||||
Name: namePrefix + p.Name(),
|
||||
DataType: dataType(&c.Type),
|
||||
NotNull: p.NotNull(),
|
||||
Unsigned: c.IsUnsigned,
|
||||
IsArray: c.IsArray,
|
||||
ArrayDims: c.ArrayDims,
|
||||
Table: table,
|
||||
IsNamedParam: isNamed,
|
||||
IsSqlcSlice: p.IsSqlcSlice(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
case *ast.FuncCall:
|
||||
fun, err := c.ResolveFuncCall(n)
|
||||
if err != nil {
|
||||
// Synthesize a function on the fly to avoid returning with an error
|
||||
// for an unknown Postgres function (e.g. defined in an extension)
|
||||
var args []*catalog.Argument
|
||||
for range n.Args.Items {
|
||||
args = append(args, &catalog.Argument{
|
||||
Type: &ast.TypeName{Name: "any"},
|
||||
})
|
||||
}
|
||||
fun = &catalog.Function{
|
||||
Name: n.Func.Name,
|
||||
Args: args,
|
||||
ReturnType: &ast.TypeName{Name: "any"},
|
||||
}
|
||||
}
|
||||
|
||||
var added bool
|
||||
for i, item := range n.Args.Items {
|
||||
funcName := fun.Name
|
||||
var argName string
|
||||
switch inode := item.(type) {
|
||||
case *ast.ParamRef:
|
||||
if inode.Number != ref.ref.Number {
|
||||
continue
|
||||
}
|
||||
case *ast.TypeCast:
|
||||
pr, ok := inode.Arg.(*ast.ParamRef)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if pr.Number != ref.ref.Number {
|
||||
continue
|
||||
}
|
||||
case *ast.NamedArgExpr:
|
||||
pr, ok := inode.Arg.(*ast.ParamRef)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if pr.Number != ref.ref.Number {
|
||||
continue
|
||||
}
|
||||
if inode.Name != nil {
|
||||
argName = *inode.Name
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
if fun.Args == nil {
|
||||
defaultName := funcName
|
||||
if argName != "" {
|
||||
defaultName = argName
|
||||
}
|
||||
|
||||
defaultP := named.NewInferredParam(defaultName, false)
|
||||
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
|
||||
added = true
|
||||
a = append(a, Parameter{
|
||||
Number: ref.ref.Number,
|
||||
Column: &Column{
|
||||
Name: p.Name(),
|
||||
DataType: "any",
|
||||
IsNamedParam: isNamed,
|
||||
NotNull: p.NotNull(),
|
||||
IsSqlcSlice: p.IsSqlcSlice(),
|
||||
},
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
var paramName string
|
||||
var paramType *ast.TypeName
|
||||
|
||||
if argName == "" {
|
||||
if i < len(fun.Args) {
|
||||
paramName = fun.Args[i].Name
|
||||
paramType = fun.Args[i].Type
|
||||
}
|
||||
} else {
|
||||
paramName = argName
|
||||
for _, arg := range fun.Args {
|
||||
if arg.Name == argName {
|
||||
paramType = arg.Type
|
||||
}
|
||||
}
|
||||
if paramType == nil {
|
||||
panic(fmt.Sprintf("named argument %s has no type", paramName))
|
||||
}
|
||||
}
|
||||
if paramName == "" {
|
||||
paramName = funcName
|
||||
}
|
||||
if paramType == nil {
|
||||
paramType = &ast.TypeName{Name: ""}
|
||||
}
|
||||
|
||||
defaultP := named.NewInferredParam(paramName, true)
|
||||
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
|
||||
added = true
|
||||
a = append(a, Parameter{
|
||||
Number: ref.ref.Number,
|
||||
Column: &Column{
|
||||
Name: p.Name(),
|
||||
DataType: dataType(paramType),
|
||||
NotNull: p.NotNull(),
|
||||
IsNamedParam: isNamed,
|
||||
IsSqlcSlice: p.IsSqlcSlice(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if fun.ReturnType == nil {
|
||||
if !added {
|
||||
addUnknownParam(ref)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
table, err := c.GetTable(&ast.TableName{
|
||||
Catalog: fun.ReturnType.Catalog,
|
||||
Schema: fun.ReturnType.Schema,
|
||||
Name: fun.ReturnType.Name,
|
||||
})
|
||||
if err != nil {
|
||||
if !added {
|
||||
addUnknownParam(ref)
|
||||
}
|
||||
continue
|
||||
}
|
||||
err = indexTable(table)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case *ast.ResTarget:
|
||||
if n.Name == nil {
|
||||
return nil, fmt.Errorf("*ast.ResTarget has nil name")
|
||||
}
|
||||
key := *n.Name
|
||||
|
||||
var schema, rel string
|
||||
// TODO: Deprecate defaultTable
|
||||
if defaultTable != nil {
|
||||
schema = defaultTable.Schema
|
||||
rel = defaultTable.Name
|
||||
}
|
||||
if ref.rv != nil {
|
||||
fqn, err := ParseTableName(ref.rv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
schema = fqn.Schema
|
||||
rel = fqn.Name
|
||||
}
|
||||
if schema == "" {
|
||||
schema = c.DefaultSchema
|
||||
}
|
||||
|
||||
tableMap, ok := typeMap[schema][rel]
|
||||
if !ok {
|
||||
return nil, sqlerr.RelationNotFound(rel)
|
||||
}
|
||||
|
||||
if c, ok := tableMap[key]; ok {
|
||||
defaultP := named.NewInferredParam(key, c.IsNotNull)
|
||||
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
|
||||
a = append(a, Parameter{
|
||||
Number: ref.ref.Number,
|
||||
Column: &Column{
|
||||
Name: p.Name(),
|
||||
OriginalName: c.Name,
|
||||
DataType: dataType(&c.Type),
|
||||
NotNull: p.NotNull(),
|
||||
Unsigned: c.IsUnsigned,
|
||||
IsArray: c.IsArray,
|
||||
ArrayDims: c.ArrayDims,
|
||||
Table: &ast.TableName{Schema: schema, Name: rel},
|
||||
Length: c.Length,
|
||||
IsNamedParam: isNamed,
|
||||
IsSqlcSlice: p.IsSqlcSlice(),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
return nil, &sqlerr.Error{
|
||||
Code: "42703",
|
||||
Message: fmt.Sprintf("column %q does not exist", key),
|
||||
Location: n.Location,
|
||||
}
|
||||
}
|
||||
|
||||
case *ast.TypeCast:
|
||||
if n.TypeName == nil {
|
||||
return nil, fmt.Errorf("*ast.TypeCast has nil type name")
|
||||
}
|
||||
col := toColumn(n.TypeName)
|
||||
defaultP := named.NewInferredParam(col.Name, col.NotNull)
|
||||
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
|
||||
|
||||
col.Name = p.Name()
|
||||
col.NotNull = p.NotNull()
|
||||
col.IsNamedParam = isNamed
|
||||
a = append(a, Parameter{
|
||||
Number: ref.ref.Number,
|
||||
Column: col,
|
||||
})
|
||||
|
||||
case *ast.ParamRef:
|
||||
a = append(a, Parameter{Number: ref.ref.Number})
|
||||
|
||||
case *ast.In:
|
||||
if n == nil || n.List == nil {
|
||||
fmt.Println("ast.In is nil")
|
||||
continue
|
||||
}
|
||||
|
||||
number := 0
|
||||
if pr, ok := n.List[0].(*ast.ParamRef); ok {
|
||||
number = pr.Number
|
||||
}
|
||||
|
||||
location := 0
|
||||
var key, alias string
|
||||
var items []string
|
||||
|
||||
if left, ok := n.Expr.(*ast.ColumnRef); ok {
|
||||
location = left.Location
|
||||
items = stringSlice(left.Fields)
|
||||
} else if left, ok := n.Expr.(*ast.ParamRef); ok {
|
||||
if len(n.List) <= 0 {
|
||||
continue
|
||||
}
|
||||
if right, ok := n.List[0].(*ast.ColumnRef); ok {
|
||||
location = left.Location
|
||||
items = stringSlice(right.Fields)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
switch len(items) {
|
||||
case 1:
|
||||
key = items[0]
|
||||
case 2:
|
||||
alias = items[0]
|
||||
key = items[1]
|
||||
default:
|
||||
panic("too many field items: " + strconv.Itoa(len(items)))
|
||||
}
|
||||
|
||||
var found int
|
||||
if n.Sel == nil {
|
||||
search := tables
|
||||
if alias != "" {
|
||||
if original, ok := aliasMap[alias]; ok {
|
||||
search = []*ast.TableName{original}
|
||||
} else {
|
||||
for _, fqn := range tables {
|
||||
if fqn.Name == alias {
|
||||
search = []*ast.TableName{fqn}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, table := range search {
|
||||
schema := table.Schema
|
||||
if schema == "" {
|
||||
schema = c.DefaultSchema
|
||||
}
|
||||
if c, ok := typeMap[schema][table.Name][key]; ok {
|
||||
found += 1
|
||||
if ref.name != "" {
|
||||
key = ref.name
|
||||
}
|
||||
defaultP := named.NewInferredParam(key, c.IsNotNull)
|
||||
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
|
||||
a = append(a, Parameter{
|
||||
Number: number,
|
||||
Column: &Column{
|
||||
Name: p.Name(),
|
||||
OriginalName: c.Name,
|
||||
DataType: dataType(&c.Type),
|
||||
NotNull: c.IsNotNull,
|
||||
Unsigned: c.IsUnsigned,
|
||||
IsArray: c.IsArray,
|
||||
ArrayDims: c.ArrayDims,
|
||||
Table: table,
|
||||
IsNamedParam: isNamed,
|
||||
IsSqlcSlice: p.IsSqlcSlice(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found == 0 {
|
||||
return nil, &sqlerr.Error{
|
||||
Code: "42703",
|
||||
Message: fmt.Sprintf("396: column %q does not exist", key),
|
||||
Location: location,
|
||||
}
|
||||
}
|
||||
if found > 1 {
|
||||
return nil, &sqlerr.Error{
|
||||
Code: "42703",
|
||||
Message: fmt.Sprintf("in same name column reference %q is ambiguous", key),
|
||||
Location: location,
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
slog.Debug("unsupported reference type", "type", fmt.Sprintf("%T", n))
|
||||
addUnknownParam(ref)
|
||||
}
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
10
internal/compiler/result.go
Normal file
10
internal/compiler/result.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
Catalog *catalog.Catalog
|
||||
Queries []*Query
|
||||
}
|
||||
138
internal/compiler/returning.go
Normal file
138
internal/compiler/returning.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
)
|
||||
|
||||
// returningTables builds virtual tables for the OLD and NEW aliases that
|
||||
// PostgreSQL 18 makes available in the RETURNING clause of INSERT, UPDATE and
|
||||
// DELETE statements. Each alias exposes the columns of the statement's target
|
||||
// table. For INSERT there is usually no old row and for DELETE there is no
|
||||
// new row, so every column reached through those aliases becomes nullable.
|
||||
func (c *Compiler) returningTables(qc *QueryCatalog, node ast.Node) ([]*Table, error) {
|
||||
if c.conf.Engine != config.EnginePostgreSQL {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var rv *ast.RangeVar
|
||||
var returning *ast.List
|
||||
oldAlias, newAlias := "old", "new"
|
||||
var oldNullable, newNullable bool
|
||||
switch n := node.(type) {
|
||||
case *ast.DeleteStmt:
|
||||
rv = firstRangeVar(n.Relations)
|
||||
returning = n.ReturningList
|
||||
if n.ReturningOldAlias != "" {
|
||||
oldAlias = n.ReturningOldAlias
|
||||
}
|
||||
if n.ReturningNewAlias != "" {
|
||||
newAlias = n.ReturningNewAlias
|
||||
}
|
||||
// A deleted row has no new value
|
||||
newNullable = true
|
||||
case *ast.InsertStmt:
|
||||
rv = n.Relation
|
||||
returning = n.ReturningList
|
||||
if n.ReturningOldAlias != "" {
|
||||
oldAlias = n.ReturningOldAlias
|
||||
}
|
||||
if n.ReturningNewAlias != "" {
|
||||
newAlias = n.ReturningNewAlias
|
||||
}
|
||||
// An inserted row has no old value, except when an ON CONFLICT
|
||||
// clause updates an existing row instead
|
||||
oldNullable = true
|
||||
case *ast.UpdateStmt:
|
||||
rv = firstRangeVar(n.Relations)
|
||||
returning = n.ReturningList
|
||||
if n.ReturningOldAlias != "" {
|
||||
oldAlias = n.ReturningOldAlias
|
||||
}
|
||||
if n.ReturningNewAlias != "" {
|
||||
newAlias = n.ReturningNewAlias
|
||||
}
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
if rv == nil || returning == nil || len(returning.Items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
fqn, err := ParseTableName(rv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
build := func(alias string, nullable bool) *Table {
|
||||
table, err := qc.GetTable(fqn)
|
||||
if err != nil {
|
||||
// An unresolvable target table is reported by the regular
|
||||
// source table lookup, so ignore the error here
|
||||
return nil
|
||||
}
|
||||
table.Rel = &ast.TableName{Name: alias}
|
||||
if nullable {
|
||||
for _, col := range table.Columns {
|
||||
col.NotNull = false
|
||||
}
|
||||
}
|
||||
return table
|
||||
}
|
||||
|
||||
var tables []*Table
|
||||
if t := build(oldAlias, oldNullable); t != nil {
|
||||
tables = append(tables, t)
|
||||
}
|
||||
if t := build(newAlias, newNullable); t != nil {
|
||||
tables = append(tables, t)
|
||||
}
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
func firstRangeVar(list *ast.List) *ast.RangeVar {
|
||||
if list == nil {
|
||||
return nil
|
||||
}
|
||||
for _, item := range list.Items {
|
||||
if rv, ok := item.(*ast.RangeVar); ok && rv != nil {
|
||||
return rv
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// returningTableForScope returns the OLD or NEW virtual table named by scope.
|
||||
// A source table with the same name shadows the virtual table, matching
|
||||
// PostgreSQL, where the implicit aliases are only available when no relation
|
||||
// in the query is already known under that name.
|
||||
func returningTableForScope(tables, rtables []*Table, scope string) *Table {
|
||||
if scope == "" {
|
||||
return nil
|
||||
}
|
||||
for _, t := range tables {
|
||||
if t.Rel.Name == scope {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
for _, t := range rtables {
|
||||
if t.Rel.Name == scope {
|
||||
return t
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tablesForRef resolves a column reference against the source tables,
|
||||
// extended with the OLD or NEW virtual table when the reference is qualified
|
||||
// with one of their names.
|
||||
func tablesForRef(ref *ast.ColumnRef, tables, rtables []*Table) []*Table {
|
||||
parts := stringSlice(ref.Fields)
|
||||
if len(parts) != 2 {
|
||||
return tables
|
||||
}
|
||||
if vt := returningTableForScope(tables, rtables, parts[0]); vt != nil {
|
||||
return append(append([]*Table{}, tables...), vt)
|
||||
}
|
||||
return tables
|
||||
}
|
||||
48
internal/compiler/selector.go
Normal file
48
internal/compiler/selector.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package compiler
|
||||
|
||||
import "strings"
|
||||
|
||||
// selector is an interface used by a compiler for generating expressions for
|
||||
// output columns in a `SELECT ...` or `RETURNING ...` statement.
|
||||
//
|
||||
// This interface is exclusively needed at the moment for SQLite, which must
|
||||
// wrap output `jsonb` columns with a `json(column_name)` invocation so that a
|
||||
// publicly consumable format (i.e. not jsonb) is returned.
|
||||
type selector interface {
|
||||
// ColumnExpr generates output to be used in a `SELECT ...` or `RETURNING
|
||||
// ...` statement based on input column name and metadata.
|
||||
ColumnExpr(name string, column *Column) string
|
||||
}
|
||||
|
||||
// defaultSelector is a selector implementation that does the simpliest possible
|
||||
// pass through when generating column expressions. Its use is suitable for all
|
||||
// database engines not requiring additional customization.
|
||||
type defaultSelector struct{}
|
||||
|
||||
func newDefaultSelector() *defaultSelector {
|
||||
return &defaultSelector{}
|
||||
}
|
||||
|
||||
func (s *defaultSelector) ColumnExpr(name string, column *Column) string {
|
||||
return name
|
||||
}
|
||||
|
||||
type sqliteSelector struct{}
|
||||
|
||||
func newSQLiteSelector() *sqliteSelector {
|
||||
return &sqliteSelector{}
|
||||
}
|
||||
|
||||
func (s *sqliteSelector) ColumnExpr(name string, column *Column) string {
|
||||
// Under SQLite, neither json nor jsonb are real data types, and rather just
|
||||
// of type blob, so database drivers just return whatever raw binary is
|
||||
// stored as values. This is a problem for jsonb, which is considered an
|
||||
// internal format to SQLite and no attempt should be made to parse it
|
||||
// outside of the database itself. For jsonb columns in SQLite, wrap values
|
||||
// in `json(col)` to coerce the internal binary format to JSON parsable by
|
||||
// the user-space application.
|
||||
if strings.EqualFold(column.DataType, "jsonb") {
|
||||
return "json(" + name + ")"
|
||||
}
|
||||
return name
|
||||
}
|
||||
35
internal/compiler/selector_test.go
Normal file
35
internal/compiler/selector_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package compiler
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSelector(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
selectorExpectColumnExpr := func(t *testing.T, selector selector, expected, name string, column *Column) {
|
||||
if actual := selector.ColumnExpr(name, column); expected != actual {
|
||||
t.Errorf("Expected %v, got %v for data type %v", expected, actual, column.DataType)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("DefaultSelectorColumnExpr", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
selector := newDefaultSelector()
|
||||
|
||||
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "integer"})
|
||||
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "json"})
|
||||
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "jsonb"})
|
||||
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "text"})
|
||||
})
|
||||
|
||||
t.Run("SQLiteSelectorColumnExpr", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
selector := newSQLiteSelector()
|
||||
|
||||
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "integer"})
|
||||
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "json"})
|
||||
selectorExpectColumnExpr(t, selector, "json(my_column)", "my_column", &Column{DataType: "jsonb"})
|
||||
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "text"})
|
||||
})
|
||||
}
|
||||
33
internal/compiler/to_column.go
Normal file
33
internal/compiler/to_column.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
|
||||
)
|
||||
|
||||
func arrayDims(n *ast.TypeName) int {
|
||||
if n == nil || n.ArrayBounds == nil {
|
||||
return 0
|
||||
}
|
||||
return len(n.ArrayBounds.Items)
|
||||
}
|
||||
|
||||
func toColumn(n *ast.TypeName) *Column {
|
||||
if n == nil {
|
||||
panic("can't build column for nil type name")
|
||||
}
|
||||
typ, err := ParseTypeName(n)
|
||||
if err != nil {
|
||||
panic("toColumn: " + err.Error())
|
||||
}
|
||||
arrayDims := arrayDims(n)
|
||||
return &Column{
|
||||
Type: typ,
|
||||
DataType: strings.TrimPrefix(astutils.Join(n.Names, "."), "."),
|
||||
NotNull: true, // XXX: How do we know if this should be null?
|
||||
IsArray: arrayDims > 0,
|
||||
ArrayDims: arrayDims,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user