Repository: sqlc-dev/sqlc
Pull request #2865 (2023-10-16 20:34:56)

fix(compiler): Support functions with OUT params
Fixes https://github.com/sqlc-dev/sqlc/issues/1654

Don't panic if a function doesn't have a return type. Load the out parameters if they exist.


Patch:
diff --git a/internal/cmd/shim.go b/internal/cmd/shim.go
index 4453d47a3..587c8bff6 100644
--- a/internal/cmd/shim.go
+++ b/internal/cmd/shim.go
@@ -209,13 +209,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,
 		})
 	}
diff --git a/internal/cmd/vet.go b/internal/cmd/vet.go
index 31aa3ec33..4f79fc8e8 100644
--- a/internal/cmd/vet.go
+++ b/internal/cmd/vet.go
@@ -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)
 			}
diff --git a/internal/codegen/golang/gen.go b/internal/codegen/golang/gen.go
index c5f1956e0..ea3d8384c 100644
--- a/internal/codegen/golang/gen.go
+++ b/internal/codegen/golang/gen.go
@@ -345,6 +345,9 @@ func filterUnusedStructs(enums []Enum, structs []Struct, queries []Query) ([]Enu
 			if query.Ret.IsStruct() {
 				for _, field := range query.Ret.Struct.Fields {
 					keepTypes[field.Type] = struct{}{}
+					for _, embedField := range field.EmbedFields {
+						keepTypes[embedField.Type] = struct{}{}
+					}
 				}
 			}
 		}
diff --git a/internal/compiler/compile.go b/internal/compiler/compile.go
index a6744fc6d..9f4a5170e 100644
--- a/internal/compiler/compile.go
+++ b/internal/compiler/compile.go
@@ -8,10 +8,10 @@ import (
 	"path/filepath"
 	"strings"
 
-	"github.com/sqlc-dev/sqlc/internal/metadata"
 	"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/source"
 	"github.com/sqlc-dev/sqlc/internal/sql/ast"
 	"github.com/sqlc-dev/sqlc/internal/sql/sqlerr"
 	"github.com/sqlc-dev/sqlc/internal/sql/sqlpath"
@@ -20,7 +20,7 @@ import (
 // TODO: Rename this interface Engine
 type Parser interface {
 	Parse(io.Reader) ([]ast.Statement, error)
-	CommentSyntax() metadata.CommentSyntax
+	CommentSyntax() source.CommentSyntax
 	IsReservedKeyword(string) bool
 }
 
@@ -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)
 			}
diff --git a/internal/compiler/output_columns.go b/internal/compiler/output_columns.go
index 6ced89ae2..0ee3b3e18 100644
--- a/internal/compiler/output_columns.go
+++ b/internal/compiler/output_columns.go
@@ -550,12 +550,15 @@ func (c *Compiler) sourceTables(qc *QueryCatalog, node ast.Node) ([]*Table, erro
 			if err != nil {
 				continue
 			}
-			table, err := qc.GetTable(&ast.TableName{
-				Catalog: fn.ReturnType.Catalog,
-				Schema:  fn.ReturnType.Schema,
-				Name:    fn.ReturnType.Name,
-			})
-			if err != nil {
+			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 {
@@ -575,12 +578,22 @@ func (c *Compiler) sourceTables(qc *QueryCatalog, node ast.Node) ([]*Table, erro
 							Schema:  fn.Rel.Schema,
 							Name:    fn.Rel.Name,
 						},
-						Columns: []*Column{
+					}
+					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,
 							},
-						},
+						}
 					}
 				}
 			}
diff --git a/internal/compiler/parse.go b/internal/compiler/parse.go
index 53e3043c7..0b626e108 100644
--- a/internal/compiler/parse.go
+++ b/internal/compiler/parse.go
@@ -43,14 +43,31 @@ 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())
+	name, cmd, err := metadata.ParseQueryNameAndType(rawSQL, metadata.CommentSyntax(c.parser.CommentSyntax()))
 	if err != nil {
 		return nil, err
 	}
+
 	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, err = metadata.ParseParamsAndFlags(cleanedComments)
+	if err != nil {
+		return nil, err
+	}
+
 	var anlys *analysis
 	if c.analyzer != nil {
 		// TODO: Handle panics
@@ -90,17 +107,11 @@ func (c *Compiler) parseQuery(stmt ast.Node, src string, o opts.Parser) (*Query,
 		return nil, err
 	}
 
-	flags, err := metadata.ParseQueryFlags(comments)
-	if err != nil {
-		return nil, err
-	}
+	md.Comments = comments
 
 	return &Query{
 		RawStmt:         raw,
-		Cmd:             cmd,
-		Comments:        comments,
-		Name:            name,
-		Flags:           flags,
+		Metadata:        md,
 		Params:          anlys.Parameters,
 		Columns:         anlys.Columns,
 		SQL:             trimmed,
diff --git a/internal/compiler/query.go b/internal/compiler/query.go
index 117cf4481..b3cf9d615 100644
--- a/internal/compiler/query.go
+++ b/internal/compiler/query.go
@@ -1,12 +1,15 @@
 package compiler
 
 import (
+	"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 {
@@ -41,15 +44,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
diff --git a/internal/compiler/query_catalog.go b/internal/compiler/query_catalog.go
index 42088446d..80b59d876 100644
--- a/internal/compiler/query_catalog.go
+++ b/internal/compiler/query_catalog.go
@@ -103,6 +103,7 @@ func (qc QueryCatalog) GetFunc(rel *ast.FuncName) (*Function, error) {
 	}
 	return &Function{
 		Rel:        rel,
+		Outs:       funcs[0].OutArgs(),
 		ReturnType: funcs[0].ReturnType,
 	}, nil
 }
diff --git a/internal/engine/dolphin/parse.go b/internal/engine/dolphin/parse.go
index 676362c44..22d3a1d22 100644
--- a/internal/engine/dolphin/parse.go
+++ b/internal/engine/dolphin/parse.go
@@ -10,7 +10,7 @@ import (
 	"github.com/pingcap/tidb/parser"
 	_ "github.com/pingcap/tidb/parser/test_driver"
 
-	"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/sqlerr"
 )
@@ -86,8 +86,8 @@ func (p *Parser) Parse(r io.Reader) ([]ast.Statement, error) {
 }
 
 // https://dev.mysql.com/doc/refman/8.0/en/comments.html
-func (p *Parser) CommentSyntax() metadata.CommentSyntax {
-	return metadata.CommentSyntax{
+func (p *Parser) CommentSyntax() source.CommentSyntax {
+	return source.CommentSyntax{
 		Dash:      true,
 		SlashStar: true,
 		Hash:      true,
diff --git a/internal/engine/postgresql/parse.go b/internal/engine/postgresql/parse.go
index 49ec98148..296a14e85 100644
--- a/internal/engine/postgresql/parse.go
+++ b/internal/engine/postgresql/parse.go
@@ -12,7 +12,7 @@ import (
 	nodes "github.com/pganalyze/pg_query_go/v4"
 	"github.com/pganalyze/pg_query_go/v4/parser"
 
-	"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/sqlerr"
 )
@@ -199,8 +199,8 @@ func normalizeErr(err error) error {
 }
 
 // https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-COMMENTS
-func (p *Parser) CommentSyntax() metadata.CommentSyntax {
-	return metadata.CommentSyntax{
+func (p *Parser) CommentSyntax() source.CommentSyntax {
+	return source.CommentSyntax{
 		Dash:      true,
 		SlashStar: true,
 	}
diff --git a/internal/engine/postgresql/parse_disabled.go b/internal/engine/postgresql/parse_disabled.go
index ecc5158d4..b07817b43 100644
--- a/internal/engine/postgresql/parse_disabled.go
+++ b/internal/engine/postgresql/parse_disabled.go
@@ -8,7 +8,7 @@ import (
 	"io"
 	"runtime"
 
-	"github.com/sqlc-dev/sqlc/internal/metadata"
+	"github.com/sqlc-dev/sqlc/internal/source"
 	"github.com/sqlc-dev/sqlc/internal/sql/ast"
 )
 
@@ -27,8 +27,8 @@ func (p *Parser) Parse(r io.Reader) ([]ast.Statement, error) {
 }
 
 // https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-COMMENTS
-func (p *Parser) CommentSyntax() metadata.CommentSyntax {
-	return metadata.CommentSyntax{
+func (p *Parser) CommentSyntax() source.CommentSyntax {
+	return source.CommentSyntax{
 		Dash:      true,
 		SlashStar: true,
 	}
diff --git a/internal/engine/sqlite/parse.go b/internal/engine/sqlite/parse.go
index bf0bacad9..56005dd2e 100644
--- a/internal/engine/sqlite/parse.go
+++ b/internal/engine/sqlite/parse.go
@@ -8,7 +8,7 @@ import (
 	"github.com/antlr/antlr4/runtime/Go/antlr/v4"
 
 	"github.com/sqlc-dev/sqlc/internal/engine/sqlite/parser"
-	"github.com/sqlc-dev/sqlc/internal/metadata"
+	"github.com/sqlc-dev/sqlc/internal/source"
 	"github.com/sqlc-dev/sqlc/internal/sql/ast"
 )
 
@@ -86,8 +86,8 @@ func (p *Parser) Parse(r io.Reader) ([]ast.Statement, error) {
 	return stmts, nil
 }
 
-func (p *Parser) CommentSyntax() metadata.CommentSyntax {
-	return metadata.CommentSyntax{
+func (p *Parser) CommentSyntax() source.CommentSyntax {
+	return source.CommentSyntax{
 		Dash:      true,
 		Hash:      false,
 		SlashStar: true,
diff --git a/internal/metadata/meta.go b/internal/metadata/meta.go
index 4176da1e2..97ff36dbd 100644
--- a/internal/metadata/meta.go
+++ b/internal/metadata/meta.go
@@ -1,15 +1,24 @@
 package metadata
 
 import (
+	"bufio"
 	"fmt"
 	"strings"
 	"unicode"
+
+	"github.com/sqlc-dev/sqlc/internal/source"
 )
 
-type CommentSyntax struct {
-	Dash      bool
-	Hash      bool
-	SlashStar bool
+type CommentSyntax source.CommentSyntax
+
+type Metadata struct {
+	Name     string
+	Cmd      string
+	Comments []string
+	Params   map[string]string
+	Flags    map[string]bool
+
+	Filename string
 }
 
 const (
@@ -83,7 +92,7 @@ func ParseQueryNameAndType(t string, commentStyle CommentSyntax) (string, string
 		if prefix == "/*" {
 			part = part[:len(part)-1] // removes the trailing "*/" element
 		}
-		if len(part) == 2 {
+		if len(part) == 3 {
 			return "", "", fmt.Errorf("missing query type [':one', ':many', ':exec', ':execrows', ':execlastid', ':execresult', ':copyfrom', 'batchexec', 'batchmany', 'batchone']: %s", line)
 		}
 		if len(part) != 4 {
@@ -104,19 +113,39 @@ func ParseQueryNameAndType(t string, commentStyle CommentSyntax) (string, string
 	return "", "", nil
 }
 
-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()
+				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
 }
diff --git a/internal/source/code.go b/internal/source/code.go
index f34e3e368..8b88a2413 100644
--- a/internal/source/code.go
+++ b/internal/source/code.go
@@ -15,6 +15,12 @@ type Edit struct {
 	OldFunc  func(string) int
 }
 
+type CommentSyntax struct {
+	Dash      bool
+	Hash      bool
+	SlashStar bool
+}
+
 func LineNumber(source string, head int) (int, int) {
 	// Calculate the true line and column number for a query, ignoring spaces
 	var comment bool
@@ -101,6 +107,9 @@ func StripComments(sql string) (string, []string, error) {
 		if strings.HasPrefix(t, "/* name:") && strings.HasSuffix(t, "*/") {
 			continue
 		}
+		if strings.HasPrefix(t, "# name:") {
+			continue
+		}
 		if strings.HasPrefix(t, "--") {
 			comments = append(comments, strings.TrimPrefix(t, "--"))
 			continue
@@ -111,7 +120,46 @@ func StripComments(sql string) (string, []string, error) {
 			comments = append(comments, t)
 			continue
 		}
+		if strings.HasPrefix(t, "#") {
+			comments = append(comments, strings.TrimPrefix(t, "#"))
+			continue
+		}
 		lines = append(lines, t)
 	}
 	return strings.Join(lines, "\n"), comments, s.Err()
 }
+
+func CleanedComments(rawSQL string, cs CommentSyntax) ([]string, error) {
+	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 !cs.Dash {
+				continue
+			}
+			prefix = "--"
+		}
+		if strings.HasPrefix(line, "/*") {
+			if !cs.SlashStar {
+				continue
+			}
+			prefix = "/*"
+		}
+		if strings.HasPrefix(line, "#") {
+			if !cs.Hash {
+				continue
+			}
+			prefix = "#"
+		}
+		if prefix == "" {
+			continue
+		}
+
+		rest := line[len(prefix):]
+		rest = strings.TrimSuffix(rest, "*/")
+		comments = append(comments, rest)
+	}
+	return comments, s.Err()
+}
diff --git a/internal/sql/catalog/func.go b/internal/sql/catalog/func.go
index 7cc712492..e17077731 100644
--- a/internal/sql/catalog/func.go
+++ b/internal/sql/catalog/func.go
@@ -39,6 +39,17 @@ func (f *Function) InArgs() []*Argument {
 	return args
 }
 
+func (f *Function) OutArgs() []*Argument {
+	var args []*Argument
+	for _, a := range f.Args {
+		switch a.Mode {
+		case ast.FuncParamOut:
+			args = append(args, a)
+		}
+	}
+	return args
+}
+
 func (c *Catalog) createFunction(stmt *ast.CreateFunctionStmt) error {
 	ns := stmt.Func.Schema
 	if ns == "" {


Test patch:
diff --git a/internal/endtoend/testdata/comment_syntax/mysql/go/query.sql.go b/internal/endtoend/testdata/comment_syntax/mysql/go/query.sql.go
index 7f4b91615..29909a8da 100644
--- a/internal/endtoend/testdata/comment_syntax/mysql/go/query.sql.go
+++ b/internal/endtoend/testdata/comment_syntax/mysql/go/query.sql.go
@@ -22,7 +22,6 @@ func (q *Queries) DoubleDash(ctx context.Context) (sql.NullString, error) {
 }
 
 const hash = `-- name: Hash :one
-# name: Hash :one
 SELECT bar FROM foo LIMIT 1
 `
 
diff --git a/internal/endtoend/testdata/func_out_param/issue.md b/internal/endtoend/testdata/func_out_param/issue.md
new file mode 100644
index 000000000..1b8779f2f
--- /dev/null
+++ b/internal/endtoend/testdata/func_out_param/issue.md
@@ -0,0 +1,1 @@
+https://github.com/sqlc-dev/sqlc/issues/1654
diff --git a/internal/endtoend/testdata/func_out_param/pgx/go/db.go b/internal/endtoend/testdata/func_out_param/pgx/go/db.go
new file mode 100644
index 000000000..8a010ccc4
--- /dev/null
+++ b/internal/endtoend/testdata/func_out_param/pgx/go/db.go
@@ -0,0 +1,32 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.22.0
+
+package querytest
+
+import (
+	"context"
+
+	"github.com/jackc/pgx/v5"
+	"github.com/jackc/pgx/v5/pgconn"
+)
+
+type DBTX interface {
+	Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
+	Query(context.Context, string, ...interface{}) (pgx.Rows, error)
+	QueryRow(context.Context, string, ...interface{}) pgx.Row
+}
+
+func New(db DBTX) *Queries {
+	return &Queries{db: db}
+}
+
+type Queries struct {
+	db DBTX
+}
+
+func (q *Queries) WithTx(tx pgx.Tx) *Queries {
+	return &Queries{
+		db: tx,
+	}
+}
diff --git a/internal/endtoend/testdata/func_out_param/pgx/go/models.go b/internal/endtoend/testdata/func_out_param/pgx/go/models.go
new file mode 100644
index 000000000..b320134bd
--- /dev/null
+++ b/internal/endtoend/testdata/func_out_param/pgx/go/models.go
@@ -0,0 +1,15 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.22.0
+
+package querytest
+
+import (
+	"github.com/jackc/pgx/v5/pgtype"
+)
+
+type Author struct {
+	ID   int64
+	Name string
+	Bio  pgtype.Text
+}
diff --git a/internal/endtoend/testdata/func_out_param/pgx/go/query.sql.go b/internal/endtoend/testdata/func_out_param/pgx/go/query.sql.go
new file mode 100644
index 000000000..b19b4a070
--- /dev/null
+++ b/internal/endtoend/testdata/func_out_param/pgx/go/query.sql.go
@@ -0,0 +1,30 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.22.0
+// source: query.sql
+
+package querytest
+
+import (
+	"context"
+
+	"github.com/jackc/pgx/v5/pgtype"
+)
+
+const createAuthor = `-- name: CreateAuthor :one
+SELECT id FROM add_author (
+  $1, $2
+)
+`
+
+type CreateAuthorParams struct {
+	Name string
+	Bio  string
+}
+
+func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (pgtype.Int4, error) {
+	row := q.db.QueryRow(ctx, createAuthor, arg.Name, arg.Bio)
+	var id pgtype.Int4
+	err := row.Scan(&id)
+	return id, err
+}
diff --git a/internal/endtoend/testdata/func_out_param/pgx/query.sql b/internal/endtoend/testdata/func_out_param/pgx/query.sql
new file mode 100644
index 000000000..b23c7dca4
--- /dev/null
+++ b/internal/endtoend/testdata/func_out_param/pgx/query.sql
@@ -0,0 +1,4 @@
+-- name: CreateAuthor :one
+SELECT * FROM add_author (
+  sqlc.arg(name), sqlc.arg(bio)
+);
diff --git a/internal/endtoend/testdata/func_out_param/pgx/schema.sql b/internal/endtoend/testdata/func_out_param/pgx/schema.sql
new file mode 100644
index 000000000..97c1022d1
--- /dev/null
+++ b/internal/endtoend/testdata/func_out_param/pgx/schema.sql
@@ -0,0 +1,14 @@
+-- Example queries for sqlc
+CREATE TABLE authors (
+  id   BIGSERIAL PRIMARY KEY,
+  name text      NOT NULL,
+  bio  text
+);
+
+CREATE OR REPLACE FUNCTION add_author (name text, bio text, out id int)
+AS $$
+DECLARE
+BEGIN
+  id = 123;
+END;
+$$ LANGUAGE plpgsql;
diff --git a/internal/endtoend/testdata/func_out_param/pgx/sqlc.yaml b/internal/endtoend/testdata/func_out_param/pgx/sqlc.yaml
new file mode 100644
index 000000000..5dc63e3f9
--- /dev/null
+++ b/internal/endtoend/testdata/func_out_param/pgx/sqlc.yaml
@@ -0,0 +1,10 @@
+version: "2"
+sql:
+  - engine: "postgresql"
+    schema: "schema.sql"
+    queries: "query.sql"
+    gen:
+      go:
+        package: "querytest"
+        out: "go"
+        sql_package: "pgx/v5"
diff --git a/internal/endtoend/testdata/invalid_queries_foo/pgx/v4/stderr.txt b/internal/endtoend/testdata/invalid_queries_foo/pgx/v4/stderr.txt
index 6b0840fc3..06ec54327 100644
--- a/internal/endtoend/testdata/invalid_queries_foo/pgx/v4/stderr.txt
+++ b/internal/endtoend/testdata/invalid_queries_foo/pgx/v4/stderr.txt
@@ -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
diff --git a/internal/endtoend/testdata/invalid_queries_foo/pgx/v5/stderr.txt b/internal/endtoend/testdata/invalid_queries_foo/pgx/v5/stderr.txt
index 6b0840fc3..06ec54327 100644
--- a/internal/endtoend/testdata/invalid_queries_foo/pgx/v5/stderr.txt
+++ b/internal/endtoend/testdata/invalid_queries_foo/pgx/v5/stderr.txt
@@ -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
diff --git a/internal/endtoend/testdata/invalid_queries_foo/stdlib/stderr.txt b/internal/endtoend/testdata/invalid_queries_foo/stdlib/stderr.txt
index 6b0840fc3..06ec54327 100644
--- a/internal/endtoend/testdata/invalid_queries_foo/stdlib/stderr.txt
+++ b/internal/endtoend/testdata/invalid_queries_foo/stdlib/stderr.txt
@@ -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
diff --git a/internal/endtoend/testdata/omit_unused_structs/postgresql/stdlib/go/models.go b/internal/endtoend/testdata/omit_unused_structs/postgresql/stdlib/go/models.go
index b1624997b..d918dc44c 100644
--- a/internal/endtoend/testdata/omit_unused_structs/postgresql/stdlib/go/models.go
+++ b/internal/endtoend/testdata/omit_unused_structs/postgresql/stdlib/go/models.go
@@ -219,6 +219,48 @@ func (ns NullQueryReturnStructEnumTableEnum) Value() (driver.Value, error) {
 	return string(ns.QueryReturnStructEnumTableEnum), nil
 }
 
+type QuerySqlcEmbedEnum string
+
+const (
+	QuerySqlcEmbedEnumM QuerySqlcEmbedEnum = "m"
+	QuerySqlcEmbedEnumN QuerySqlcEmbedEnum = "n"
+)
+
+func (e *QuerySqlcEmbedEnum) Scan(src interface{}) error {
+	switch s := src.(type) {
+	case []byte:
+		*e = QuerySqlcEmbedEnum(s)
+	case string:
+		*e = QuerySqlcEmbedEnum(s)
+	default:
+		return fmt.Errorf("unsupported scan type for QuerySqlcEmbedEnum: %T", src)
+	}
+	return nil
+}
+
+type NullQuerySqlcEmbedEnum struct {
+	QuerySqlcEmbedEnum QuerySqlcEmbedEnum
+	Valid              bool // Valid is true if QuerySqlcEmbedEnum is not NULL
+}
+
+// Scan implements the Scanner interface.
+func (ns *NullQuerySqlcEmbedEnum) Scan(value interface{}) error {
+	if value == nil {
+		ns.QuerySqlcEmbedEnum, ns.Valid = "", false
+		return nil
+	}
+	ns.Valid = true
+	return ns.QuerySqlcEmbedEnum.Scan(value)
+}
+
+// Value implements the driver Valuer interface.
+func (ns NullQuerySqlcEmbedEnum) Value() (driver.Value, error) {
+	if !ns.Valid {
+		return nil, nil
+	}
+	return string(ns.QuerySqlcEmbedEnum), nil
+}
+
 type QueryParamEnumTable struct {
 	ID    int32
 	Other QueryParamEnumTableEnum
@@ -229,3 +271,8 @@ type QueryReturnFullTable struct {
 	ID    int32
 	Value NullQueryReturnFullTableEnum
 }
+
+type QuerySqlcEmbedTable struct {
+	ID    int32
+	Value NullQuerySqlcEmbedEnum
+}
diff --git a/internal/endtoend/testdata/omit_unused_structs/postgresql/stdlib/go/query.sql.go b/internal/endtoend/testdata/omit_unused_structs/postgresql/stdlib/go/query.sql.go
index f65910269..91457e09a 100644
--- a/internal/endtoend/testdata/omit_unused_structs/postgresql/stdlib/go/query.sql.go
+++ b/internal/endtoend/testdata/omit_unused_structs/postgresql/stdlib/go/query.sql.go
@@ -90,3 +90,18 @@ func (q *Queries) query_return_struct_enum_table(ctx context.Context, id int32)
 	err := row.Scan(&i.Value, &i.Another)
 	return i, err
 }
+
+const query_sqlc_embed_table = `-- name: query_sqlc_embed_table :one
+SELECT query_sqlc_embed_table.id, query_sqlc_embed_table.value FROM query_sqlc_embed_table WHERE id = $1
+`
+
+type query_sqlc_embed_tableRow struct {
+	QuerySqlcEmbedTable QuerySqlcEmbedTable
+}
+
+func (q *Queries) query_sqlc_embed_table(ctx context.Context, id int32) (query_sqlc_embed_tableRow, error) {
+	row := q.db.QueryRowContext(ctx, query_sqlc_embed_table, id)
+	var i query_sqlc_embed_tableRow
+	err := row.Scan(&i.QuerySqlcEmbedTable.ID, &i.QuerySqlcEmbedTable.Value)
+	return i, err
+}
diff --git a/internal/endtoend/testdata/omit_unused_structs/postgresql/stdlib/query.sql b/internal/endtoend/testdata/omit_unused_structs/postgresql/stdlib/query.sql
index 1420d869d..599a22748 100644
--- a/internal/endtoend/testdata/omit_unused_structs/postgresql/stdlib/query.sql
+++ b/internal/endtoend/testdata/omit_unused_structs/postgresql/stdlib/query.sql
@@ -11,4 +11,7 @@ SELECT id FROM query_param_struct_enum_table WHERE id = $1 AND value = $2;
 SELECT value FROM query_return_enum_table WHERE id = $1;
 
 -- name: query_return_struct_enum_table :one
-SELECT value, another FROM query_return_struct_enum_table WHERE id = $1;
\ No newline at end of file
+SELECT value, another FROM query_return_struct_enum_table WHERE id = $1;
+
+-- name: query_sqlc_embed_table :one
+SELECT sqlc.embed(query_sqlc_embed_table) FROM query_sqlc_embed_table WHERE id = $1;
diff --git a/internal/endtoend/testdata/omit_unused_structs/postgresql/stdlib/schema.sql b/internal/endtoend/testdata/omit_unused_structs/postgresql/stdlib/schema.sql
index cc2ae14b2..486119c33 100644
--- a/internal/endtoend/testdata/omit_unused_structs/postgresql/stdlib/schema.sql
+++ b/internal/endtoend/testdata/omit_unused_structs/postgresql/stdlib/schema.sql
@@ -50,4 +50,12 @@ CREATE TABLE query_return_struct_enum_table (
     id      INTEGER PRIMARY KEY,
     value   query_return_struct_enum_table_enum,
     another INTEGER
-);
\ No newline at end of file
+);
+
+CREATE TYPE query_sqlc_embed_enum AS ENUM (
+    'm', 'n'
+);
+CREATE TABLE query_sqlc_embed_table (
+    id      INTEGER PRIMARY KEY,
+    value   query_sqlc_embed_enum
+)
diff --git a/internal/metadata/meta_test.go b/internal/metadata/meta_test.go
index cbfcb6fba..3c2be6d6d 100644
--- a/internal/metadata/meta_test.go
+++ b/internal/metadata/meta_test.go
@@ -32,34 +32,108 @@ func TestParseQueryNameAndType(t *testing.T) {
 		}
 	}
 
-	query := `-- name: CreateFoo :one`
-	queryName, queryType, err := ParseQueryNameAndType(query, CommentSyntax{Dash: true})
-	if err != nil {
-		t.Errorf("expected valid metadata: %q", query)
-	}
-	if queryName != "CreateFoo" {
-		t.Errorf("incorrect queryName parsed: %q", query)
-	}
-	if queryType != CmdOne {
-		t.Errorf("incorrect queryType parsed: %q", query)
+	for query, cs := range map[string]CommentSyntax{
+		`-- name: CreateFoo :one`:    {Dash: true},
+		`# name: CreateFoo :one`:     {Hash: true},
+		`/* name: CreateFoo :one */`: {SlashStar: true},
+	} {
+		queryName, queryCmd, err := ParseQueryNameAndType(query, cs)
+		if err != nil {
+			t.Errorf("expected valid metadata: %q", query)
+		}
+		if queryName != "CreateFoo" {
+			t.Errorf("incorrect queryName parsed: (%q) %q", queryName, query)
+		}
+		if queryCmd != CmdOne {
+			t.Errorf("incorrect queryCmd parsed: (%q) %q", queryCmd, query)
+		}
 	}
 
 }
 
+func TestParseQueryParams(t *testing.T) {
+	for _, comments := range [][]string{
+		{
+			" name: CreateFoo :one",
+			" @param foo_id UUID",
+		},
+		{
+			" name: CreateFoo :one ",
+			" @param foo_id UUID ",
+		},
+		{
+			" name: CreateFoo :one",
+			"@param foo_id UUID",
+			" invalid",
+		},
+		{
+			" name: CreateFoo :one",
+			" @invalid",
+			" @param foo_id UUID",
+		},
+		{
+			" name: GetFoos :many ",
+			" @param foo_id UUID ",
+			" @param @invalid UUID ",
+		},
+	} {
+		params, _, err := ParseParamsAndFlags(comments)
+		if err != nil {
+			t.Errorf("expected comments to parse, got err: %s", err)
+		}
+
+		pt, ok := params["foo_id"]
+		if !ok {
+			t.Errorf("expected param not found")
+		}
+
+		if pt != "UUID" {
+			t.Error("unexpected param metadata:", pt)
+		}
+
+		_, ok = params["invalid"]
+		if ok {
+			t.Errorf("unexpected param found")
+		}
+	}
+}
+
 func TestParseQueryFlags(t *testing.T) {
 	for _, comments := range [][]string{
 		{
-			"-- name: CreateFoo :one",
-			"-- @flag-foo",
+			" name: CreateFoo :one",
+			" @flag-foo",
+		},
+		{
+			" name: CreateFoo :one ",
+			"@flag-foo ",
+		},
+		{
+			" name: CreateFoo :one",
+			" @flag-foo @flag-bar",
+		},
+		{
+			" name: GetFoos :many",
+			" @param @flag-bar UUID",
+			" @flag-foo",
+		},
+		{
+			" name: GetFoos :many",
+			" @flag-foo",
+			" @param @flag-bar UUID",
 		},
 	} {
-		flags, err := ParseQueryFlags(comments)
+		_, flags, err := ParseParamsAndFlags(comments)
 		if err != nil {
-			t.Errorf("expected query flags to parse, got error: %s", err)
+			t.Errorf("expected comments to parse, got err: %s", err)
 		}
 
 		if !flags["@flag-foo"] {
 			t.Errorf("expected flag not found")
 		}
+
+		if flags["@flag-bar"] {
+			t.Errorf("unexpected flag found")
+		}
 	}
-}
\ No newline at end of file
+}