diff --git a/internal/compiler/expand.go b/internal/compiler/expand.go index 98dd82cbdc..c21e413927 100644 --- a/internal/compiler/expand.go +++ b/internal/compiler/expand.go @@ -84,6 +84,13 @@ func (c *Compiler) expandStmt(qc *QueryCatalog, raw *ast.RawStmt, node ast.Node) return nil, err } + // Virtual tables for the OLD and NEW aliases available in a RETURNING + // clause (PostgreSQL 18) + rtables, err := c.returningTables(qc, node) + if err != nil { + return nil, err + } + var targets *ast.List switch n := node.(type) { case *ast.DeleteStmt: @@ -131,7 +138,11 @@ func (c *Compiler) expandStmt(qc *QueryCatalog, raw *ast.RawStmt, node ast.Node) } } } - for _, t := range tables { + starTables := tables + if vt := returningTableForScope(tables, rtables, scope); vt != nil { + starTables = []*Table{vt} + } + for _, t := range starTables { if scope != "" && scope != t.Rel.Name { continue } diff --git a/internal/compiler/output_columns.go b/internal/compiler/output_columns.go index 77c58852cd..76b63eb365 100644 --- a/internal/compiler/output_columns.go +++ b/internal/compiler/output_columns.go @@ -58,6 +58,13 @@ func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, er return nil, err } + // Virtual tables for the OLD and NEW aliases available in a RETURNING + // clause (PostgreSQL 18) + rtables, err := c.returningTables(qc, node) + if err != nil { + return nil, err + } + targets := &ast.List{} switch n := node.(type) { case *ast.DeleteStmt: @@ -235,7 +242,7 @@ func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, er continue } if ref, ok := arg.(*ast.ColumnRef); ok { - columns, err := outputColumnRefs(res, tables, ref) + columns, err := outputColumnRefs(res, tablesForRef(ref, tables, rtables), ref) if err != nil { return nil, err } @@ -268,8 +275,12 @@ func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, er } // TODO: This code is copied in func expand() - for _, t := range tables { - scope := astutils.Join(n.Fields, ".") + scope := astutils.Join(n.Fields, ".") + starTables := tables + if vt := returningTableForScope(tables, rtables, scope); vt != nil { + starTables = []*Table{vt} + } + for _, t := range starTables { if scope != "" && scope != t.Rel.Name { continue } @@ -297,7 +308,7 @@ func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, er continue } - columns, err := outputColumnRefs(res, tables, n) + columns, err := outputColumnRefs(res, tablesForRef(n, tables, rtables), n) if err != nil { return nil, err } diff --git a/internal/compiler/returning.go b/internal/compiler/returning.go new file mode 100644 index 0000000000..0a38abee86 --- /dev/null +++ b/internal/compiler/returning.go @@ -0,0 +1,138 @@ +package compiler + +import ( + "github.com/sqlc-dev/sqlc/internal/config" + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +// returningTables builds virtual tables for the OLD and NEW aliases that +// PostgreSQL 18 makes available in the RETURNING clause of INSERT, UPDATE and +// DELETE statements. Each alias exposes the columns of the statement's target +// table. For INSERT there is usually no old row and for DELETE there is no +// new row, so every column reached through those aliases becomes nullable. +func (c *Compiler) returningTables(qc *QueryCatalog, node ast.Node) ([]*Table, error) { + if c.conf.Engine != config.EnginePostgreSQL { + return nil, nil + } + + var rv *ast.RangeVar + var returning *ast.List + oldAlias, newAlias := "old", "new" + var oldNullable, newNullable bool + switch n := node.(type) { + case *ast.DeleteStmt: + rv = firstRangeVar(n.Relations) + returning = n.ReturningList + if n.ReturningOldAlias != "" { + oldAlias = n.ReturningOldAlias + } + if n.ReturningNewAlias != "" { + newAlias = n.ReturningNewAlias + } + // A deleted row has no new value + newNullable = true + case *ast.InsertStmt: + rv = n.Relation + returning = n.ReturningList + if n.ReturningOldAlias != "" { + oldAlias = n.ReturningOldAlias + } + if n.ReturningNewAlias != "" { + newAlias = n.ReturningNewAlias + } + // An inserted row has no old value, except when an ON CONFLICT + // clause updates an existing row instead + oldNullable = true + case *ast.UpdateStmt: + rv = firstRangeVar(n.Relations) + returning = n.ReturningList + if n.ReturningOldAlias != "" { + oldAlias = n.ReturningOldAlias + } + if n.ReturningNewAlias != "" { + newAlias = n.ReturningNewAlias + } + default: + return nil, nil + } + if rv == nil || returning == nil || len(returning.Items) == 0 { + return nil, nil + } + + fqn, err := ParseTableName(rv) + if err != nil { + return nil, err + } + + build := func(alias string, nullable bool) *Table { + table, err := qc.GetTable(fqn) + if err != nil { + // An unresolvable target table is reported by the regular + // source table lookup, so ignore the error here + return nil + } + table.Rel = &ast.TableName{Name: alias} + if nullable { + for _, col := range table.Columns { + col.NotNull = false + } + } + return table + } + + var tables []*Table + if t := build(oldAlias, oldNullable); t != nil { + tables = append(tables, t) + } + if t := build(newAlias, newNullable); t != nil { + tables = append(tables, t) + } + return tables, nil +} + +func firstRangeVar(list *ast.List) *ast.RangeVar { + if list == nil { + return nil + } + for _, item := range list.Items { + if rv, ok := item.(*ast.RangeVar); ok && rv != nil { + return rv + } + } + return nil +} + +// returningTableForScope returns the OLD or NEW virtual table named by scope. +// A source table with the same name shadows the virtual table, matching +// PostgreSQL, where the implicit aliases are only available when no relation +// in the query is already known under that name. +func returningTableForScope(tables, rtables []*Table, scope string) *Table { + if scope == "" { + return nil + } + for _, t := range tables { + if t.Rel.Name == scope { + return nil + } + } + for _, t := range rtables { + if t.Rel.Name == scope { + return t + } + } + return nil +} + +// tablesForRef resolves a column reference against the source tables, +// extended with the OLD or NEW virtual table when the reference is qualified +// with one of their names. +func tablesForRef(ref *ast.ColumnRef, tables, rtables []*Table) []*Table { + parts := stringSlice(ref.Fields) + if len(parts) != 2 { + return tables + } + if vt := returningTableForScope(tables, rtables, parts[0]); vt != nil { + return append(append([]*Table{}, tables...), vt) + } + return tables +} diff --git a/internal/endtoend/testdata/returning_old_new/postgresql/pgx/v5/exec.json b/internal/endtoend/testdata/returning_old_new/postgresql/pgx/v5/exec.json new file mode 100644 index 0000000000..2e996ca79d --- /dev/null +++ b/internal/endtoend/testdata/returning_old_new/postgresql/pgx/v5/exec.json @@ -0,0 +1,3 @@ +{ + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/returning_old_new/postgresql/pgx/v5/go/db.go b/internal/endtoend/testdata/returning_old_new/postgresql/pgx/v5/go/db.go new file mode 100644 index 0000000000..0057c62319 --- /dev/null +++ b/internal/endtoend/testdata/returning_old_new/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, ...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/returning_old_new/postgresql/pgx/v5/go/models.go b/internal/endtoend/testdata/returning_old_new/postgresql/pgx/v5/go/models.go new file mode 100644 index 0000000000..b2a355d80a --- /dev/null +++ b/internal/endtoend/testdata/returning_old_new/postgresql/pgx/v5/go/models.go @@ -0,0 +1,15 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "github.com/jackc/pgx/v5/pgtype" +) + +type User struct { + ID int64 + Name string + Bio pgtype.Text +} diff --git a/internal/endtoend/testdata/returning_old_new/postgresql/pgx/v5/go/query.sql.go b/internal/endtoend/testdata/returning_old_new/postgresql/pgx/v5/go/query.sql.go new file mode 100644 index 0000000000..1ef5f230f1 --- /dev/null +++ b/internal/endtoend/testdata/returning_old_new/postgresql/pgx/v5/go/query.sql.go @@ -0,0 +1,124 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const deleteReturningOldNew = `-- name: DeleteReturningOldNew :one +DELETE FROM users WHERE id = $1 +RETURNING old.name, new.name +` + +type DeleteReturningOldNewRow struct { + Name string + Name_2 pgtype.Text +} + +func (q *Queries) DeleteReturningOldNew(ctx context.Context, id int64) (DeleteReturningOldNewRow, error) { + row := q.db.QueryRow(ctx, deleteReturningOldNew, id) + var i DeleteReturningOldNewRow + err := row.Scan(&i.Name, &i.Name_2) + return i, err +} + +const insertReturningOldNew = `-- name: InsertReturningOldNew :one +INSERT INTO users (name) VALUES ($1) +RETURNING old.id, new.id +` + +type InsertReturningOldNewRow struct { + ID pgtype.Int8 + ID_2 int64 +} + +func (q *Queries) InsertReturningOldNew(ctx context.Context, name string) (InsertReturningOldNewRow, error) { + row := q.db.QueryRow(ctx, insertReturningOldNew, name) + var i InsertReturningOldNewRow + err := row.Scan(&i.ID, &i.ID_2) + return i, err +} + +const updateReturningOld = `-- name: UpdateReturningOld :one +UPDATE users SET name = $1 WHERE id = $2 +RETURNING old.name +` + +type UpdateReturningOldParams struct { + Name string + ID int64 +} + +func (q *Queries) UpdateReturningOld(ctx context.Context, arg UpdateReturningOldParams) (string, error) { + row := q.db.QueryRow(ctx, updateReturningOld, arg.Name, arg.ID) + var name string + err := row.Scan(&name) + return name, err +} + +const updateReturningOldNew = `-- name: UpdateReturningOldNew :one +UPDATE users SET name = $1 WHERE id = $2 +RETURNING old.name, new.name +` + +type UpdateReturningOldNewParams struct { + Name string + ID int64 +} + +type UpdateReturningOldNewRow struct { + Name string + Name_2 string +} + +func (q *Queries) UpdateReturningOldNew(ctx context.Context, arg UpdateReturningOldNewParams) (UpdateReturningOldNewRow, error) { + row := q.db.QueryRow(ctx, updateReturningOldNew, arg.Name, arg.ID) + var i UpdateReturningOldNewRow + err := row.Scan(&i.Name, &i.Name_2) + return i, err +} + +const updateReturningOldStar = `-- name: UpdateReturningOldStar :one +UPDATE users SET name = $1 WHERE id = $2 +RETURNING old.id, old.name, old.bio +` + +type UpdateReturningOldStarParams struct { + Name string + ID int64 +} + +func (q *Queries) UpdateReturningOldStar(ctx context.Context, arg UpdateReturningOldStarParams) (User, error) { + row := q.db.QueryRow(ctx, updateReturningOldStar, arg.Name, arg.ID) + var i User + err := row.Scan(&i.ID, &i.Name, &i.Bio) + return i, err +} + +const updateReturningWithAliases = `-- name: UpdateReturningWithAliases :one +UPDATE users SET name = $1 WHERE id = $2 +RETURNING WITH (OLD AS o, NEW AS n) o.name, n.name +` + +type UpdateReturningWithAliasesParams struct { + Name string + ID int64 +} + +type UpdateReturningWithAliasesRow struct { + Name string + Name_2 string +} + +func (q *Queries) UpdateReturningWithAliases(ctx context.Context, arg UpdateReturningWithAliasesParams) (UpdateReturningWithAliasesRow, error) { + row := q.db.QueryRow(ctx, updateReturningWithAliases, arg.Name, arg.ID) + var i UpdateReturningWithAliasesRow + err := row.Scan(&i.Name, &i.Name_2) + return i, err +} diff --git a/internal/endtoend/testdata/returning_old_new/postgresql/pgx/v5/query.sql b/internal/endtoend/testdata/returning_old_new/postgresql/pgx/v5/query.sql new file mode 100644 index 0000000000..ca465ee980 --- /dev/null +++ b/internal/endtoend/testdata/returning_old_new/postgresql/pgx/v5/query.sql @@ -0,0 +1,23 @@ +-- name: UpdateReturningOld :one +UPDATE users SET name = $1 WHERE id = $2 +RETURNING old.name; + +-- name: UpdateReturningOldNew :one +UPDATE users SET name = $1 WHERE id = $2 +RETURNING old.name, new.name; + +-- name: UpdateReturningOldStar :one +UPDATE users SET name = $1 WHERE id = $2 +RETURNING old.*; + +-- name: InsertReturningOldNew :one +INSERT INTO users (name) VALUES ($1) +RETURNING old.id, new.id; + +-- name: DeleteReturningOldNew :one +DELETE FROM users WHERE id = $1 +RETURNING old.name, new.name; + +-- name: UpdateReturningWithAliases :one +UPDATE users SET name = $1 WHERE id = $2 +RETURNING WITH (OLD AS o, NEW AS n) o.name, n.name; diff --git a/internal/endtoend/testdata/returning_old_new/postgresql/pgx/v5/schema.sql b/internal/endtoend/testdata/returning_old_new/postgresql/pgx/v5/schema.sql new file mode 100644 index 0000000000..ad2d63a4f9 --- /dev/null +++ b/internal/endtoend/testdata/returning_old_new/postgresql/pgx/v5/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE users ( + id bigserial PRIMARY KEY, + name text NOT NULL, + bio text +); diff --git a/internal/endtoend/testdata/returning_old_new/postgresql/pgx/v5/sqlc.json b/internal/endtoend/testdata/returning_old_new/postgresql/pgx/v5/sqlc.json new file mode 100644 index 0000000000..32ede07158 --- /dev/null +++ b/internal/endtoend/testdata/returning_old_new/postgresql/pgx/v5/sqlc.json @@ -0,0 +1,13 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "postgresql", + "sql_package": "pgx/v5", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/returning_old_new/postgresql/stdlib/exec.json b/internal/endtoend/testdata/returning_old_new/postgresql/stdlib/exec.json new file mode 100644 index 0000000000..2e996ca79d --- /dev/null +++ b/internal/endtoend/testdata/returning_old_new/postgresql/stdlib/exec.json @@ -0,0 +1,3 @@ +{ + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/returning_old_new/postgresql/stdlib/go/db.go b/internal/endtoend/testdata/returning_old_new/postgresql/stdlib/go/db.go new file mode 100644 index 0000000000..80dd6ab1f6 --- /dev/null +++ b/internal/endtoend/testdata/returning_old_new/postgresql/stdlib/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/returning_old_new/postgresql/stdlib/go/models.go b/internal/endtoend/testdata/returning_old_new/postgresql/stdlib/go/models.go new file mode 100644 index 0000000000..b5f44f2b8b --- /dev/null +++ b/internal/endtoend/testdata/returning_old_new/postgresql/stdlib/go/models.go @@ -0,0 +1,15 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "database/sql" +) + +type User struct { + ID int64 + Name string + Bio sql.NullString +} diff --git a/internal/endtoend/testdata/returning_old_new/postgresql/stdlib/go/query.sql.go b/internal/endtoend/testdata/returning_old_new/postgresql/stdlib/go/query.sql.go new file mode 100644 index 0000000000..ba3139ee00 --- /dev/null +++ b/internal/endtoend/testdata/returning_old_new/postgresql/stdlib/go/query.sql.go @@ -0,0 +1,123 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const deleteReturningOldNew = `-- name: DeleteReturningOldNew :one +DELETE FROM users WHERE id = $1 +RETURNING old.name, new.name +` + +type DeleteReturningOldNewRow struct { + Name string + Name_2 sql.NullString +} + +func (q *Queries) DeleteReturningOldNew(ctx context.Context, id int64) (DeleteReturningOldNewRow, error) { + row := q.db.QueryRowContext(ctx, deleteReturningOldNew, id) + var i DeleteReturningOldNewRow + err := row.Scan(&i.Name, &i.Name_2) + return i, err +} + +const insertReturningOldNew = `-- name: InsertReturningOldNew :one +INSERT INTO users (name) VALUES ($1) +RETURNING old.id, new.id +` + +type InsertReturningOldNewRow struct { + ID sql.NullInt64 + ID_2 int64 +} + +func (q *Queries) InsertReturningOldNew(ctx context.Context, name string) (InsertReturningOldNewRow, error) { + row := q.db.QueryRowContext(ctx, insertReturningOldNew, name) + var i InsertReturningOldNewRow + err := row.Scan(&i.ID, &i.ID_2) + return i, err +} + +const updateReturningOld = `-- name: UpdateReturningOld :one +UPDATE users SET name = $1 WHERE id = $2 +RETURNING old.name +` + +type UpdateReturningOldParams struct { + Name string + ID int64 +} + +func (q *Queries) UpdateReturningOld(ctx context.Context, arg UpdateReturningOldParams) (string, error) { + row := q.db.QueryRowContext(ctx, updateReturningOld, arg.Name, arg.ID) + var name string + err := row.Scan(&name) + return name, err +} + +const updateReturningOldNew = `-- name: UpdateReturningOldNew :one +UPDATE users SET name = $1 WHERE id = $2 +RETURNING old.name, new.name +` + +type UpdateReturningOldNewParams struct { + Name string + ID int64 +} + +type UpdateReturningOldNewRow struct { + Name string + Name_2 string +} + +func (q *Queries) UpdateReturningOldNew(ctx context.Context, arg UpdateReturningOldNewParams) (UpdateReturningOldNewRow, error) { + row := q.db.QueryRowContext(ctx, updateReturningOldNew, arg.Name, arg.ID) + var i UpdateReturningOldNewRow + err := row.Scan(&i.Name, &i.Name_2) + return i, err +} + +const updateReturningOldStar = `-- name: UpdateReturningOldStar :one +UPDATE users SET name = $1 WHERE id = $2 +RETURNING old.id, old.name, old.bio +` + +type UpdateReturningOldStarParams struct { + Name string + ID int64 +} + +func (q *Queries) UpdateReturningOldStar(ctx context.Context, arg UpdateReturningOldStarParams) (User, error) { + row := q.db.QueryRowContext(ctx, updateReturningOldStar, arg.Name, arg.ID) + var i User + err := row.Scan(&i.ID, &i.Name, &i.Bio) + return i, err +} + +const updateReturningWithAliases = `-- name: UpdateReturningWithAliases :one +UPDATE users SET name = $1 WHERE id = $2 +RETURNING WITH (OLD AS o, NEW AS n) o.name, n.name +` + +type UpdateReturningWithAliasesParams struct { + Name string + ID int64 +} + +type UpdateReturningWithAliasesRow struct { + Name string + Name_2 string +} + +func (q *Queries) UpdateReturningWithAliases(ctx context.Context, arg UpdateReturningWithAliasesParams) (UpdateReturningWithAliasesRow, error) { + row := q.db.QueryRowContext(ctx, updateReturningWithAliases, arg.Name, arg.ID) + var i UpdateReturningWithAliasesRow + err := row.Scan(&i.Name, &i.Name_2) + return i, err +} diff --git a/internal/endtoend/testdata/returning_old_new/postgresql/stdlib/query.sql b/internal/endtoend/testdata/returning_old_new/postgresql/stdlib/query.sql new file mode 100644 index 0000000000..ca465ee980 --- /dev/null +++ b/internal/endtoend/testdata/returning_old_new/postgresql/stdlib/query.sql @@ -0,0 +1,23 @@ +-- name: UpdateReturningOld :one +UPDATE users SET name = $1 WHERE id = $2 +RETURNING old.name; + +-- name: UpdateReturningOldNew :one +UPDATE users SET name = $1 WHERE id = $2 +RETURNING old.name, new.name; + +-- name: UpdateReturningOldStar :one +UPDATE users SET name = $1 WHERE id = $2 +RETURNING old.*; + +-- name: InsertReturningOldNew :one +INSERT INTO users (name) VALUES ($1) +RETURNING old.id, new.id; + +-- name: DeleteReturningOldNew :one +DELETE FROM users WHERE id = $1 +RETURNING old.name, new.name; + +-- name: UpdateReturningWithAliases :one +UPDATE users SET name = $1 WHERE id = $2 +RETURNING WITH (OLD AS o, NEW AS n) o.name, n.name; diff --git a/internal/endtoend/testdata/returning_old_new/postgresql/stdlib/schema.sql b/internal/endtoend/testdata/returning_old_new/postgresql/stdlib/schema.sql new file mode 100644 index 0000000000..ad2d63a4f9 --- /dev/null +++ b/internal/endtoend/testdata/returning_old_new/postgresql/stdlib/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE users ( + id bigserial PRIMARY KEY, + name text NOT NULL, + bio text +); diff --git a/internal/endtoend/testdata/returning_old_new/postgresql/stdlib/sqlc.json b/internal/endtoend/testdata/returning_old_new/postgresql/stdlib/sqlc.json new file mode 100644 index 0000000000..f717ca2e66 --- /dev/null +++ b/internal/endtoend/testdata/returning_old_new/postgresql/stdlib/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "postgresql", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/engine/postgresql/convert.go b/internal/engine/postgresql/convert.go index 6a71c609dc..b1f746d193 100644 --- a/internal/engine/postgresql/convert.go +++ b/internal/engine/postgresql/convert.go @@ -1418,15 +1418,36 @@ func convertDeleteStmt(n *pg.DeleteStmt) *ast.DeleteStmt { if n == nil { return nil } + oldAlias, newAlias := convertReturningOptions(n.ReturningClause) return &ast.DeleteStmt{ Relations: &ast.List{ Items: []ast.Node{convertRangeVar(n.Relation)}, }, - UsingClause: convertSlice(n.UsingClause), - WhereClause: convertNode(n.WhereClause), - ReturningList: convertSlice(n.ReturningClause.GetExprs()), - WithClause: convertWithClause(n.WithClause), + UsingClause: convertSlice(n.UsingClause), + WhereClause: convertNode(n.WhereClause), + ReturningList: convertSlice(n.ReturningClause.GetExprs()), + ReturningOldAlias: oldAlias, + ReturningNewAlias: newAlias, + WithClause: convertWithClause(n.WithClause), + } +} + +// convertReturningOptions extracts the aliases assigned to the OLD and NEW +// rows by a PostgreSQL 18 RETURNING WITH (...) option list. +func convertReturningOptions(n *pg.ReturningClause) (oldAlias, newAlias string) { + for _, option := range n.GetOptions() { + ro := option.GetReturningOption() + if ro == nil { + continue + } + switch ro.Option { + case pg.ReturningOptionKind_RETURNING_OPTION_OLD: + oldAlias = ro.Value + case pg.ReturningOptionKind_RETURNING_OPTION_NEW: + newAlias = ro.Value + } } + return oldAlias, newAlias } func convertDiscardStmt(n *pg.DiscardStmt) *ast.DiscardStmt { @@ -1801,14 +1822,17 @@ func convertInsertStmt(n *pg.InsertStmt) *ast.InsertStmt { if n == nil { return nil } + oldAlias, newAlias := convertReturningOptions(n.ReturningClause) return &ast.InsertStmt{ - Relation: convertRangeVar(n.Relation), - Cols: convertSlice(n.Cols), - SelectStmt: convertNode(n.SelectStmt), - OnConflictClause: convertOnConflictClause(n.OnConflictClause), - ReturningList: convertSlice(n.ReturningClause.GetExprs()), - WithClause: convertWithClause(n.WithClause), - Override: ast.OverridingKind(n.Override), + Relation: convertRangeVar(n.Relation), + Cols: convertSlice(n.Cols), + SelectStmt: convertNode(n.SelectStmt), + OnConflictClause: convertOnConflictClause(n.OnConflictClause), + ReturningList: convertSlice(n.ReturningClause.GetExprs()), + ReturningOldAlias: oldAlias, + ReturningNewAlias: newAlias, + WithClause: convertWithClause(n.WithClause), + Override: ast.OverridingKind(n.Override), } } @@ -2805,15 +2829,18 @@ func convertUpdateStmt(n *pg.UpdateStmt) *ast.UpdateStmt { return nil } + oldAlias, newAlias := convertReturningOptions(n.ReturningClause) return &ast.UpdateStmt{ Relations: &ast.List{ Items: []ast.Node{convertRangeVar(n.Relation)}, }, - TargetList: convertSlice(n.TargetList), - WhereClause: convertNode(n.WhereClause), - FromClause: convertSlice(n.FromClause), - ReturningList: convertSlice(n.ReturningClause.GetExprs()), - WithClause: convertWithClause(n.WithClause), + TargetList: convertSlice(n.TargetList), + WhereClause: convertNode(n.WhereClause), + FromClause: convertSlice(n.FromClause), + ReturningList: convertSlice(n.ReturningClause.GetExprs()), + ReturningOldAlias: oldAlias, + ReturningNewAlias: newAlias, + WithClause: convertWithClause(n.WithClause), } } diff --git a/internal/sql/ast/delete_stmt.go b/internal/sql/ast/delete_stmt.go index d23617881a..74683f20ba 100644 --- a/internal/sql/ast/delete_stmt.go +++ b/internal/sql/ast/delete_stmt.go @@ -12,6 +12,9 @@ type DeleteStmt struct { // MySQL multi-table DELETE support Targets *List // Tables to delete from (e.g., jt.*, pt.*) FromClause Node // FROM clause with JOINs (Node to support JoinExpr) + // PostgreSQL 18 RETURNING WITH (OLD AS ..., NEW AS ...) aliases + ReturningOldAlias string + ReturningNewAlias string } func (n *DeleteStmt) Pos() int { @@ -63,6 +66,7 @@ func (n *DeleteStmt) Format(buf *TrackedBuffer, d format.Dialect) { if items(n.ReturningList) { buf.WriteString(" RETURNING ") + formatReturningOptions(buf, d, n.ReturningOldAlias, n.ReturningNewAlias) buf.astFormat(n.ReturningList, d) } } diff --git a/internal/sql/ast/insert_stmt.go b/internal/sql/ast/insert_stmt.go index 4d5c8d1df2..6568deb328 100644 --- a/internal/sql/ast/insert_stmt.go +++ b/internal/sql/ast/insert_stmt.go @@ -12,6 +12,9 @@ type InsertStmt struct { WithClause *WithClause Override OverridingKind DefaultValues bool // SQLite-specific: INSERT INTO ... DEFAULT VALUES + // PostgreSQL 18 RETURNING WITH (OLD AS ..., NEW AS ...) aliases + ReturningOldAlias string + ReturningNewAlias string } func (n *InsertStmt) Pos() int { @@ -57,6 +60,7 @@ func (n *InsertStmt) Format(buf *TrackedBuffer, d format.Dialect) { if items(n.ReturningList) { buf.WriteString(" RETURNING ") + formatReturningOptions(buf, d, n.ReturningOldAlias, n.ReturningNewAlias) buf.astFormat(n.ReturningList, d) } } diff --git a/internal/sql/ast/returning.go b/internal/sql/ast/returning.go new file mode 100644 index 0000000000..70de843415 --- /dev/null +++ b/internal/sql/ast/returning.go @@ -0,0 +1,24 @@ +package ast + +import "github.com/sqlc-dev/sqlc/internal/sql/format" + +// formatReturningOptions writes the PostgreSQL 18 RETURNING WITH (...) option +// list that renames the OLD and NEW aliases available in a RETURNING clause. +func formatReturningOptions(buf *TrackedBuffer, d format.Dialect, oldAlias, newAlias string) { + if oldAlias == "" && newAlias == "" { + return + } + buf.WriteString("WITH (") + if oldAlias != "" { + buf.WriteString("OLD AS ") + buf.WriteString(d.QuoteIdent(oldAlias)) + } + if newAlias != "" { + if oldAlias != "" { + buf.WriteString(", ") + } + buf.WriteString("NEW AS ") + buf.WriteString(d.QuoteIdent(newAlias)) + } + buf.WriteString(") ") +} diff --git a/internal/sql/ast/update_stmt.go b/internal/sql/ast/update_stmt.go index 5376a8c6ce..54382b3f43 100644 --- a/internal/sql/ast/update_stmt.go +++ b/internal/sql/ast/update_stmt.go @@ -14,6 +14,9 @@ type UpdateStmt struct { LimitCount Node ReturningList *List WithClause *WithClause + // PostgreSQL 18 RETURNING WITH (OLD AS ..., NEW AS ...) aliases + ReturningOldAlias string + ReturningNewAlias string } func (n *UpdateStmt) Pos() int { @@ -117,6 +120,7 @@ func (n *UpdateStmt) Format(buf *TrackedBuffer, d format.Dialect) { if items(n.ReturningList) { buf.WriteString(" RETURNING ") + formatReturningOptions(buf, d, n.ReturningOldAlias, n.ReturningNewAlias) buf.astFormat(n.ReturningList, d) } }