Skip to content

Commit 7bdd11b

Browse files
authored
postgresql: support OLD and NEW in RETURNING clauses (#4562)
1 parent 2e0f745 commit 7bdd11b

22 files changed

Lines changed: 671 additions & 21 deletions

File tree

internal/compiler/expand.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@ func (c *Compiler) expandStmt(qc *QueryCatalog, raw *ast.RawStmt, node ast.Node)
8484
return nil, err
8585
}
8686

87+
// Virtual tables for the OLD and NEW aliases available in a RETURNING
88+
// clause (PostgreSQL 18)
89+
rtables, err := c.returningTables(qc, node)
90+
if err != nil {
91+
return nil, err
92+
}
93+
8794
var targets *ast.List
8895
switch n := node.(type) {
8996
case *ast.DeleteStmt:
@@ -131,7 +138,11 @@ func (c *Compiler) expandStmt(qc *QueryCatalog, raw *ast.RawStmt, node ast.Node)
131138
}
132139
}
133140
}
134-
for _, t := range tables {
141+
starTables := tables
142+
if vt := returningTableForScope(tables, rtables, scope); vt != nil {
143+
starTables = []*Table{vt}
144+
}
145+
for _, t := range starTables {
135146
if scope != "" && scope != t.Rel.Name {
136147
continue
137148
}

internal/compiler/output_columns.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, er
5858
return nil, err
5959
}
6060

61+
// Virtual tables for the OLD and NEW aliases available in a RETURNING
62+
// clause (PostgreSQL 18)
63+
rtables, err := c.returningTables(qc, node)
64+
if err != nil {
65+
return nil, err
66+
}
67+
6168
targets := &ast.List{}
6269
switch n := node.(type) {
6370
case *ast.DeleteStmt:
@@ -235,7 +242,7 @@ func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, er
235242
continue
236243
}
237244
if ref, ok := arg.(*ast.ColumnRef); ok {
238-
columns, err := outputColumnRefs(res, tables, ref)
245+
columns, err := outputColumnRefs(res, tablesForRef(ref, tables, rtables), ref)
239246
if err != nil {
240247
return nil, err
241248
}
@@ -268,8 +275,12 @@ func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, er
268275
}
269276

270277
// TODO: This code is copied in func expand()
271-
for _, t := range tables {
272-
scope := astutils.Join(n.Fields, ".")
278+
scope := astutils.Join(n.Fields, ".")
279+
starTables := tables
280+
if vt := returningTableForScope(tables, rtables, scope); vt != nil {
281+
starTables = []*Table{vt}
282+
}
283+
for _, t := range starTables {
273284
if scope != "" && scope != t.Rel.Name {
274285
continue
275286
}
@@ -297,7 +308,7 @@ func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, er
297308
continue
298309
}
299310

300-
columns, err := outputColumnRefs(res, tables, n)
311+
columns, err := outputColumnRefs(res, tablesForRef(n, tables, rtables), n)
301312
if err != nil {
302313
return nil, err
303314
}

internal/compiler/returning.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package compiler
2+
3+
import (
4+
"github.com/sqlc-dev/sqlc/internal/config"
5+
"github.com/sqlc-dev/sqlc/internal/sql/ast"
6+
)
7+
8+
// returningTables builds virtual tables for the OLD and NEW aliases that
9+
// PostgreSQL 18 makes available in the RETURNING clause of INSERT, UPDATE and
10+
// DELETE statements. Each alias exposes the columns of the statement's target
11+
// table. For INSERT there is usually no old row and for DELETE there is no
12+
// new row, so every column reached through those aliases becomes nullable.
13+
func (c *Compiler) returningTables(qc *QueryCatalog, node ast.Node) ([]*Table, error) {
14+
if c.conf.Engine != config.EnginePostgreSQL {
15+
return nil, nil
16+
}
17+
18+
var rv *ast.RangeVar
19+
var returning *ast.List
20+
oldAlias, newAlias := "old", "new"
21+
var oldNullable, newNullable bool
22+
switch n := node.(type) {
23+
case *ast.DeleteStmt:
24+
rv = firstRangeVar(n.Relations)
25+
returning = n.ReturningList
26+
if n.ReturningOldAlias != "" {
27+
oldAlias = n.ReturningOldAlias
28+
}
29+
if n.ReturningNewAlias != "" {
30+
newAlias = n.ReturningNewAlias
31+
}
32+
// A deleted row has no new value
33+
newNullable = true
34+
case *ast.InsertStmt:
35+
rv = n.Relation
36+
returning = n.ReturningList
37+
if n.ReturningOldAlias != "" {
38+
oldAlias = n.ReturningOldAlias
39+
}
40+
if n.ReturningNewAlias != "" {
41+
newAlias = n.ReturningNewAlias
42+
}
43+
// An inserted row has no old value, except when an ON CONFLICT
44+
// clause updates an existing row instead
45+
oldNullable = true
46+
case *ast.UpdateStmt:
47+
rv = firstRangeVar(n.Relations)
48+
returning = n.ReturningList
49+
if n.ReturningOldAlias != "" {
50+
oldAlias = n.ReturningOldAlias
51+
}
52+
if n.ReturningNewAlias != "" {
53+
newAlias = n.ReturningNewAlias
54+
}
55+
default:
56+
return nil, nil
57+
}
58+
if rv == nil || returning == nil || len(returning.Items) == 0 {
59+
return nil, nil
60+
}
61+
62+
fqn, err := ParseTableName(rv)
63+
if err != nil {
64+
return nil, err
65+
}
66+
67+
build := func(alias string, nullable bool) *Table {
68+
table, err := qc.GetTable(fqn)
69+
if err != nil {
70+
// An unresolvable target table is reported by the regular
71+
// source table lookup, so ignore the error here
72+
return nil
73+
}
74+
table.Rel = &ast.TableName{Name: alias}
75+
if nullable {
76+
for _, col := range table.Columns {
77+
col.NotNull = false
78+
}
79+
}
80+
return table
81+
}
82+
83+
var tables []*Table
84+
if t := build(oldAlias, oldNullable); t != nil {
85+
tables = append(tables, t)
86+
}
87+
if t := build(newAlias, newNullable); t != nil {
88+
tables = append(tables, t)
89+
}
90+
return tables, nil
91+
}
92+
93+
func firstRangeVar(list *ast.List) *ast.RangeVar {
94+
if list == nil {
95+
return nil
96+
}
97+
for _, item := range list.Items {
98+
if rv, ok := item.(*ast.RangeVar); ok && rv != nil {
99+
return rv
100+
}
101+
}
102+
return nil
103+
}
104+
105+
// returningTableForScope returns the OLD or NEW virtual table named by scope.
106+
// A source table with the same name shadows the virtual table, matching
107+
// PostgreSQL, where the implicit aliases are only available when no relation
108+
// in the query is already known under that name.
109+
func returningTableForScope(tables, rtables []*Table, scope string) *Table {
110+
if scope == "" {
111+
return nil
112+
}
113+
for _, t := range tables {
114+
if t.Rel.Name == scope {
115+
return nil
116+
}
117+
}
118+
for _, t := range rtables {
119+
if t.Rel.Name == scope {
120+
return t
121+
}
122+
}
123+
return nil
124+
}
125+
126+
// tablesForRef resolves a column reference against the source tables,
127+
// extended with the OLD or NEW virtual table when the reference is qualified
128+
// with one of their names.
129+
func tablesForRef(ref *ast.ColumnRef, tables, rtables []*Table) []*Table {
130+
parts := stringSlice(ref.Fields)
131+
if len(parts) != 2 {
132+
return tables
133+
}
134+
if vt := returningTableForScope(tables, rtables, parts[0]); vt != nil {
135+
return append(append([]*Table{}, tables...), vt)
136+
}
137+
return tables
138+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"contexts": ["base"]
3+
}

internal/endtoend/testdata/returning_old_new/postgresql/pgx/v5/go/db.go

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/endtoend/testdata/returning_old_new/postgresql/pgx/v5/go/models.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/endtoend/testdata/returning_old_new/postgresql/pgx/v5/go/query.sql.go

Lines changed: 124 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)