Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(compiler): Parse query parameter metadata from comments #2850

Merged
merged 12 commits into from
Oct 16, 2023
Merged
8 changes: 4 additions & 4 deletions internal/cmd/shim.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,13 @@ func pluginQueries(r *compiler.Result) []*plugin.Query {
}
}
out = append(out, &plugin.Query{
Name: q.Name,
Cmd: q.Cmd,
Name: q.Metadata.Name,
Cmd: q.Metadata.Cmd,
Text: q.SQL,
Comments: q.Comments,
Comments: q.Metadata.Comments,
Columns: columns,
Params: params,
Filename: q.Filename,
Filename: q.Metadata.Filename,
InsertIntoTable: iit,
})
}
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/vet.go
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ func (c *checker) checkSQL(ctx context.Context, s config.SQL) error {
req := codeGenRequest(result, combo)
cfg := vetConfig(req)
for i, query := range req.Queries {
if result.Queries[i].Flags[QueryFlagSqlcVetDisable] {
if result.Queries[i].Metadata.Flags[QueryFlagSqlcVetDisable] {
if debug.Active {
log.Printf("Skipping vet rules for query: %s\n", query.Name)
}
Expand Down
11 changes: 6 additions & 5 deletions internal/compiler/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,15 @@ func (c *Compiler) parseQueries(o opts.Parser) (*Result, error) {
merr.Add(filename, src, loc, err)
continue
}
if query.Name != "" {
if _, exists := set[query.Name]; exists {
merr.Add(filename, src, stmt.Raw.Pos(), fmt.Errorf("duplicate query name: %s", query.Name))
queryName := query.Metadata.Name
if queryName != "" {
if _, exists := set[queryName]; exists {
merr.Add(filename, src, stmt.Raw.Pos(), fmt.Errorf("duplicate query name: %s", queryName))
continue
}
set[query.Name] = struct{}{}
set[queryName] = struct{}{}
}
query.Filename = filepath.Base(filename)
query.Metadata.Filename = filepath.Base(filename)
if query != nil {
q = append(q, query)
}
Expand Down
17 changes: 5 additions & 12 deletions internal/compiler/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,12 @@ func (c *Compiler) parseQuery(stmt ast.Node, src string, o opts.Parser) (*Query,
return nil, errors.New("missing semicolon at end of file")
}

name, cmd, err := metadata.ParseQueryNameAndType(strings.TrimSpace(rawSQL), c.parser.CommentSyntax())
md, err := metadata.ParseQueryMetadata(rawSQL, c.parser.CommentSyntax())
if err != nil {
return nil, err
}
if err := validate.Cmd(raw.Stmt, name, cmd); err != nil {

if err := validate.Cmd(raw.Stmt, md.Name, md.Cmd); err != nil {
return nil, err
}

Expand Down Expand Up @@ -85,22 +86,14 @@ func (c *Compiler) parseQuery(stmt ast.Node, src string, o opts.Parser) (*Query,
}
}

trimmed, comments, err := source.StripComments(expanded)
if err != nil {
return nil, err
}

flags, err := metadata.ParseQueryFlags(comments)
trimmed, err := source.StripComments(expanded)
if err != nil {
return nil, err
}

return &Query{
RawStmt: raw,
Cmd: cmd,
Comments: comments,
Name: name,
Flags: flags,
Metadata: md,
Params: anlys.Parameters,
Columns: anlys.Columns,
SQL: trimmed,
Expand Down
9 changes: 2 additions & 7 deletions internal/compiler/query.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package compiler

import (
"github.com/sqlc-dev/sqlc/internal/metadata"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
)

Expand Down Expand Up @@ -41,15 +42,9 @@ type Column struct {

type Query struct {
SQL string
Name string
Cmd string // TODO: Pick a better name. One of: one, many, exec, execrows, copyFrom
Flags map[string]bool
Metadata metadata.Metadata
Columns []*Column
Params []Parameter
Comments []string

// XXX: Hack
Filename string

// Needed for CopyFrom
InsertIntoTable *ast.TableName
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# package querytest
query.sql:1:1: invalid query comment: -- name: ListFoos
query.sql:1:1: missing query type [':one', ':many', ':exec', ':execrows', ':execlastid', ':execresult', ':copyfrom', 'batchexec', 'batchmany', 'batchone']: -- name: ListFoos
query.sql:5:1: invalid query comment: -- name: ListFoos :one :many
query.sql:8:1: invalid query type: :two
query.sql:11:1: query "DeleteFoo" specifies parameter ":one" without containing a RETURNING clause
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# package querytest
query.sql:1:1: invalid query comment: -- name: ListFoos
query.sql:1:1: missing query type [':one', ':many', ':exec', ':execrows', ':execlastid', ':execresult', ':copyfrom', 'batchexec', 'batchmany', 'batchone']: -- name: ListFoos
query.sql:5:1: invalid query comment: -- name: ListFoos :one :many
query.sql:8:1: invalid query type: :two
query.sql:11:1: query "DeleteFoo" specifies parameter ":one" without containing a RETURNING clause
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# package querytest
query.sql:1:1: invalid query comment: -- name: ListFoos
query.sql:1:1: missing query type [':one', ':many', ':exec', ':execrows', ':execlastid', ':execresult', ':copyfrom', 'batchexec', 'batchmany', 'batchone']: -- name: ListFoos
query.sql:5:1: invalid query comment: -- name: ListFoos :one :many
query.sql:8:1: invalid query type: :two
query.sql:11:1: query "DeleteFoo" specifies parameter ":one" without containing a RETURNING clause
Expand Down
106 changes: 79 additions & 27 deletions internal/metadata/meta.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
package metadata

import (
"bufio"
"fmt"
"strings"
"unicode"
)

type Metadata struct {
Name string
Cmd string
Comments []string
Params map[string]string
Flags map[string]bool

Filename string
}

type CommentSyntax struct {
Dash bool
Hash bool
Expand Down Expand Up @@ -44,8 +55,12 @@ func validateQueryName(name string) error {
return nil
}

func ParseQueryNameAndType(t string, commentStyle CommentSyntax) (string, string, error) {
for _, line := range strings.Split(t, "\n") {
func ParseQueryMetadata(rawSql string, commentStyle CommentSyntax) (Metadata, error) {
md := Metadata{}
s := bufio.NewScanner(strings.NewReader(strings.TrimSpace(rawSql)))
var comments []string
for s.Scan() {
line := s.Text()
var prefix string
if strings.HasPrefix(line, "--") {
if !commentStyle.Dash {
Expand All @@ -68,55 +83,92 @@ func ParseQueryNameAndType(t string, commentStyle CommentSyntax) (string, string
if prefix == "" {
continue
}

rest := line[len(prefix):]
rest = strings.TrimSuffix(rest, "*/")
comments = append(comments, rest)

if !strings.HasPrefix(strings.TrimSpace(rest), "name") {
continue
}
if !strings.Contains(rest, ":") {
continue
}
if !strings.HasPrefix(rest, " name: ") {
return "", "", fmt.Errorf("invalid metadata: %s", line)
return md, fmt.Errorf("invalid metadata: %s", line)
}

part := strings.Split(strings.TrimSpace(line), " ")
if prefix == "/*" {
part = part[:len(part)-1] // removes the trailing "*/" element
}
if len(part) == 2 {
return "", "", fmt.Errorf("missing query type [':one', ':many', ':exec', ':execrows', ':execlastid', ':execresult', ':copyfrom', 'batchexec', 'batchmany', 'batchone']: %s", line)
comments = comments[:len(comments)-1] // Remove tha name line from returned comments

parts := strings.Split(strings.TrimSpace(rest), " ")

if len(parts) == 2 {
return md, fmt.Errorf("missing query type [':one', ':many', ':exec', ':execrows', ':execlastid', ':execresult', ':copyfrom', 'batchexec', 'batchmany', 'batchone']: %s", line)
}
if len(part) != 4 {
return "", "", fmt.Errorf("invalid query comment: %s", line)
if len(parts) > 3 {
return md, fmt.Errorf("invalid query comment: %s", line)
}
queryName := part[2]
queryType := strings.TrimSpace(part[3])
queryName := parts[1]
queryType := parts[2]
switch queryType {
case CmdOne, CmdMany, CmdExec, CmdExecResult, CmdExecRows, CmdExecLastId, CmdCopyFrom, CmdBatchExec, CmdBatchMany, CmdBatchOne:
default:
return "", "", fmt.Errorf("invalid query type: %s", queryType)
return md, fmt.Errorf("invalid query type: %s", queryType)
}
if err := validateQueryName(queryName); err != nil {
return "", "", err
return md, err
}
return queryName, queryType, nil
md.Name = queryName
md.Cmd = queryType
}

md.Comments = comments

var err error
md.Params, md.Flags, err = parseParamsAndFlags(md.Comments)
if err != nil {
return md, err
}
return "", "", nil

return md, s.Err()
}

func ParseQueryFlags(comments []string) (map[string]bool, error) {
func parseParamsAndFlags(comments []string) (map[string]string, map[string]bool, error) {
params := make(map[string]string)
flags := make(map[string]bool)

for _, line := range comments {
cleanLine := strings.TrimPrefix(line, "--")
cleanLine = strings.TrimPrefix(cleanLine, "/*")
cleanLine = strings.TrimPrefix(cleanLine, "#")
cleanLine = strings.TrimSuffix(cleanLine, "*/")
cleanLine = strings.TrimSpace(cleanLine)
if strings.HasPrefix(cleanLine, "@") {
flagName := strings.SplitN(cleanLine, " ", 2)[0]
flags[flagName] = true
s := bufio.NewScanner(strings.NewReader(line))
s.Split(bufio.ScanWords)

s.Scan()
token := s.Text()

if !strings.HasPrefix(token, "@") {
continue
}

switch token {
case "@param":
s.Scan()
name := s.Text()
var rest []string
for s.Scan() {
paramToken := s.Text()
if paramToken == "*/" {
break
}
rest = append(rest, paramToken)
}
params[name] = strings.Join(rest, " ")
default:
flags[token] = true
}

if s.Err() != nil {
return params, flags, s.Err()
}
}
return flags, nil

return params, flags, nil
}
Loading