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
202 lines
4.6 KiB
Go
202 lines
4.6 KiB
Go
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
|
|
}
|