Skip to content

Commit a506f85

Browse files
committed
Simplified changes and removed sqlc.embed.jsonb directive.
1 parent fe89bbd commit a506f85

15 files changed

Lines changed: 311 additions & 716 deletions

File tree

docs/howto/embedding.md

Lines changed: 18 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -60,50 +60,31 @@ type ScoreAndTestsRow struct {
6060
}
6161
```
6262

63-
#### Nested JSON objects and arrays
63+
#### JSON objects and arrays
6464

65-
`sqlc.jsonb_build_object."Name"(key, value, key, value, ...)` builds a named
66-
Go struct from an inline JSON shape — a typed wrapper around Postgres's
67-
`jsonb_build_object`, so keys must be string literals. This is most useful
68-
for pulling a one-to-many relationship into a single query as a slice
69-
field, instead of running a separate query per parent row. It's **pgx/v5
70-
only**: `sqlc generate` fails with a clear error for any other
71-
`sql_package`.
72-
73-
`"Name"` is required and must be a double-quoted identifier in that
74-
position, not an argument — Postgres parses this as a 3-part qualified
75-
function name (`catalog.schema.name`), and sqlc reads the struct name
76-
directly off of it.
77-
78-
```sql
79-
CREATE TABLE authors (
80-
id bigserial PRIMARY KEY,
81-
name text NOT NULL
82-
);
83-
84-
CREATE TABLE books (
85-
id bigserial PRIMARY KEY,
86-
author_id bigint NOT NULL REFERENCES authors (id),
87-
title text NOT NULL
88-
);
89-
```
65+
`sqlc.jsonb_build_object."Name"(key, value, ...)` returns a JSON-shaped
66+
result decoded straight into a named Go struct (or `[]struct` inside
67+
`ARRAY(...)`), useful for pulling a one-to-many relationship into a single
68+
query instead of one query per parent row. It's **pgx/v5 only** — `sqlc
69+
generate` fails with a clear error for any other `sql_package`. `"Name"` is
70+
required and is read off the 3-part qualified function name Postgres parses
71+
(`catalog.schema.name`), not an argument; keys must be string literals.
9072

9173
```sql
9274
-- name: GetAuthors :many
9375
SELECT
94-
sqlc.embed(authors),
76+
authors.id,
9577
ARRAY(
9678
SELECT sqlc.jsonb_build_object."Book"('id', books.id, 'title', books.title)
97-
FROM books
98-
WHERE books.author_id = authors.id
79+
FROM books WHERE books.author_id = authors.id
9980
) AS books
10081
FROM authors;
10182
```
10283

10384
```go
10485
type GetAuthorsRow struct {
105-
Author Author
106-
Books []Book
86+
ID int64
87+
Books []Book
10788
}
10889

10990
type Book struct {
@@ -112,82 +93,9 @@ type Book struct {
11293
}
11394
```
11495

115-
No custom `Scan`/`Value` methods, no wrapper type — `Books` is a plain
116-
`[]Book`; pgx v5 decodes the `jsonb[]` column into it directly. A lone
117-
`sqlc.jsonb_build_object."Name"(...)` (not wrapped in `ARRAY(...)`) works
118-
the same way and produces a plain struct field instead of a slice:
119-
120-
```sql
121-
-- name: GetAuthorSummary :one
122-
SELECT sqlc.jsonb_build_object."AuthorSummary"('name', name) AS summary FROM authors LIMIT 1;
123-
```
124-
125-
```go
126-
type GetAuthorSummaryRow struct {
127-
Summary AuthorSummary
128-
}
129-
```
130-
131-
Two queries that use the same explicit name reuse the same Go type, as long
132-
as their shapes match (this works even mixing scalar and `ARRAY(...)` uses
133-
of the same name). A shape mismatch, or a name that collides with an
134-
existing model/enum type, fails generation with an error instead of
135-
emitting Go code that won't compile.
136-
137-
##### Overriding generated names
138-
139-
The struct name and individual field names can be overridden via the
140-
`rename` option:
141-
142-
```json
143-
{
144-
"rename": {
145-
"Book": "BookSummary",
146-
"Book.id": "BookID"
147-
}
148-
}
149-
```
150-
151-
`"Book"` renames the type; `"Book.id"` renames just the `id` field within
152-
it, without affecting other types that also have an `id` key. Use the plain
153-
key (`"id"`) instead to rename that field everywhere.
154-
155-
##### Embedding a whole row as JSON
156-
157-
Listing every column by hand is tedious when you just want the whole row.
158-
`sqlc.embed.jsonb(table)` builds a JSON object from all of a table's columns,
159-
the same way `sqlc.embed(table)` gives you the table's model — but as a
160-
single JSON value, so it can be nested inside `ARRAY(...)` to return a slice
161-
of rows from one query:
162-
163-
```sql
164-
-- name: GetAuthorsWithBooks :many
165-
SELECT
166-
authors.id,
167-
ARRAY(SELECT sqlc.embed.jsonb(books) FROM books WHERE books.author_id = authors.id) AS books
168-
FROM authors;
169-
```
170-
171-
```go
172-
type GetAuthorsWithBooksRow struct {
173-
ID int64 `json:"id"`
174-
Books []Book `json:"books"`
175-
}
176-
177-
type Book struct {
178-
ID int64 `json:"id"`
179-
AuthorID int64 `json:"author_id"`
180-
Title string `json:"title"`
181-
}
182-
```
183-
184-
The generated struct is named from the result alias — singularized for the
185-
`ARRAY(...)` case (`AS books``Book`), or used as-is for a scalar
186-
`sqlc.embed.jsonb(table) AS author` (→ `Author`). Fields, types and JSON keys
187-
come from the table's columns, so the object always decodes cleanly. Under
188-
the hood the call is rewritten to `to_jsonb(table)`, which you'll see in
189-
`EXPLAIN` output and logs.
190-
191-
Like `sqlc.jsonb_build_object`, this is pgx/v5 only, and the generated name
192-
must not collide with a model or another JSON type; use the `rename` option
193-
if it does.
96+
The call is rewritten to Postgres's `jsonb_build_object`, which you'll see in
97+
`EXPLAIN` output. Two queries that use the same name share one Go type when
98+
their shapes match; a shape mismatch, or a name that collides with a model or
99+
another JSON type, fails generation. Names and fields can be overridden with
100+
`rename`: `"Book"` renames the type, `"Book.id"` just the `id` field within
101+
it.

internal/codegen/golang/gen.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ func generate(req *plugin.GenerateRequest, options *opts.Options, enums []Enum,
213213
}
214214

215215
if usesJSON(queries) && tctx.SQLDriver != opts.SQLDriverPGXV5 {
216-
return nil, errors.New("sqlc.jsonb_build_object(...) and sqlc.embed.jsonb(...) are only supported by pgx/v5")
216+
return nil, errors.New("sqlc.jsonb_build_object(...) is only supported by pgx/v5")
217217
}
218218

219219
funcMap := template.FuncMap{

internal/compiler/json_columns.go

Lines changed: 44 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
package compiler
22

33
import (
4+
"errors"
45
"fmt"
56

67
"github.com/sqlc-dev/sqlc/internal/sql/ast"
7-
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
8+
"github.com/sqlc-dev/sqlc/internal/sql/rewrite"
89
)
910

1011
// outputJSONColumn types an sqlc.jsonb_build_object."Name"(key, value, ...)
1112
// call as a synthesized struct: Name is the call's own function name, fields
1213
// come from the key/value pairs. Keys must be string literals; values are
13-
// typed like any other SELECT target.
14+
// typed by jsonValueColumn.
1415
func (c *Compiler) outputJSONColumn(qc *QueryCatalog, tables []*Table, res *ast.ResTarget, call *ast.FuncCall) (*Column, error) {
1516
var args []ast.Node
1617
if call.Args != nil {
1718
args = call.Args.Items
1819
}
19-
2020
jsonName := call.Func.Name
2121

2222
if len(args)%2 != 0 {
@@ -29,16 +29,10 @@ func (c *Compiler) outputJSONColumn(qc *QueryCatalog, tables []*Table, res *ast.
2929
if !ok {
3030
return nil, fmt.Errorf("sqlc.jsonb_build_object.%q(...) argument %d must be a string literal key", jsonName, i+1)
3131
}
32-
33-
valCols, err := c.outputColumn(qc, tables, &ast.ResTarget{Val: args[i+1]})
32+
val, err := c.jsonValueColumn(qc, tables, args[i+1], key)
3433
if err != nil {
3534
return nil, err
3635
}
37-
if len(valCols) != 1 {
38-
return nil, fmt.Errorf("sqlc.jsonb_build_object.%q(...) value for key %q must resolve to a single column", jsonName, key)
39-
}
40-
41-
val := valCols[0]
4236
val.Name = key
4337
fields = append(fields, val)
4438
}
@@ -47,67 +41,53 @@ func (c *Compiler) outputJSONColumn(qc *QueryCatalog, tables []*Table, res *ast.
4741
if res.Name != nil {
4842
name = *res.Name
4943
}
50-
return &Column{
51-
Name: name,
52-
DataType: "any",
53-
NotNull: true,
54-
JSONFields: fields,
55-
JSONName: jsonName,
56-
}, nil
44+
return &Column{Name: name, DataType: "any", NotNull: true, JSONFields: fields, JSONName: jsonName}, nil
5745
}
5846

59-
// outputEmbedJSONColumn types an sqlc.embed.jsonb(table) call as a struct
60-
// mirroring the table's columns, decoded from a single to_jsonb(table) value.
61-
// The name comes from the result alias; the array case leaves it empty for
62-
// the enclosing ARRAY_SUBLINK to fill from its own alias.
63-
func (c *Compiler) outputEmbedJSONColumn(qc *QueryCatalog, tables []*Table, res *ast.ResTarget, call *ast.FuncCall) (*Column, error) {
64-
if call.Args == nil || len(call.Args.Items) != 1 {
65-
return nil, fmt.Errorf("sqlc.embed.jsonb(...) takes a single table argument")
66-
}
67-
ref, ok := call.Args.Items[0].(*ast.ColumnRef)
68-
if !ok {
69-
return nil, fmt.Errorf("sqlc.embed.jsonb(...) argument must be a table reference")
70-
}
71-
target := astutils.Join(ref.Fields, ".")
72-
73-
var table *Table
74-
for _, t := range tables {
75-
if t.Rel.Name == target {
76-
table = t
77-
break
47+
// jsonValueColumn types a single sqlc.jsonb_build_object value: a column
48+
// reference, a nested JSON call, or an ARRAY(...) of one of those.
49+
func (c *Compiler) jsonValueColumn(qc *QueryCatalog, tables []*Table, node ast.Node, key string) (*Column, error) {
50+
switch v := node.(type) {
51+
case *ast.ColumnRef:
52+
cols, err := outputColumnRefs(&ast.ResTarget{Val: v}, tables, v)
53+
if err != nil {
54+
return nil, err
55+
}
56+
if len(cols) != 1 {
57+
return nil, fmt.Errorf("sqlc.jsonb_build_object value for key %q must resolve to a single column", key)
58+
}
59+
return cols[0], nil
60+
case *ast.SubLink:
61+
if v.SubLinkType == ast.ARRAY_SUBLINK {
62+
return c.arraySubLinkColumn(qc, tables, v)
63+
}
64+
case *ast.FuncCall:
65+
if rewrite.IsJSONCall(v) {
66+
return c.outputJSONColumn(qc, tables, &ast.ResTarget{}, v)
7867
}
7968
}
80-
if table == nil {
81-
return nil, fmt.Errorf("sqlc.embed.jsonb(%s): table not found in the query's FROM clause", target)
82-
}
69+
return &Column{DataType: "any"}, nil
70+
}
8371

84-
var fields []*Column
85-
for _, col := range table.Columns {
86-
fields = append(fields, &Column{
87-
Name: col.Name,
88-
DataType: col.DataType,
89-
NotNull: col.NotNull,
90-
Unsigned: col.Unsigned,
91-
IsArray: col.IsArray,
92-
ArrayDims: col.ArrayDims,
93-
Length: col.Length,
94-
Type: col.Type,
95-
})
72+
// arraySubLinkColumn types an ARRAY(subquery) expression: the subquery must
73+
// yield exactly one column, which becomes the array element.
74+
func (c *Compiler) arraySubLinkColumn(qc *QueryCatalog, tables []*Table, sublink *ast.SubLink) (*Column, error) {
75+
subcols, err := c.outputColumns(qc, sublink.Subselect)
76+
if err != nil {
77+
return nil, err
9678
}
97-
98-
name := "json"
99-
jsonName := ""
100-
if res.Name != nil {
101-
name = *res.Name
102-
jsonName = *res.Name
79+
if len(subcols) != 1 {
80+
return nil, errors.New("ARRAY() subquery must return only one column")
81+
}
82+
first := subcols[0]
83+
if first.IsArray {
84+
first.ArrayDims++
85+
} else {
86+
first.IsArray = true
87+
first.ArrayDims = 1
10388
}
104-
return &Column{
105-
Name: name,
106-
DataType: "any",
107-
NotNull: true,
108-
JSONFields: fields,
109-
JSONName: jsonName,
110-
}, nil
89+
first.NotNull = true
90+
return first, nil
11191
}
11292

11393
func jsonStringLiteral(node ast.Node) (string, bool) {

internal/compiler/json_columns_test.go

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -153,49 +153,3 @@ func TestOutputColumnsJSON(t *testing.T) {
153153
}
154154
})
155155
}
156-
157-
func TestOutputColumnsEmbedJSON(t *testing.T) {
158-
c := newTestCompiler(t, testSchema)
159-
160-
t.Run("scalar mirrors the table's columns", func(t *testing.T) {
161-
a := mustAnalyze(t, c, `SELECT sqlc.embed.jsonb(items) AS obj FROM items;`)
162-
col := a.Columns[0]
163-
if col.JSONName != "obj" {
164-
t.Errorf("JSONName = %q, want %q", col.JSONName, "obj")
165-
}
166-
if col.IsArray {
167-
t.Errorf("IsArray = true, want false")
168-
}
169-
if !col.NotNull {
170-
t.Errorf("NotNull = false, want true")
171-
}
172-
if len(col.JSONFields) != 3 {
173-
t.Fatalf("expected 3 JSONFields (id, x, y), got %d", len(col.JSONFields))
174-
}
175-
names := []string{col.JSONFields[0].Name, col.JSONFields[1].Name, col.JSONFields[2].Name}
176-
if names[0] != "id" || names[1] != "x" || names[2] != "y" {
177-
t.Errorf("field names = %v, want [id x y]", names)
178-
}
179-
})
180-
181-
t.Run("array singularizes the outer alias for the element name", func(t *testing.T) {
182-
a := mustAnalyze(t, c, `SELECT ARRAY(SELECT sqlc.embed.jsonb(items) FROM items) AS objs;`)
183-
col := a.Columns[0]
184-
if !col.IsArray || col.ArrayDims != 1 {
185-
t.Errorf("IsArray/ArrayDims = %v/%d, want true/1", col.IsArray, col.ArrayDims)
186-
}
187-
if col.JSONName != "obj" {
188-
t.Errorf("JSONName = %q, want %q (singular of the alias)", col.JSONName, "obj")
189-
}
190-
if len(col.JSONFields) != 3 {
191-
t.Fatalf("expected 3 JSONFields, got %d", len(col.JSONFields))
192-
}
193-
})
194-
195-
t.Run("unknown table", func(t *testing.T) {
196-
err := analyzeErr(t, c, `SELECT sqlc.embed.jsonb(missing) AS obj FROM items;`)
197-
if !strings.Contains(err.Error(), "table not found") {
198-
t.Errorf("error = %v, want mention of table not found", err)
199-
}
200-
})
201-
}

0 commit comments

Comments
 (0)