diff --git a/docs/howto/row_types.md b/docs/howto/row_types.md new file mode 100644 index 0000000000..280d73e319 --- /dev/null +++ b/docs/howto/row_types.md @@ -0,0 +1,100 @@ +# Sharing a row type between queries + +sqlc generates a struct for the rows of each query that returns more than one +column, named after the query. Queries that select the same columns therefore +return different types, even though the structs are identical. + +```sql +CREATE TABLE authors ( + id BIGSERIAL PRIMARY KEY, + name text NOT NULL +); + +CREATE TABLE books ( + id BIGSERIAL PRIMARY KEY, + author_id bigint NOT NULL REFERENCES authors (id), + title text NOT NULL +); +``` + +```sql +-- name: GetBook :one +SELECT books.id, books.title, authors.name AS author_name +FROM books +JOIN authors ON authors.id = books.author_id +WHERE books.id = $1; + +-- name: ListBooksByAuthor :many +SELECT books.id, books.title, authors.name AS author_name +FROM books +JOIN authors ON authors.id = books.author_id +WHERE books.author_id = $1; +``` + +```go +type GetBookRow struct { + ID int64 + Title string + AuthorName string +} + +type ListBooksByAuthorRow struct { + ID int64 + Title string + AuthorName string +} +``` + +To have them return one type, name it with `:type ` after the +command: + +```sql +-- name: GetBook :one :type BookWithAuthor +SELECT books.id, books.title, authors.name AS author_name +FROM books +JOIN authors ON authors.id = books.author_id +WHERE books.id = $1; + +-- name: ListBooksByAuthor :many :type BookWithAuthor +SELECT books.id, books.title, authors.name AS author_name +FROM books +JOIN authors ON authors.id = books.author_id +WHERE books.author_id = $1; +``` + +```go +type BookWithAuthor struct { + ID int64 + Title string + AuthorName string +} + +func (q *Queries) GetBook(ctx context.Context, id int64) (BookWithAuthor, error) { + // ... +} + +func (q *Queries) ListBooksByAuthor(ctx context.Context, authorID int64) ([]BookWithAuthor, error) { + // ... +} +``` + +The type name works with [embedded structs](embedding.md) too, and a single +query can use it just to choose the name of its row type. + +## Rules + +- Every query that names a type must return the same fields, in the same + order, with the same Go types and struct tags. Otherwise `sqlc generate` + fails and says which column differs: + + ``` + query ListBooks: :type BookWithAuthor does not match query GetBook: column 3 is AuthorID int64, want AuthorName string + ``` + +- The query must return more than one column, since a query with a single + column returns that column's value instead of a struct. +- The name must not be one sqlc already uses for a table's model. Queries + whose columns are exactly those of a table already return its model, such as + `Book`, without an annotation. +- The type name always wins: a query annotated with `:type` returns that type + even when its columns match a table's model. diff --git a/docs/reference/query-annotations.md b/docs/reference/query-annotations.md index 4fabe05aae..12b3b6d4c5 100644 --- a/docs/reference/query-annotations.md +++ b/docs/reference/query-annotations.md @@ -7,6 +7,14 @@ command. The format of this comment is as follows: -- name: ``` +A query that returns rows may also name the type they are returned as with +`:type `, so that several queries can return the same type. See +[sharing a row type between queries](../howto/row_types.md). + +```sql +-- name: :type +``` + ## `:exec` The generated method will return the error from diff --git a/docs/toc.yaml b/docs/toc.yaml index 464fe334de..3787eab467 100644 --- a/docs/toc.yaml +++ b/docs/toc.yaml @@ -40,6 +40,7 @@ sections: - howto/ddl.md - howto/structs.md - howto/embedding.md + - howto/row_types.md - howto/overrides.md - howto/rename.md diff --git a/internal/cmd/parse.go b/internal/cmd/parse.go index 72f59612c8..843cf69d7b 100644 --- a/internal/cmd/parse.go +++ b/internal/cmd/parse.go @@ -142,12 +142,12 @@ Examples: if err != nil { return fmt.Errorf("failed to read statement source: %w", err) } - name, cmd, err := metadata.ParseQueryNameAndType(rawSQL, commentSyntax) + md, err := metadata.ParseQueryNameAndType(rawSQL, commentSyntax) if err != nil { return fmt.Errorf("failed to parse query annotation: %w", err) } - ps.Name = name - ps.Cmd = cmd + ps.Name = md.Name + ps.Cmd = md.Cmd out = append(out, ps) } diff --git a/internal/cmd/shim.go b/internal/cmd/shim.go index 654500429a..e44ef4c08b 100644 --- a/internal/cmd/shim.go +++ b/internal/cmd/shim.go @@ -161,6 +161,7 @@ func pluginQueries(r *compiler.Result) []*plugin.Query { Params: params, Filename: q.Metadata.Filename, InsertIntoTable: iit, + TypeName: q.Metadata.TypeName, }) } return out diff --git a/internal/codegen/golang/result.go b/internal/codegen/golang/result.go index 50d654abd2..842f93c8c6 100644 --- a/internal/codegen/golang/result.go +++ b/internal/codegen/golang/result.go @@ -185,6 +185,7 @@ func argName(name string) string { func buildQueries(req *plugin.GenerateRequest, options *opts.Options, enums []Enum, structs []Struct) ([]Query, error) { models := buildModelTypeSet(enums, structs) qualifier := options.ModelsTypeQualifier() + rowTypes := newRowTypes(structs) qs := make([]Query, 0, len(req.Queries)) for _, query := range req.Queries { if query.Name == "" { @@ -269,6 +270,10 @@ func buildQueries(req *plugin.GenerateRequest, options *opts.Options, enums []En } } + if query.TypeName != "" && !returnsStruct(query) { + return nil, fmt.Errorf("query %s: :type %s needs a query that returns more than one column", query.Name, query.TypeName) + } + if len(query.Columns) == 1 && query.Columns[0].EmbedTable == nil { c := query.Columns[0] name := columnName(c, 0) @@ -303,7 +308,7 @@ func buildQueries(req *plugin.GenerateRequest, options *opts.Options, enums []En var emit bool for _, s := range structs { - if len(s.Fields) != len(query.Columns) { + if query.TypeName != "" || len(s.Fields) != len(query.Columns) { continue } same := true @@ -331,12 +336,22 @@ func buildQueries(req *plugin.GenerateRequest, options *opts.Options, enums []En embed: newGoEmbed(c.EmbedTable, structs, req.Catalog.DefaultSchema), }) } + name := gq.MethodName + "Row" + if query.TypeName != "" { + name = query.TypeName + } var err error - gs, err = columnsToStruct(req, options, gq.MethodName+"Row", columns, true, models, qualifier) + gs, err = columnsToStruct(req, options, name, columns, true, models, qualifier) if err != nil { return nil, err } emit = true + if query.TypeName != "" { + gs, emit, err = rowTypes.add(query.Name, gs) + if err != nil { + return nil, err + } + } } gq.Ret = QueryValue{ Emit: emit, @@ -361,6 +376,76 @@ var cmdReturnsData = map[string]struct{}{ metadata.CmdOne: {}, } +// returnsStruct reports whether the Go code for query returns its rows as a +// struct, which is what a ":type" annotation names. +func returnsStruct(query *plugin.Query) bool { + if len(query.Columns) == 1 && query.Columns[0].EmbedTable == nil { + return false + } + return putOutColumns(query) +} + +// rowTypes tracks the structs named by ":type" annotations, so that the +// queries sharing a name return one struct. +type rowTypes struct { + models map[string]bool + named map[string]rowType +} + +type rowType struct { + query string // the first query to use the name, which emits the struct + s *Struct +} + +func newRowTypes(models []Struct) *rowTypes { + r := &rowTypes{models: map[string]bool{}, named: map[string]rowType{}} + for _, m := range models { + r.models[m.Name] = true + } + return r +} + +// add takes the struct built for the rows of a query annotated with ":type" +// and returns the struct the query returns and whether to emit it. The first +// query to use a name emits its struct; the others return that struct, as long +// as their columns give the same fields. +func (r *rowTypes) add(query string, s *Struct) (*Struct, bool, error) { + if r.models[s.Name] { + return nil, false, fmt.Errorf("query %s: :type %s is already the name of a model", query, s.Name) + } + first, ok := r.named[s.Name] + if !ok { + r.named[s.Name] = rowType{query: query, s: s} + return s, true, nil + } + if diff := fieldsDiff(first.s.Fields, s.Fields); diff != "" { + return nil, false, fmt.Errorf("query %s: :type %s does not match query %s: %s", query, s.Name, first.query, diff) + } + return first.s, false, nil +} + +// fieldsDiff describes the first difference of got from want, or returns "" +// if they are the same. +func fieldsDiff(want, got []Field) string { + if len(got) != len(want) { + return fmt.Sprintf("%d columns, want %d", len(got), len(want)) + } + for i, w := range want { + g := got[i] + if g.Name != w.Name || g.Type != w.Type || g.Tag() != w.Tag() || fieldsDiff(w.EmbedFields, g.EmbedFields) != "" { + return fmt.Sprintf("column %d is %s, want %s", i+1, describeField(g), describeField(w)) + } + } + return "" +} + +func describeField(f Field) string { + if tag := f.Tag(); tag != "" { + return fmt.Sprintf("%s %s `%s`", f.Name, f.Type, tag) + } + return f.Name + " " + f.Type +} + func putOutColumns(query *plugin.Query) bool { _, found := cmdReturnsData[query.Cmd] return found diff --git a/internal/compiler/parse.go b/internal/compiler/parse.go index 0fcc7cee54..b8d727ebbe 100644 --- a/internal/compiler/parse.go +++ b/internal/compiler/parse.go @@ -54,24 +54,19 @@ func (c *Compiler) parseQuery(stmt ast.Node, pp *preprocess.Result, o opts.Parse return nil, errors.New("missing semicolon at end of file") } - name, cmd, err := metadata.ParseQueryNameAndType(rawSQL, metadata.CommentSyntax(c.parser.CommentSyntax())) + md, err := metadata.ParseQueryNameAndType(rawSQL, metadata.CommentSyntax(c.parser.CommentSyntax())) if err != nil { return nil, err } - if name == "" { + if md.Name == "" { return nil, nil } - 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 } - 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 { diff --git a/internal/compiler/parse_core.go b/internal/compiler/parse_core.go index 4c36d7d08c..57ead2ec4a 100644 --- a/internal/compiler/parse_core.go +++ b/internal/compiler/parse_core.go @@ -24,18 +24,17 @@ func (c *Compiler) parseQueryCore(raw *ast.RawStmt, src string, pre *preprocess. return nil, errors.New("missing semicolon at end of file") } - name, cmd, err := metadata.ParseQueryNameAndType(rawSQL, metadata.CommentSyntax(c.parser.CommentSyntax())) + md, err := metadata.ParseQueryNameAndType(rawSQL, metadata.CommentSyntax(c.parser.CommentSyntax())) if err != nil { return nil, err } - if name == "" { + if md.Name == "" { return nil, nil } - 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 } - md := metadata.Metadata{Name: name, Cmd: cmd} cleanedComments, err := source.CleanedComments(rawSQL, c.parser.CommentSyntax()) if err != nil { return nil, err diff --git a/internal/endtoend/testdata/codegen_json/gen/codegen.json b/internal/endtoend/testdata/codegen_json/gen/codegen.json index 47a6e3a39c..b380b56682 100644 --- a/internal/endtoend/testdata/codegen_json/gen/codegen.json +++ b/internal/endtoend/testdata/codegen_json/gen/codegen.json @@ -66389,7 +66389,8 @@ ], "comments": [], "filename": "query.sql", - "insert_into_table": null + "insert_into_table": null, + "type_name": "" }, { "text": "SELECT id, name, bio FROM authors\nORDER BY name", @@ -66478,7 +66479,8 @@ "params": [], "comments": [], "filename": "query.sql", - "insert_into_table": null + "insert_into_table": null, + "type_name": "" }, { "text": "INSERT INTO authors (\n name, bio\n) VALUES (\n $1, $2\n)\nRETURNING id, name, bio", @@ -66630,7 +66632,8 @@ "catalog": "", "schema": "", "name": "authors" - } + }, + "type_name": "" }, { "text": "DELETE FROM authors\nWHERE id = $1", @@ -66670,7 +66673,8 @@ ], "comments": [], "filename": "query.sql", - "insert_into_table": null + "insert_into_table": null, + "type_name": "" } ], "sqlc_version": "v1.31.1", diff --git a/internal/endtoend/testdata/process_plugin_sqlc_gen_json/gen/codegen.json b/internal/endtoend/testdata/process_plugin_sqlc_gen_json/gen/codegen.json index 4a85e7bd7e..a909507580 100644 --- a/internal/endtoend/testdata/process_plugin_sqlc_gen_json/gen/codegen.json +++ b/internal/endtoend/testdata/process_plugin_sqlc_gen_json/gen/codegen.json @@ -66391,7 +66391,8 @@ ], "comments": [], "filename": "query.sql", - "insert_into_table": null + "insert_into_table": null, + "type_name": "" }, { "text": "SELECT id, name, bio FROM authors\nORDER BY name", @@ -66480,7 +66481,8 @@ "params": [], "comments": [], "filename": "query.sql", - "insert_into_table": null + "insert_into_table": null, + "type_name": "" }, { "text": "INSERT INTO authors (\n name, bio\n) VALUES (\n $1, $2\n)\nRETURNING id, name, bio", @@ -66632,7 +66634,8 @@ "catalog": "", "schema": "", "name": "authors" - } + }, + "type_name": "" }, { "text": "DELETE FROM authors\nWHERE id = $1", @@ -66672,7 +66675,8 @@ ], "comments": [], "filename": "query.sql", - "insert_into_table": null + "insert_into_table": null, + "type_name": "" } ], "sqlc_version": "v1.31.1", diff --git a/internal/endtoend/testdata/query_type_annotation/postgresql/pgx/v5/go/db.go b/internal/endtoend/testdata/query_type_annotation/postgresql/pgx/v5/go/db.go new file mode 100644 index 0000000000..2666c1c05d --- /dev/null +++ b/internal/endtoend/testdata/query_type_annotation/postgresql/pgx/v5/go/db.go @@ -0,0 +1,32 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...any) (pgconn.CommandTag, error) + Query(context.Context, string, ...any) (pgx.Rows, error) + QueryRow(context.Context, string, ...any) 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/query_type_annotation/postgresql/pgx/v5/go/models.go b/internal/endtoend/testdata/query_type_annotation/postgresql/pgx/v5/go/models.go new file mode 100644 index 0000000000..927a05454c --- /dev/null +++ b/internal/endtoend/testdata/query_type_annotation/postgresql/pgx/v5/go/models.go @@ -0,0 +1,16 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +type Author struct { + ID int64 + Name string +} + +type Book struct { + ID int64 + AuthorID int64 + Title string +} diff --git a/internal/endtoend/testdata/query_type_annotation/postgresql/pgx/v5/go/query.sql.go b/internal/endtoend/testdata/query_type_annotation/postgresql/pgx/v5/go/query.sql.go new file mode 100644 index 0000000000..3fbfbca3a7 --- /dev/null +++ b/internal/endtoend/testdata/query_type_annotation/postgresql/pgx/v5/go/query.sql.go @@ -0,0 +1,171 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" +) + +const getBook = `-- name: GetBook :one +SELECT books.id, books.title, authors.name AS author_name +FROM books +JOIN authors ON authors.id = books.author_id +WHERE books.id = $1 +` + +type BookWithAuthor struct { + ID int64 + Title string + AuthorName string +} + +func (q *Queries) GetBook(ctx context.Context, id int64) (BookWithAuthor, error) { + row := q.db.QueryRow(ctx, getBook, id) + var i BookWithAuthor + err := row.Scan(&i.ID, &i.Title, &i.AuthorName) + return i, err +} + +const getBookAndAuthor = `-- name: GetBookAndAuthor :one +SELECT books.id, books.author_id, books.title, authors.id, authors.name +FROM books +JOIN authors ON authors.id = books.author_id +WHERE books.id = $1 +` + +type BookAndAuthor struct { + Book Book + Author Author +} + +func (q *Queries) GetBookAndAuthor(ctx context.Context, id int64) (BookAndAuthor, error) { + row := q.db.QueryRow(ctx, getBookAndAuthor, id) + var i BookAndAuthor + err := row.Scan( + &i.Book.ID, + &i.Book.AuthorID, + &i.Book.Title, + &i.Author.ID, + &i.Author.Name, + ) + return i, err +} + +const listAuthors = `-- name: ListAuthors :many +SELECT id, name FROM authors +` + +type AuthorSummary struct { + ID int64 + Name string +} + +func (q *Queries) ListAuthors(ctx context.Context) ([]AuthorSummary, error) { + rows, err := q.db.Query(ctx, listAuthors) + if err != nil { + return nil, err + } + defer rows.Close() + var items []AuthorSummary + for rows.Next() { + var i AuthorSummary + if err := rows.Scan(&i.ID, &i.Name); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listBooksAndAuthors = `-- name: ListBooksAndAuthors :many +SELECT books.id, books.author_id, books.title, authors.id, authors.name +FROM books +JOIN authors ON authors.id = books.author_id +ORDER BY books.id +` + +func (q *Queries) ListBooksAndAuthors(ctx context.Context) ([]BookAndAuthor, error) { + rows, err := q.db.Query(ctx, listBooksAndAuthors) + if err != nil { + return nil, err + } + defer rows.Close() + var items []BookAndAuthor + for rows.Next() { + var i BookAndAuthor + if err := rows.Scan( + &i.Book.ID, + &i.Book.AuthorID, + &i.Book.Title, + &i.Author.ID, + &i.Author.Name, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listBooksByAuthor = `-- name: ListBooksByAuthor :many +SELECT books.id, books.title, authors.name AS author_name +FROM books +JOIN authors ON authors.id = books.author_id +WHERE books.author_id = $1 +` + +func (q *Queries) ListBooksByAuthor(ctx context.Context, authorID int64) ([]BookWithAuthor, error) { + rows, err := q.db.Query(ctx, listBooksByAuthor, authorID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []BookWithAuthor + for rows.Next() { + var i BookWithAuthor + if err := rows.Scan(&i.ID, &i.Title, &i.AuthorName); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listBooksByTitle = `-- name: ListBooksByTitle :many +SELECT b.id, b.title, a.name AS author_name +FROM books b +JOIN authors a ON a.id = b.author_id +WHERE b.title = $1 +` + +func (q *Queries) ListBooksByTitle(ctx context.Context, title string) ([]BookWithAuthor, error) { + rows, err := q.db.Query(ctx, listBooksByTitle, title) + if err != nil { + return nil, err + } + defer rows.Close() + var items []BookWithAuthor + for rows.Next() { + var i BookWithAuthor + if err := rows.Scan(&i.ID, &i.Title, &i.AuthorName); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/query_type_annotation/postgresql/pgx/v5/query.sql b/internal/endtoend/testdata/query_type_annotation/postgresql/pgx/v5/query.sql new file mode 100644 index 0000000000..62e3a4b307 --- /dev/null +++ b/internal/endtoend/testdata/query_type_annotation/postgresql/pgx/v5/query.sql @@ -0,0 +1,32 @@ +-- name: GetBook :one :type BookWithAuthor +SELECT books.id, books.title, authors.name AS author_name +FROM books +JOIN authors ON authors.id = books.author_id +WHERE books.id = $1; + +-- name: ListBooksByAuthor :many :type BookWithAuthor +SELECT books.id, books.title, authors.name AS author_name +FROM books +JOIN authors ON authors.id = books.author_id +WHERE books.author_id = $1; + +-- name: ListBooksByTitle :many :type BookWithAuthor +SELECT b.id, b.title, a.name AS author_name +FROM books b +JOIN authors a ON a.id = b.author_id +WHERE b.title = $1; + +-- name: GetBookAndAuthor :one :type BookAndAuthor +SELECT sqlc.embed(books), sqlc.embed(authors) +FROM books +JOIN authors ON authors.id = books.author_id +WHERE books.id = $1; + +-- name: ListBooksAndAuthors :many :type BookAndAuthor +SELECT sqlc.embed(books), sqlc.embed(authors) +FROM books +JOIN authors ON authors.id = books.author_id +ORDER BY books.id; + +-- name: ListAuthors :many :type AuthorSummary +SELECT id, name FROM authors; diff --git a/internal/endtoend/testdata/query_type_annotation/postgresql/pgx/v5/schema.sql b/internal/endtoend/testdata/query_type_annotation/postgresql/pgx/v5/schema.sql new file mode 100644 index 0000000000..c2f36fd302 --- /dev/null +++ b/internal/endtoend/testdata/query_type_annotation/postgresql/pgx/v5/schema.sql @@ -0,0 +1,10 @@ +CREATE TABLE authors ( + id BIGSERIAL PRIMARY KEY, + name text NOT NULL +); + +CREATE TABLE books ( + id BIGSERIAL PRIMARY KEY, + author_id bigint NOT NULL REFERENCES authors (id), + title text NOT NULL +); diff --git a/internal/endtoend/testdata/query_type_annotation/postgresql/pgx/v5/sqlc.yaml b/internal/endtoend/testdata/query_type_annotation/postgresql/pgx/v5/sqlc.yaml new file mode 100644 index 0000000000..5dc63e3f91 --- /dev/null +++ b/internal/endtoend/testdata/query_type_annotation/postgresql/pgx/v5/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/query_type_annotation_invalid/postgresql/pgx/v5/query.sql b/internal/endtoend/testdata/query_type_annotation_invalid/postgresql/pgx/v5/query.sql new file mode 100644 index 0000000000..b31ba26d71 --- /dev/null +++ b/internal/endtoend/testdata/query_type_annotation_invalid/postgresql/pgx/v5/query.sql @@ -0,0 +1,8 @@ +-- name: GetBook :one :type +SELECT id, title FROM books WHERE id = $1; + +-- name: ListBooks :many :type 9Books +SELECT id, title FROM books; + +-- name: ListAuthors :many :type AuthorRow extra +SELECT id, name FROM authors; diff --git a/internal/endtoend/testdata/query_type_annotation_invalid/postgresql/pgx/v5/schema.sql b/internal/endtoend/testdata/query_type_annotation_invalid/postgresql/pgx/v5/schema.sql new file mode 100644 index 0000000000..c2f36fd302 --- /dev/null +++ b/internal/endtoend/testdata/query_type_annotation_invalid/postgresql/pgx/v5/schema.sql @@ -0,0 +1,10 @@ +CREATE TABLE authors ( + id BIGSERIAL PRIMARY KEY, + name text NOT NULL +); + +CREATE TABLE books ( + id BIGSERIAL PRIMARY KEY, + author_id bigint NOT NULL REFERENCES authors (id), + title text NOT NULL +); diff --git a/internal/endtoend/testdata/query_type_annotation_invalid/postgresql/pgx/v5/sqlc.yaml b/internal/endtoend/testdata/query_type_annotation_invalid/postgresql/pgx/v5/sqlc.yaml new file mode 100644 index 0000000000..5dc63e3f91 --- /dev/null +++ b/internal/endtoend/testdata/query_type_annotation_invalid/postgresql/pgx/v5/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/query_type_annotation_invalid/postgresql/pgx/v5/stderr.txt b/internal/endtoend/testdata/query_type_annotation_invalid/postgresql/pgx/v5/stderr.txt new file mode 100644 index 0000000000..21e30b48c2 --- /dev/null +++ b/internal/endtoend/testdata/query_type_annotation_invalid/postgresql/pgx/v5/stderr.txt @@ -0,0 +1,4 @@ +# package querytest +query.sql:1:1: missing type name after :type: -- name: GetBook :one :type +query.sql:5:1: invalid type name "9Books" +query.sql:8:1: invalid query comment: -- name: ListAuthors :many :type AuthorRow extra diff --git a/internal/endtoend/testdata/query_type_annotation_mismatch/postgresql/pgx/v5/query.sql b/internal/endtoend/testdata/query_type_annotation_mismatch/postgresql/pgx/v5/query.sql new file mode 100644 index 0000000000..9e9068d3c8 --- /dev/null +++ b/internal/endtoend/testdata/query_type_annotation_mismatch/postgresql/pgx/v5/query.sql @@ -0,0 +1,9 @@ +-- name: GetBook :one :type BookWithAuthor +SELECT books.id, books.title, authors.name AS author_name +FROM books +JOIN authors ON authors.id = books.author_id +WHERE books.id = $1; + +-- name: ListBooks :many :type BookWithAuthor +SELECT books.id, books.title, books.author_id +FROM books; diff --git a/internal/endtoend/testdata/query_type_annotation_mismatch/postgresql/pgx/v5/schema.sql b/internal/endtoend/testdata/query_type_annotation_mismatch/postgresql/pgx/v5/schema.sql new file mode 100644 index 0000000000..c2f36fd302 --- /dev/null +++ b/internal/endtoend/testdata/query_type_annotation_mismatch/postgresql/pgx/v5/schema.sql @@ -0,0 +1,10 @@ +CREATE TABLE authors ( + id BIGSERIAL PRIMARY KEY, + name text NOT NULL +); + +CREATE TABLE books ( + id BIGSERIAL PRIMARY KEY, + author_id bigint NOT NULL REFERENCES authors (id), + title text NOT NULL +); diff --git a/internal/endtoend/testdata/query_type_annotation_mismatch/postgresql/pgx/v5/sqlc.yaml b/internal/endtoend/testdata/query_type_annotation_mismatch/postgresql/pgx/v5/sqlc.yaml new file mode 100644 index 0000000000..5dc63e3f91 --- /dev/null +++ b/internal/endtoend/testdata/query_type_annotation_mismatch/postgresql/pgx/v5/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/query_type_annotation_mismatch/postgresql/pgx/v5/stderr.txt b/internal/endtoend/testdata/query_type_annotation_mismatch/postgresql/pgx/v5/stderr.txt new file mode 100644 index 0000000000..60ec536005 --- /dev/null +++ b/internal/endtoend/testdata/query_type_annotation_mismatch/postgresql/pgx/v5/stderr.txt @@ -0,0 +1,2 @@ +# package querytest +error generating code: query ListBooks: :type BookWithAuthor does not match query GetBook: column 3 is AuthorID int64, want AuthorName string diff --git a/internal/endtoend/testdata/query_type_annotation_model_name/postgresql/pgx/v5/query.sql b/internal/endtoend/testdata/query_type_annotation_model_name/postgresql/pgx/v5/query.sql new file mode 100644 index 0000000000..be0562f817 --- /dev/null +++ b/internal/endtoend/testdata/query_type_annotation_model_name/postgresql/pgx/v5/query.sql @@ -0,0 +1,3 @@ +-- name: ListBookTitles :many :type Book +SELECT books.id, books.title +FROM books; diff --git a/internal/endtoend/testdata/query_type_annotation_model_name/postgresql/pgx/v5/schema.sql b/internal/endtoend/testdata/query_type_annotation_model_name/postgresql/pgx/v5/schema.sql new file mode 100644 index 0000000000..c2f36fd302 --- /dev/null +++ b/internal/endtoend/testdata/query_type_annotation_model_name/postgresql/pgx/v5/schema.sql @@ -0,0 +1,10 @@ +CREATE TABLE authors ( + id BIGSERIAL PRIMARY KEY, + name text NOT NULL +); + +CREATE TABLE books ( + id BIGSERIAL PRIMARY KEY, + author_id bigint NOT NULL REFERENCES authors (id), + title text NOT NULL +); diff --git a/internal/endtoend/testdata/query_type_annotation_model_name/postgresql/pgx/v5/sqlc.yaml b/internal/endtoend/testdata/query_type_annotation_model_name/postgresql/pgx/v5/sqlc.yaml new file mode 100644 index 0000000000..5dc63e3f91 --- /dev/null +++ b/internal/endtoend/testdata/query_type_annotation_model_name/postgresql/pgx/v5/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/query_type_annotation_model_name/postgresql/pgx/v5/stderr.txt b/internal/endtoend/testdata/query_type_annotation_model_name/postgresql/pgx/v5/stderr.txt new file mode 100644 index 0000000000..ebe4d3fe79 --- /dev/null +++ b/internal/endtoend/testdata/query_type_annotation_model_name/postgresql/pgx/v5/stderr.txt @@ -0,0 +1,2 @@ +# package querytest +error generating code: query ListBookTitles: :type Book is already the name of a model diff --git a/internal/endtoend/testdata/query_type_annotation_single_column/postgresql/pgx/v5/query.sql b/internal/endtoend/testdata/query_type_annotation_single_column/postgresql/pgx/v5/query.sql new file mode 100644 index 0000000000..02866e9760 --- /dev/null +++ b/internal/endtoend/testdata/query_type_annotation_single_column/postgresql/pgx/v5/query.sql @@ -0,0 +1,2 @@ +-- name: ListBookTitles :many :type BookTitle +SELECT title FROM books; diff --git a/internal/endtoend/testdata/query_type_annotation_single_column/postgresql/pgx/v5/schema.sql b/internal/endtoend/testdata/query_type_annotation_single_column/postgresql/pgx/v5/schema.sql new file mode 100644 index 0000000000..c2f36fd302 --- /dev/null +++ b/internal/endtoend/testdata/query_type_annotation_single_column/postgresql/pgx/v5/schema.sql @@ -0,0 +1,10 @@ +CREATE TABLE authors ( + id BIGSERIAL PRIMARY KEY, + name text NOT NULL +); + +CREATE TABLE books ( + id BIGSERIAL PRIMARY KEY, + author_id bigint NOT NULL REFERENCES authors (id), + title text NOT NULL +); diff --git a/internal/endtoend/testdata/query_type_annotation_single_column/postgresql/pgx/v5/sqlc.yaml b/internal/endtoend/testdata/query_type_annotation_single_column/postgresql/pgx/v5/sqlc.yaml new file mode 100644 index 0000000000..5dc63e3f91 --- /dev/null +++ b/internal/endtoend/testdata/query_type_annotation_single_column/postgresql/pgx/v5/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/query_type_annotation_single_column/postgresql/pgx/v5/stderr.txt b/internal/endtoend/testdata/query_type_annotation_single_column/postgresql/pgx/v5/stderr.txt new file mode 100644 index 0000000000..06fb158a56 --- /dev/null +++ b/internal/endtoend/testdata/query_type_annotation_single_column/postgresql/pgx/v5/stderr.txt @@ -0,0 +1,2 @@ +# package querytest +error generating code: query ListBookTitles: :type BookTitle needs a query that returns more than one column diff --git a/internal/metadata/meta.go b/internal/metadata/meta.go index 76ee992a7a..a80d9f62a7 100644 --- a/internal/metadata/meta.go +++ b/internal/metadata/meta.go @@ -15,6 +15,7 @@ type CommentSyntax source.CommentSyntax type Metadata struct { Name string Cmd string + TypeName string // from ":type "; queries that share it return the same type Comments []string Params map[string]string Flags map[string]bool @@ -58,7 +59,11 @@ func validateQueryName(name string) error { return nil } -func ParseQueryNameAndType(t string, commentStyle CommentSyntax) (string, string, error) { +// ParseQueryNameAndType parses the "name:" annotation of a query, which holds +// its name, its command and an optional ":type ". Only Name, Cmd and +// TypeName are set on the returned Metadata; it is empty if the query has no +// such annotation. +func ParseQueryNameAndType(t string, commentStyle CommentSyntax) (Metadata, error) { for line := range strings.SplitSeq(t, "\n") { var prefix string if strings.HasPrefix(line, "--") { @@ -90,7 +95,7 @@ func ParseQueryNameAndType(t string, commentStyle CommentSyntax) (string, string continue } if !strings.HasPrefix(rest, " name: ") { - return "", "", fmt.Errorf("invalid metadata: %s", line) + return Metadata{}, fmt.Errorf("invalid metadata: %s", line) } part := strings.Split(strings.TrimSpace(line), " ") @@ -98,24 +103,34 @@ func ParseQueryNameAndType(t string, commentStyle CommentSyntax) (string, string part = part[:len(part)-1] // removes the trailing "*/" element } 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 { - return "", "", fmt.Errorf("invalid query comment: %s", line) + return Metadata{}, fmt.Errorf("missing query type [':one', ':many', ':exec', ':execrows', ':execlastid', ':execresult', ':copyfrom', 'batchexec', 'batchmany', 'batchone']: %s", line) + } + var typeName string + switch { + case len(part) == 4: + case len(part) == 5 && part[4] == ":type": + return Metadata{}, fmt.Errorf("missing type name after :type: %s", line) + case len(part) == 6 && part[4] == ":type": + typeName = part[5] + default: + return Metadata{}, fmt.Errorf("invalid query comment: %s", line) } queryName := part[2] queryType := strings.TrimSpace(part[3]) switch queryType { case CmdOne, CmdMany, CmdExec, CmdExecResult, CmdExecRows, CmdExecLastId, CmdCopyFrom, CmdBatchExec, CmdBatchMany, CmdBatchOne: default: - return "", "", fmt.Errorf("invalid query type: %s", queryType) + return Metadata{}, fmt.Errorf("invalid query type: %s", queryType) } if err := validateQueryName(queryName); err != nil { - return "", "", err + return Metadata{}, err + } + if typeName != "" && validateQueryName(typeName) != nil { + return Metadata{}, fmt.Errorf("invalid type name %q", typeName) } - return queryName, queryType, nil + return Metadata{Name: queryName, Cmd: queryType, TypeName: typeName}, nil } - return "", "", nil + return Metadata{}, nil } // ParseCommentFlags processes the comments provided with queries to determine the metadata params, flags and rules to skip. diff --git a/internal/metadata/meta_test.go b/internal/metadata/meta_test.go index e9ef54586e..089310af46 100644 --- a/internal/metadata/meta_test.go +++ b/internal/metadata/meta_test.go @@ -19,7 +19,7 @@ func TestParseQueryNameAndType(t *testing.T) { "-- name:CreateFoo", `--name:CreateFoo :two`, } { - if _, _, err := ParseQueryNameAndType(query, CommentSyntax{Dash: true}); err == nil { + if _, err := ParseQueryNameAndType(query, CommentSyntax{Dash: true}); err == nil { t.Errorf("expected invalid metadata: %q", query) } } @@ -29,7 +29,7 @@ func TestParseQueryNameAndType(t *testing.T) { `-- name comment`, `--name comment`, } { - if _, _, err := ParseQueryNameAndType(query, CommentSyntax{Dash: true}); err != nil { + if _, err := ParseQueryNameAndType(query, CommentSyntax{Dash: true}); err != nil { t.Errorf("expected valid comment: %q", query) } } @@ -39,15 +39,15 @@ func TestParseQueryNameAndType(t *testing.T) { `# name: CreateFoo :one`: {Hash: true}, `/* name: CreateFoo :one */`: {SlashStar: true}, } { - queryName, queryCmd, err := ParseQueryNameAndType(query, cs) + md, 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 md.Name != "CreateFoo" { + t.Errorf("incorrect queryName parsed: (%q) %q", md.Name, query) } - if queryCmd != CmdOne { - t.Errorf("incorrect queryCmd parsed: (%q) %q", queryCmd, query) + if md.Cmd != CmdOne { + t.Errorf("incorrect queryCmd parsed: (%q) %q", md.Cmd, query) } } diff --git a/internal/plugin/codegen.pb.go b/internal/plugin/codegen.pb.go index 525ffc72ef..5feab61f30 100644 --- a/internal/plugin/codegen.pb.go +++ b/internal/plugin/codegen.pb.go @@ -816,6 +816,9 @@ type Query struct { Comments []string `protobuf:"bytes,6,rep,name=comments,proto3" json:"comments,omitempty"` Filename string `protobuf:"bytes,7,opt,name=filename,proto3" json:"filename,omitempty"` InsertIntoTable *Identifier `protobuf:"bytes,8,opt,name=insert_into_table,proto3" json:"insert_into_table,omitempty"` + // Set by a ":type " annotation. Queries with the same type_name + // return rows of the same type. + TypeName string `protobuf:"bytes,9,opt,name=type_name,proto3" json:"type_name,omitempty"` } func (x *Query) Reset() { @@ -906,6 +909,13 @@ func (x *Query) GetInsertIntoTable() *Identifier { return nil } +func (x *Query) GetTypeName() string { + if x != nil { + return x.TypeName + } + return "" +} + type Parameter struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1308,7 +1318,7 @@ var file_plugin_codegen_proto_rawDesc = []byte{ 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x18, 0x10, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x75, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x72, 0x72, 0x61, 0x79, 0x5f, 0x64, 0x69, 0x6d, 0x73, 0x18, 0x11, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x09, 0x61, 0x72, 0x72, 0x61, 0x79, 0x44, 0x69, 0x6d, 0x73, 0x22, 0x94, + 0x01, 0x28, 0x05, 0x52, 0x09, 0x61, 0x72, 0x72, 0x61, 0x79, 0x44, 0x69, 0x6d, 0x73, 0x22, 0xb2, 0x02, 0x0a, 0x05, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, @@ -1326,45 +1336,47 @@ var file_plugin_codegen_proto_rawDesc = []byte{ 0x74, 0x6f, 0x5f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x11, 0x69, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x5f, 0x69, 0x6e, 0x74, 0x6f, 0x5f, - 0x74, 0x61, 0x62, 0x6c, 0x65, 0x22, 0x4b, 0x0a, 0x09, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, - 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x06, 0x63, 0x6f, - 0x6c, 0x75, 0x6d, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6c, 0x75, - 0x67, 0x69, 0x6e, 0x2e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x52, 0x06, 0x63, 0x6f, 0x6c, 0x75, - 0x6d, 0x6e, 0x22, 0x87, 0x02, 0x0a, 0x0f, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, - 0x67, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, - 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x08, 0x73, 0x65, 0x74, 0x74, - 0x69, 0x6e, 0x67, 0x73, 0x12, 0x29, 0x0a, 0x07, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x43, - 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x07, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x12, - 0x27, 0x0a, 0x07, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x0d, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, - 0x07, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x71, 0x6c, 0x63, - 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, - 0x73, 0x71, 0x6c, 0x63, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x26, 0x0a, 0x0e, - 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x5f, 0x6f, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x5f, 0x6f, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x67, 0x6c, - 0x6f, 0x62, 0x61, 0x6c, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x36, 0x0a, 0x10, - 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x22, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x0c, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x66, - 0x69, 0x6c, 0x65, 0x73, 0x32, 0x4f, 0x0a, 0x0e, 0x43, 0x6f, 0x64, 0x65, 0x67, 0x65, 0x6e, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3d, 0x0a, 0x08, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, - 0x74, 0x65, 0x12, 0x17, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x47, 0x65, 0x6e, 0x65, - 0x72, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x70, 0x6c, - 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x7c, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6c, 0x75, - 0x67, 0x69, 0x6e, 0x42, 0x0c, 0x43, 0x6f, 0x64, 0x65, 0x67, 0x65, 0x6e, 0x50, 0x72, 0x6f, 0x74, - 0x6f, 0x50, 0x01, 0x5a, 0x28, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x73, 0x71, 0x6c, 0x63, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x73, 0x71, 0x6c, 0x63, 0x2f, 0x69, 0x6e, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0xa2, 0x02, 0x03, - 0x50, 0x58, 0x58, 0xaa, 0x02, 0x06, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0xca, 0x02, 0x06, 0x50, - 0x6c, 0x75, 0x67, 0x69, 0x6e, 0xe2, 0x02, 0x12, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x5c, 0x47, - 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x06, 0x50, 0x6c, 0x75, - 0x67, 0x69, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x22, 0x4b, 0x0a, 0x09, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, + 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x06, 0x63, 0x6f, 0x6c, 0x75, + 0x6d, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x2e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x52, 0x06, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, + 0x22, 0x87, 0x02, 0x0a, 0x0f, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, + 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, + 0x67, 0x73, 0x12, 0x29, 0x0a, 0x07, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x43, 0x61, 0x74, + 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x07, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x12, 0x27, 0x0a, + 0x07, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, + 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x07, 0x71, + 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x71, 0x6c, 0x63, 0x5f, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x71, + 0x6c, 0x63, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x26, 0x0a, 0x0e, 0x70, 0x6c, + 0x75, 0x67, 0x69, 0x6e, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x0e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x5f, 0x6f, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x67, 0x6c, 0x6f, 0x62, + 0x61, 0x6c, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x36, 0x0a, 0x10, 0x47, 0x65, + 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x22, + 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, + 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c, + 0x65, 0x73, 0x32, 0x4f, 0x0a, 0x0e, 0x43, 0x6f, 0x64, 0x65, 0x67, 0x65, 0x6e, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x12, 0x3d, 0x0a, 0x08, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, + 0x12, 0x17, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x42, 0x7c, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x42, 0x0c, 0x43, 0x6f, 0x64, 0x65, 0x67, 0x65, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, + 0x01, 0x5a, 0x28, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x71, + 0x6c, 0x63, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x73, 0x71, 0x6c, 0x63, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0xa2, 0x02, 0x03, 0x50, 0x58, + 0x58, 0xaa, 0x02, 0x06, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0xca, 0x02, 0x06, 0x50, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0xe2, 0x02, 0x12, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x5c, 0x47, 0x50, 0x42, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x06, 0x50, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/protos/plugin/codegen.proto b/protos/plugin/codegen.proto index e6faf19bad..73250bb233 100644 --- a/protos/plugin/codegen.proto +++ b/protos/plugin/codegen.proto @@ -111,6 +111,9 @@ message Query { repeated string comments = 6 [json_name = "comments"]; string filename = 7 [json_name = "filename"]; Identifier insert_into_table = 8 [json_name = "insert_into_table"]; + // Set by a ":type " annotation. Queries with the same type_name + // return rows of the same type. + string type_name = 9 [json_name = "type_name"]; } message Parameter {