diff --git a/docs/howto/merge.md b/docs/howto/merge.md new file mode 100644 index 0000000000..b0f9161a6c --- /dev/null +++ b/docs/howto/merge.md @@ -0,0 +1,77 @@ +# Merging rows + +sqlc supports PostgreSQL's [`MERGE`](https://www.postgresql.org/docs/current/sql-merge.html) +statement (PostgreSQL 15+). Parameters are inferred in every clause: the source +subquery, the join condition, `WHEN` conditions, `UPDATE SET` assignments, and +`INSERT ... VALUES` lists. + +```sql +CREATE TABLE inventory ( + sku text PRIMARY KEY, + quantity integer NOT NULL, + updated_by text +); + +CREATE TABLE inventory_updates ( + sku text PRIMARY KEY, + delta integer NOT NULL +); +``` + +```sql +-- name: SyncInventory :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED AND t.quantity + u.delta <= $1 THEN + DELETE +WHEN MATCHED THEN + UPDATE SET quantity = t.quantity + u.delta, updated_by = $2 +WHEN NOT MATCHED THEN + INSERT (sku, quantity, updated_by) VALUES (u.sku, u.delta, $2); +``` + +```go +type SyncInventoryParams struct { + Quantity int32 + UpdatedBy sql.NullString +} + +func (q *Queries) SyncInventory(ctx context.Context, arg SyncInventoryParams) error { + _, err := q.db.ExecContext(ctx, syncInventory, arg.Quantity, arg.UpdatedBy) + return err +} +``` + +## Returning merged rows + +On PostgreSQL 17 and later, `MERGE` supports a `RETURNING` clause, which can be +used with `:one` or `:many`. The clause can reference columns from both the +target and the source relations. + +```sql +-- name: MergeInventory :many +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET quantity = u.delta +WHEN NOT MATCHED THEN + INSERT (sku, quantity) VALUES (u.sku, u.delta) +RETURNING t.sku, t.quantity; +``` + +```go +type MergeInventoryRow struct { + Sku string + Quantity int32 +} + +func (q *Queries) MergeInventory(ctx context.Context) ([]MergeInventoryRow, error) { + // ... +} +``` + +Note that sqlc does not validate the server version: generating code for a +`MERGE ... RETURNING` query succeeds, but running it against a server older +than PostgreSQL 17 returns a syntax error at execution time. diff --git a/docs/index.rst b/docs/index.rst index f11c8903d5..b11b8018e6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -58,6 +58,7 @@ code ever again. howto/insert.md howto/update.md howto/delete.md + howto/merge.md howto/prepared_query.md howto/transactions.md diff --git a/internal/compiler/expand.go b/internal/compiler/expand.go index c60b7618b2..3eb4728d3e 100644 --- a/internal/compiler/expand.go +++ b/internal/compiler/expand.go @@ -24,6 +24,7 @@ func (c *Compiler) expand(qc *QueryCatalog, raw *ast.RawStmt) ([]source.Edit, er switch node.(type) { case *ast.DeleteStmt: case *ast.InsertStmt: + case *ast.MergeStmt: case *ast.SelectStmt: case *ast.UpdateStmt: default: @@ -90,6 +91,8 @@ func (c *Compiler) expandStmt(qc *QueryCatalog, raw *ast.RawStmt, node ast.Node) targets = n.ReturningList case *ast.InsertStmt: targets = n.ReturningList + case *ast.MergeStmt: + targets = n.ReturningList case *ast.SelectStmt: targets = n.TargetList case *ast.UpdateStmt: diff --git a/internal/compiler/find_params.go b/internal/compiler/find_params.go index 8199addd33..f519de69ea 100644 --- a/internal/compiler/find_params.go +++ b/internal/compiler/find_params.go @@ -7,6 +7,30 @@ import ( "github.com/sqlc-dev/sqlc/internal/sql/astutils" ) +// paramRefForAssignment returns the ParamRef assigned to a single UPDATE SET +// target, handling both `col = $1` and the multi-column form +// `(a, b) = ($1, $2)`, where each column is a separate ResTarget carrying a +// MultiAssignRef with its own Colno pointing into a shared RowExpr source. +func paramRefForAssignment(target *ast.ResTarget) (*ast.ParamRef, bool) { + switch val := target.Val.(type) { + case *ast.ParamRef: + return val, true + case *ast.MultiAssignRef: + row, ok := val.Source.(*ast.RowExpr) + if !ok || row.Args == nil { + return nil, false + } + idx := val.Colno - 1 + if idx < 0 || idx >= len(row.Args.Items) { + return nil, false + } + if ref, ok := row.Args.Items[idx].(*ast.ParamRef); ok { + return ref, true + } + } + return nil, false +} + func findParameters(root ast.Node) ([]paramRef, []error) { refs := make([]paramRef, 0) errors := make([]error, 0) @@ -110,6 +134,66 @@ func (p paramSearch) Visit(node ast.Node) astutils.Visitor { } } + case *ast.MergeStmt: + if n.MergeWhenClauses == nil { + break + } + for _, item := range n.MergeWhenClauses.Items { + clause, ok := item.(*ast.MergeWhenClause) + if !ok { + continue + } + switch clause.CommandType { + case ast.CmdTypeUpdate: + // WHEN MATCHED THEN UPDATE SET col = $1 + if clause.TargetList == nil { + continue + } + for _, item := range clause.TargetList.Items { + target, ok := item.(*ast.ResTarget) + if !ok { + continue + } + ref, ok := paramRefForAssignment(target) + if !ok { + continue + } + *p.refs = append(*p.refs, paramRef{parent: target, ref: ref, rv: n.Relation}) + p.seen[ref.Location] = struct{}{} + } + case ast.CmdTypeInsert: + // WHEN NOT MATCHED THEN INSERT (a, b) VALUES ($1, $2) + if clause.Values == nil { + continue + } + if clause.TargetList == nil || len(clause.TargetList.Items) == 0 { + // Without an explicit column list sqlc cannot map the + // positional VALUES parameters to target columns. + params := astutils.Search(clause.Values, func(node ast.Node) bool { + _, ok := node.(*ast.ParamRef) + return ok + }) + if len(params.Items) > 0 { + *p.errs = append(*p.errs, fmt.Errorf("MERGE INSERT with parameters requires an explicit column list")) + return p + } + continue + } + for i, v := range clause.Values.Items { + ref, ok := v.(*ast.ParamRef) + if !ok { + continue + } + if len(clause.TargetList.Items) <= i { + *p.errs = append(*p.errs, fmt.Errorf("MERGE INSERT has more expressions than target columns")) + return p + } + *p.refs = append(*p.refs, paramRef{parent: clause.TargetList.Items[i], ref: ref, rv: n.Relation}) + p.seen[ref.Location] = struct{}{} + } + } + } + case *ast.UpdateStmt: for _, item := range n.TargetList.Items { target, ok := item.(*ast.ResTarget) diff --git a/internal/compiler/output_columns.go b/internal/compiler/output_columns.go index 6cc2567ebe..7a7486bd7c 100644 --- a/internal/compiler/output_columns.go +++ b/internal/compiler/output_columns.go @@ -63,6 +63,8 @@ func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, er targets = n.ReturningList case *ast.InsertStmt: targets = n.ReturningList + case *ast.MergeStmt: + targets = n.ReturningList case *ast.SelectStmt: targets = n.TargetList isUnion := len(targets.Items) == 0 && n.Larg != nil @@ -404,9 +406,73 @@ func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, er } } + if m, ok := node.(*ast.MergeStmt); ok && mergeHasNotMatchedBySource(m) { + // A WHEN NOT MATCHED BY SOURCE action returns NULL for every column of + // the source relation (there is no matching source row), so any source + // column in the RETURNING list is nullable. The target is always + // m.Relation, so treat every provenance-bearing column that is not the + // target as a source column (this covers subquery sources too). + for _, col := range cols { + if !col.NotNull || col.skipTableRequiredCheck { + continue + } + // Columns without table provenance (e.g. merge_action()) are never + // NULL from a NOT MATCHED BY SOURCE action. + if col.Table == nil && col.TableAlias == "" { + continue + } + if !columnIsMergeTarget(col, m.Relation) { + col.NotNull = false + } + } + } + return cols, nil } +// mergeHasNotMatchedBySource reports whether the MERGE has a WHEN NOT MATCHED +// BY SOURCE clause, which makes source-relation columns nullable in RETURNING. +func mergeHasNotMatchedBySource(m *ast.MergeStmt) bool { + if m.MergeWhenClauses == nil { + return false + } + for _, item := range m.MergeWhenClauses.Items { + if clause, ok := item.(*ast.MergeWhenClause); ok { + if clause.MatchKind == ast.MergeWhenNotMatchedBySource { + return true + } + } + } + return false +} + +// columnIsMergeTarget reports whether col is qualified as (or resolves to) the +// MERGE target relation. +func columnIsMergeTarget(col *Column, target *ast.RangeVar) bool { + if target == nil { + return false + } + var name, alias string + if target.Relname != nil { + name = *target.Relname + } + if target.Alias != nil && target.Alias.Aliasname != nil { + alias = *target.Alias.Aliasname + } + if col.TableAlias != "" { + // Qualified: it is the target only if the qualifier is the target's + // alias (or, when the target is unaliased, its table name). + if alias != "" { + return col.TableAlias == alias + } + return col.TableAlias == name + } + if col.Table != nil && name != "" { + return col.Table.Name == name + } + return false +} + const ( tableNotFound = iota tableRequired @@ -494,6 +560,19 @@ func (c *Compiler) sourceTables(qc *QueryCatalog, node ast.Node) ([]*Table, erro list = &ast.List{ Items: []ast.Node{n.Relation}, } + case *ast.MergeStmt: + // PostgreSQL expands MERGE ... RETURNING * to all source columns + // followed by all target columns, so collect the source relation + // before the target relation. + // https://www.postgresql.org/docs/17/sql-merge.html + var tv tableVisitor + if n.SourceRelation != nil { + astutils.Walk(&tv, n.SourceRelation) + } + if n.Relation != nil { + astutils.Walk(&tv, n.Relation) + } + list = &tv.list case *ast.SelectStmt: var tv tableVisitor astutils.Walk(&tv, n.FromClause) diff --git a/internal/compiler/query_catalog.go b/internal/compiler/query_catalog.go index 80b59d876c..163baf7784 100644 --- a/internal/compiler/query_catalog.go +++ b/internal/compiler/query_catalog.go @@ -21,6 +21,8 @@ func (comp *Compiler) buildQueryCatalog(c *catalog.Catalog, node ast.Node, embed with = n.WithClause case *ast.InsertStmt: with = n.WithClause + case *ast.MergeStmt: + with = n.WithClause case *ast.UpdateStmt: with = n.WithClause case *ast.SelectStmt: diff --git a/internal/endtoend/testdata/merge/postgresql/parse/exec.json b/internal/endtoend/testdata/merge/postgresql/parse/exec.json new file mode 100644 index 0000000000..0a75ff458d --- /dev/null +++ b/internal/endtoend/testdata/merge/postgresql/parse/exec.json @@ -0,0 +1,5 @@ +{ + "command": "parse", + "args": ["--dialect", "postgresql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/merge/postgresql/parse/query.sql b/internal/endtoend/testdata/merge/postgresql/parse/query.sql new file mode 100644 index 0000000000..5df9c19411 --- /dev/null +++ b/internal/endtoend/testdata/merge/postgresql/parse/query.sql @@ -0,0 +1,2 @@ +-- name: SyncInventory :exec +MERGE INTO inventory AS t USING inventory_updates AS u ON t.sku = u.sku WHEN MATCHED AND t.quantity <= $1 THEN DELETE WHEN MATCHED THEN UPDATE SET quantity = u.delta WHEN NOT MATCHED THEN INSERT (sku, quantity) VALUES (u.sku, $2); diff --git a/internal/endtoend/testdata/merge/postgresql/parse/stdout.txt b/internal/endtoend/testdata/merge/postgresql/parse/stdout.txt new file mode 100644 index 0000000000..fbdbb06a0e --- /dev/null +++ b/internal/endtoend/testdata/merge/postgresql/parse/stdout.txt @@ -0,0 +1,211 @@ +[ + { + "name": "SyncInventory", + "cmd": ":exec", + "ast": { + "Stmt": { + "Relation": { + "Catalogname": null, + "Schemaname": null, + "Relname": "inventory", + "Inh": true, + "Relpersistence": 112, + "Alias": { + "Aliasname": "t", + "Colnames": { + "Items": null + } + }, + "Location": 40 + }, + "SourceRelation": { + "Catalogname": null, + "Schemaname": null, + "Relname": "inventory_updates", + "Inh": true, + "Relpersistence": 112, + "Alias": { + "Aliasname": "u", + "Colnames": { + "Items": null + } + }, + "Location": 61 + }, + "JoinCondition": { + "Kind": 1, + "Name": { + "Items": [ + { + "Str": "=" + } + ] + }, + "Lexpr": { + "Name": "", + "Fields": { + "Items": [ + { + "Str": "t" + }, + { + "Str": "sku" + } + ] + }, + "Location": 87 + }, + "Rexpr": { + "Name": "", + "Fields": { + "Items": [ + { + "Str": "u" + }, + { + "Str": "sku" + } + ] + }, + "Location": 95 + }, + "Location": 93 + }, + "MergeWhenClauses": { + "Items": [ + { + "MatchKind": 1, + "CommandType": 5, + "Override": 1, + "Condition": { + "Kind": 1, + "Name": { + "Items": [ + { + "Str": "\u003c=" + } + ] + }, + "Lexpr": { + "Name": "", + "Fields": { + "Items": [ + { + "Str": "t" + }, + { + "Str": "quantity" + } + ] + }, + "Location": 118 + }, + "Rexpr": { + "Number": 1, + "Location": 132, + "Dollar": true + }, + "Location": 129 + }, + "TargetList": { + "Items": null + }, + "Values": { + "Items": null + } + }, + { + "MatchKind": 1, + "CommandType": 3, + "Override": 1, + "Condition": {}, + "TargetList": { + "Items": [ + { + "Name": "quantity", + "Indirection": { + "Items": null + }, + "Val": { + "Name": "", + "Fields": { + "Items": [ + { + "Str": "u" + }, + { + "Str": "delta" + } + ] + }, + "Location": 187 + }, + "Location": 176 + } + ] + }, + "Values": { + "Items": null + } + }, + { + "MatchKind": 3, + "CommandType": 4, + "Override": 1, + "Condition": {}, + "TargetList": { + "Items": [ + { + "Name": "sku", + "Indirection": { + "Items": null + }, + "Val": {}, + "Location": 225 + }, + { + "Name": "quantity", + "Indirection": { + "Items": null + }, + "Val": {}, + "Location": 230 + } + ] + }, + "Values": { + "Items": [ + { + "Name": "", + "Fields": { + "Items": [ + { + "Str": "u" + }, + { + "Str": "sku" + } + ] + }, + "Location": 248 + }, + { + "Number": 2, + "Location": 255, + "Dollar": true + } + ] + } + } + ] + }, + "ReturningList": { + "Items": null + }, + "WithClause": null + }, + "StmtLocation": 0, + "StmtLen": 258 + } + } +] diff --git a/internal/endtoend/testdata/merge/postgresql/pgx/v4/go/db.go b/internal/endtoend/testdata/merge/postgresql/pgx/v4/go/db.go new file mode 100644 index 0000000000..9da6d9a49f --- /dev/null +++ b/internal/endtoend/testdata/merge/postgresql/pgx/v4/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/pgconn" + "github.com/jackc/pgx/v4" +) + +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/merge/postgresql/pgx/v4/go/models.go b/internal/endtoend/testdata/merge/postgresql/pgx/v4/go/models.go new file mode 100644 index 0000000000..4be31ab1ec --- /dev/null +++ b/internal/endtoend/testdata/merge/postgresql/pgx/v4/go/models.go @@ -0,0 +1,20 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "database/sql" +) + +type Inventory struct { + Sku string + Quantity int32 + UpdatedBy sql.NullString +} + +type InventoryUpdate struct { + Sku string + Delta int32 +} diff --git a/internal/endtoend/testdata/merge/postgresql/pgx/v4/go/query.sql.go b/internal/endtoend/testdata/merge/postgresql/pgx/v4/go/query.sql.go new file mode 100644 index 0000000000..7c2e043a1e --- /dev/null +++ b/internal/endtoend/testdata/merge/postgresql/pgx/v4/go/query.sql.go @@ -0,0 +1,105 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const mergeDelete = `-- name: MergeDelete :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + DELETE +` + +func (q *Queries) MergeDelete(ctx context.Context) error { + _, err := q.db.Exec(ctx, mergeDelete) + return err +} + +const mergeFromSubquery = `-- name: MergeFromSubquery :exec +MERGE INTO inventory AS t +USING (SELECT sku, delta FROM inventory_updates WHERE delta > $1) AS u +ON t.sku = u.sku AND t.quantity > $2 +WHEN MATCHED THEN + UPDATE SET quantity = $3 +WHEN NOT MATCHED THEN + INSERT (sku, quantity) VALUES (u.sku, $3) +` + +type MergeFromSubqueryParams struct { + Delta int32 + Quantity int32 + Quantity_2 int32 +} + +func (q *Queries) MergeFromSubquery(ctx context.Context, arg MergeFromSubqueryParams) error { + _, err := q.db.Exec(ctx, mergeFromSubquery, arg.Delta, arg.Quantity, arg.Quantity_2) + return err +} + +const mergeMultiSet = `-- name: MergeMultiSet :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET (quantity, updated_by) = ($1, $2) +` + +type MergeMultiSetParams struct { + Quantity int32 + UpdatedBy sql.NullString +} + +func (q *Queries) MergeMultiSet(ctx context.Context, arg MergeMultiSetParams) error { + _, err := q.db.Exec(ctx, mergeMultiSet, arg.Quantity, arg.UpdatedBy) + return err +} + +const mergeNamedParams = `-- name: MergeNamedParams :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET updated_by = $1 +WHEN NOT MATCHED THEN + INSERT (sku, quantity, updated_by) VALUES (u.sku, $2, $1) +` + +type MergeNamedParamsParams struct { + UpdatedBy sql.NullString + StartQty int32 +} + +func (q *Queries) MergeNamedParams(ctx context.Context, arg MergeNamedParamsParams) error { + _, err := q.db.Exec(ctx, mergeNamedParams, arg.UpdatedBy, arg.StartQty) + return err +} + +const syncInventory = `-- name: SyncInventory :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED AND t.quantity + u.delta <= $1 THEN + DELETE +WHEN MATCHED THEN + UPDATE SET quantity = t.quantity + u.delta, updated_by = $2 +WHEN NOT MATCHED THEN + INSERT (sku, quantity, updated_by) VALUES (u.sku, u.delta, $2) +` + +type SyncInventoryParams struct { + Quantity int32 + UpdatedBy sql.NullString +} + +func (q *Queries) SyncInventory(ctx context.Context, arg SyncInventoryParams) error { + _, err := q.db.Exec(ctx, syncInventory, arg.Quantity, arg.UpdatedBy) + return err +} diff --git a/internal/endtoend/testdata/merge/postgresql/pgx/v4/query.sql b/internal/endtoend/testdata/merge/postgresql/pgx/v4/query.sql new file mode 100644 index 0000000000..93607f0533 --- /dev/null +++ b/internal/endtoend/testdata/merge/postgresql/pgx/v4/query.sql @@ -0,0 +1,42 @@ +-- name: SyncInventory :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED AND t.quantity + u.delta <= $1 THEN + DELETE +WHEN MATCHED THEN + UPDATE SET quantity = t.quantity + u.delta, updated_by = $2 +WHEN NOT MATCHED THEN + INSERT (sku, quantity, updated_by) VALUES (u.sku, u.delta, $2); + +-- name: MergeFromSubquery :exec +MERGE INTO inventory AS t +USING (SELECT sku, delta FROM inventory_updates WHERE delta > $1) AS u +ON t.sku = u.sku AND t.quantity > $2 +WHEN MATCHED THEN + UPDATE SET quantity = $3 +WHEN NOT MATCHED THEN + INSERT (sku, quantity) VALUES (u.sku, $3); + +-- name: MergeDelete :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + DELETE; + +-- name: MergeNamedParams :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET updated_by = @updated_by +WHEN NOT MATCHED THEN + INSERT (sku, quantity, updated_by) VALUES (u.sku, sqlc.arg(start_qty), @updated_by); + +-- name: MergeMultiSet :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET (quantity, updated_by) = ($1, $2); diff --git a/internal/endtoend/testdata/merge/postgresql/pgx/v4/schema.sql b/internal/endtoend/testdata/merge/postgresql/pgx/v4/schema.sql new file mode 100644 index 0000000000..43060f19e9 --- /dev/null +++ b/internal/endtoend/testdata/merge/postgresql/pgx/v4/schema.sql @@ -0,0 +1,10 @@ +CREATE TABLE inventory ( + sku text PRIMARY KEY, + quantity integer NOT NULL, + updated_by text +); + +CREATE TABLE inventory_updates ( + sku text PRIMARY KEY, + delta integer NOT NULL +); diff --git a/internal/endtoend/testdata/merge/postgresql/pgx/v4/sqlc.json b/internal/endtoend/testdata/merge/postgresql/pgx/v4/sqlc.json new file mode 100644 index 0000000000..d1244c9e7a --- /dev/null +++ b/internal/endtoend/testdata/merge/postgresql/pgx/v4/sqlc.json @@ -0,0 +1,13 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "postgresql", + "sql_package": "pgx/v4", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/merge/postgresql/pgx/v5/go/db.go b/internal/endtoend/testdata/merge/postgresql/pgx/v5/go/db.go new file mode 100644 index 0000000000..0057c62319 --- /dev/null +++ b/internal/endtoend/testdata/merge/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/merge/postgresql/pgx/v5/go/models.go b/internal/endtoend/testdata/merge/postgresql/pgx/v5/go/models.go new file mode 100644 index 0000000000..d7cd07310d --- /dev/null +++ b/internal/endtoend/testdata/merge/postgresql/pgx/v5/go/models.go @@ -0,0 +1,20 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "github.com/jackc/pgx/v5/pgtype" +) + +type Inventory struct { + Sku string + Quantity int32 + UpdatedBy pgtype.Text +} + +type InventoryUpdate struct { + Sku string + Delta int32 +} diff --git a/internal/endtoend/testdata/merge/postgresql/pgx/v5/go/query.sql.go b/internal/endtoend/testdata/merge/postgresql/pgx/v5/go/query.sql.go new file mode 100644 index 0000000000..dba6b03019 --- /dev/null +++ b/internal/endtoend/testdata/merge/postgresql/pgx/v5/go/query.sql.go @@ -0,0 +1,106 @@ +// 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 mergeDelete = `-- name: MergeDelete :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + DELETE +` + +func (q *Queries) MergeDelete(ctx context.Context) error { + _, err := q.db.Exec(ctx, mergeDelete) + return err +} + +const mergeFromSubquery = `-- name: MergeFromSubquery :exec +MERGE INTO inventory AS t +USING (SELECT sku, delta FROM inventory_updates WHERE delta > $1) AS u +ON t.sku = u.sku AND t.quantity > $2 +WHEN MATCHED THEN + UPDATE SET quantity = $3 +WHEN NOT MATCHED THEN + INSERT (sku, quantity) VALUES (u.sku, $3) +` + +type MergeFromSubqueryParams struct { + Delta int32 + Quantity int32 + Quantity_2 int32 +} + +func (q *Queries) MergeFromSubquery(ctx context.Context, arg MergeFromSubqueryParams) error { + _, err := q.db.Exec(ctx, mergeFromSubquery, arg.Delta, arg.Quantity, arg.Quantity_2) + return err +} + +const mergeMultiSet = `-- name: MergeMultiSet :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET (quantity, updated_by) = ($1, $2) +` + +type MergeMultiSetParams struct { + Quantity int32 + UpdatedBy pgtype.Text +} + +func (q *Queries) MergeMultiSet(ctx context.Context, arg MergeMultiSetParams) error { + _, err := q.db.Exec(ctx, mergeMultiSet, arg.Quantity, arg.UpdatedBy) + return err +} + +const mergeNamedParams = `-- name: MergeNamedParams :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET updated_by = $1 +WHEN NOT MATCHED THEN + INSERT (sku, quantity, updated_by) VALUES (u.sku, $2, $1) +` + +type MergeNamedParamsParams struct { + UpdatedBy pgtype.Text + StartQty int32 +} + +func (q *Queries) MergeNamedParams(ctx context.Context, arg MergeNamedParamsParams) error { + _, err := q.db.Exec(ctx, mergeNamedParams, arg.UpdatedBy, arg.StartQty) + return err +} + +const syncInventory = `-- name: SyncInventory :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED AND t.quantity + u.delta <= $1 THEN + DELETE +WHEN MATCHED THEN + UPDATE SET quantity = t.quantity + u.delta, updated_by = $2 +WHEN NOT MATCHED THEN + INSERT (sku, quantity, updated_by) VALUES (u.sku, u.delta, $2) +` + +type SyncInventoryParams struct { + Quantity int32 + UpdatedBy pgtype.Text +} + +func (q *Queries) SyncInventory(ctx context.Context, arg SyncInventoryParams) error { + _, err := q.db.Exec(ctx, syncInventory, arg.Quantity, arg.UpdatedBy) + return err +} diff --git a/internal/endtoend/testdata/merge/postgresql/pgx/v5/query.sql b/internal/endtoend/testdata/merge/postgresql/pgx/v5/query.sql new file mode 100644 index 0000000000..93607f0533 --- /dev/null +++ b/internal/endtoend/testdata/merge/postgresql/pgx/v5/query.sql @@ -0,0 +1,42 @@ +-- name: SyncInventory :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED AND t.quantity + u.delta <= $1 THEN + DELETE +WHEN MATCHED THEN + UPDATE SET quantity = t.quantity + u.delta, updated_by = $2 +WHEN NOT MATCHED THEN + INSERT (sku, quantity, updated_by) VALUES (u.sku, u.delta, $2); + +-- name: MergeFromSubquery :exec +MERGE INTO inventory AS t +USING (SELECT sku, delta FROM inventory_updates WHERE delta > $1) AS u +ON t.sku = u.sku AND t.quantity > $2 +WHEN MATCHED THEN + UPDATE SET quantity = $3 +WHEN NOT MATCHED THEN + INSERT (sku, quantity) VALUES (u.sku, $3); + +-- name: MergeDelete :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + DELETE; + +-- name: MergeNamedParams :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET updated_by = @updated_by +WHEN NOT MATCHED THEN + INSERT (sku, quantity, updated_by) VALUES (u.sku, sqlc.arg(start_qty), @updated_by); + +-- name: MergeMultiSet :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET (quantity, updated_by) = ($1, $2); diff --git a/internal/endtoend/testdata/merge/postgresql/pgx/v5/schema.sql b/internal/endtoend/testdata/merge/postgresql/pgx/v5/schema.sql new file mode 100644 index 0000000000..43060f19e9 --- /dev/null +++ b/internal/endtoend/testdata/merge/postgresql/pgx/v5/schema.sql @@ -0,0 +1,10 @@ +CREATE TABLE inventory ( + sku text PRIMARY KEY, + quantity integer NOT NULL, + updated_by text +); + +CREATE TABLE inventory_updates ( + sku text PRIMARY KEY, + delta integer NOT NULL +); diff --git a/internal/endtoend/testdata/merge/postgresql/pgx/v5/sqlc.json b/internal/endtoend/testdata/merge/postgresql/pgx/v5/sqlc.json new file mode 100644 index 0000000000..32ede07158 --- /dev/null +++ b/internal/endtoend/testdata/merge/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/merge/postgresql/stdlib/go/db.go b/internal/endtoend/testdata/merge/postgresql/stdlib/go/db.go new file mode 100644 index 0000000000..80dd6ab1f6 --- /dev/null +++ b/internal/endtoend/testdata/merge/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/merge/postgresql/stdlib/go/models.go b/internal/endtoend/testdata/merge/postgresql/stdlib/go/models.go new file mode 100644 index 0000000000..4be31ab1ec --- /dev/null +++ b/internal/endtoend/testdata/merge/postgresql/stdlib/go/models.go @@ -0,0 +1,20 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "database/sql" +) + +type Inventory struct { + Sku string + Quantity int32 + UpdatedBy sql.NullString +} + +type InventoryUpdate struct { + Sku string + Delta int32 +} diff --git a/internal/endtoend/testdata/merge/postgresql/stdlib/go/query.sql.go b/internal/endtoend/testdata/merge/postgresql/stdlib/go/query.sql.go new file mode 100644 index 0000000000..45528c6fde --- /dev/null +++ b/internal/endtoend/testdata/merge/postgresql/stdlib/go/query.sql.go @@ -0,0 +1,105 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const mergeDelete = `-- name: MergeDelete :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + DELETE +` + +func (q *Queries) MergeDelete(ctx context.Context) error { + _, err := q.db.ExecContext(ctx, mergeDelete) + return err +} + +const mergeFromSubquery = `-- name: MergeFromSubquery :exec +MERGE INTO inventory AS t +USING (SELECT sku, delta FROM inventory_updates WHERE delta > $1) AS u +ON t.sku = u.sku AND t.quantity > $2 +WHEN MATCHED THEN + UPDATE SET quantity = $3 +WHEN NOT MATCHED THEN + INSERT (sku, quantity) VALUES (u.sku, $3) +` + +type MergeFromSubqueryParams struct { + Delta int32 + Quantity int32 + Quantity_2 int32 +} + +func (q *Queries) MergeFromSubquery(ctx context.Context, arg MergeFromSubqueryParams) error { + _, err := q.db.ExecContext(ctx, mergeFromSubquery, arg.Delta, arg.Quantity, arg.Quantity_2) + return err +} + +const mergeMultiSet = `-- name: MergeMultiSet :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET (quantity, updated_by) = ($1, $2) +` + +type MergeMultiSetParams struct { + Quantity int32 + UpdatedBy sql.NullString +} + +func (q *Queries) MergeMultiSet(ctx context.Context, arg MergeMultiSetParams) error { + _, err := q.db.ExecContext(ctx, mergeMultiSet, arg.Quantity, arg.UpdatedBy) + return err +} + +const mergeNamedParams = `-- name: MergeNamedParams :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET updated_by = $1 +WHEN NOT MATCHED THEN + INSERT (sku, quantity, updated_by) VALUES (u.sku, $2, $1) +` + +type MergeNamedParamsParams struct { + UpdatedBy sql.NullString + StartQty int32 +} + +func (q *Queries) MergeNamedParams(ctx context.Context, arg MergeNamedParamsParams) error { + _, err := q.db.ExecContext(ctx, mergeNamedParams, arg.UpdatedBy, arg.StartQty) + return err +} + +const syncInventory = `-- name: SyncInventory :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED AND t.quantity + u.delta <= $1 THEN + DELETE +WHEN MATCHED THEN + UPDATE SET quantity = t.quantity + u.delta, updated_by = $2 +WHEN NOT MATCHED THEN + INSERT (sku, quantity, updated_by) VALUES (u.sku, u.delta, $2) +` + +type SyncInventoryParams struct { + Quantity int32 + UpdatedBy sql.NullString +} + +func (q *Queries) SyncInventory(ctx context.Context, arg SyncInventoryParams) error { + _, err := q.db.ExecContext(ctx, syncInventory, arg.Quantity, arg.UpdatedBy) + return err +} diff --git a/internal/endtoend/testdata/merge/postgresql/stdlib/query.sql b/internal/endtoend/testdata/merge/postgresql/stdlib/query.sql new file mode 100644 index 0000000000..93607f0533 --- /dev/null +++ b/internal/endtoend/testdata/merge/postgresql/stdlib/query.sql @@ -0,0 +1,42 @@ +-- name: SyncInventory :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED AND t.quantity + u.delta <= $1 THEN + DELETE +WHEN MATCHED THEN + UPDATE SET quantity = t.quantity + u.delta, updated_by = $2 +WHEN NOT MATCHED THEN + INSERT (sku, quantity, updated_by) VALUES (u.sku, u.delta, $2); + +-- name: MergeFromSubquery :exec +MERGE INTO inventory AS t +USING (SELECT sku, delta FROM inventory_updates WHERE delta > $1) AS u +ON t.sku = u.sku AND t.quantity > $2 +WHEN MATCHED THEN + UPDATE SET quantity = $3 +WHEN NOT MATCHED THEN + INSERT (sku, quantity) VALUES (u.sku, $3); + +-- name: MergeDelete :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + DELETE; + +-- name: MergeNamedParams :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET updated_by = @updated_by +WHEN NOT MATCHED THEN + INSERT (sku, quantity, updated_by) VALUES (u.sku, sqlc.arg(start_qty), @updated_by); + +-- name: MergeMultiSet :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET (quantity, updated_by) = ($1, $2); diff --git a/internal/endtoend/testdata/merge/postgresql/stdlib/schema.sql b/internal/endtoend/testdata/merge/postgresql/stdlib/schema.sql new file mode 100644 index 0000000000..43060f19e9 --- /dev/null +++ b/internal/endtoend/testdata/merge/postgresql/stdlib/schema.sql @@ -0,0 +1,10 @@ +CREATE TABLE inventory ( + sku text PRIMARY KEY, + quantity integer NOT NULL, + updated_by text +); + +CREATE TABLE inventory_updates ( + sku text PRIMARY KEY, + delta integer NOT NULL +); diff --git a/internal/endtoend/testdata/merge/postgresql/stdlib/sqlc.json b/internal/endtoend/testdata/merge/postgresql/stdlib/sqlc.json new file mode 100644 index 0000000000..f717ca2e66 --- /dev/null +++ b/internal/endtoend/testdata/merge/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/endtoend/testdata/merge_insert_invalid/postgresql/pgx/v5/exec.json b/internal/endtoend/testdata/merge_insert_invalid/postgresql/pgx/v5/exec.json new file mode 100644 index 0000000000..2e996ca79d --- /dev/null +++ b/internal/endtoend/testdata/merge_insert_invalid/postgresql/pgx/v5/exec.json @@ -0,0 +1,3 @@ +{ + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/merge_insert_invalid/postgresql/pgx/v5/query.sql b/internal/endtoend/testdata/merge_insert_invalid/postgresql/pgx/v5/query.sql new file mode 100644 index 0000000000..cbb47a5125 --- /dev/null +++ b/internal/endtoend/testdata/merge_insert_invalid/postgresql/pgx/v5/query.sql @@ -0,0 +1,6 @@ +-- name: MergeInsertNoColumnList :exec +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN NOT MATCHED THEN + INSERT VALUES (u.sku, $1); diff --git a/internal/endtoend/testdata/merge_insert_invalid/postgresql/pgx/v5/schema.sql b/internal/endtoend/testdata/merge_insert_invalid/postgresql/pgx/v5/schema.sql new file mode 100644 index 0000000000..4e6e21d8cb --- /dev/null +++ b/internal/endtoend/testdata/merge_insert_invalid/postgresql/pgx/v5/schema.sql @@ -0,0 +1,9 @@ +CREATE TABLE inventory ( + sku text PRIMARY KEY, + quantity integer NOT NULL +); + +CREATE TABLE inventory_updates ( + sku text PRIMARY KEY, + delta integer NOT NULL +); diff --git a/internal/endtoend/testdata/merge_insert_invalid/postgresql/pgx/v5/sqlc.json b/internal/endtoend/testdata/merge_insert_invalid/postgresql/pgx/v5/sqlc.json new file mode 100644 index 0000000000..32ede07158 --- /dev/null +++ b/internal/endtoend/testdata/merge_insert_invalid/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/merge_insert_invalid/postgresql/pgx/v5/stderr.txt b/internal/endtoend/testdata/merge_insert_invalid/postgresql/pgx/v5/stderr.txt new file mode 100644 index 0000000000..08f9a9f0cc --- /dev/null +++ b/internal/endtoend/testdata/merge_insert_invalid/postgresql/pgx/v5/stderr.txt @@ -0,0 +1,2 @@ +# package querytest +query.sql:1:1: MERGE INSERT with parameters requires an explicit column list diff --git a/internal/endtoend/testdata/merge_invalid/postgresql/pgx/v5/exec.json b/internal/endtoend/testdata/merge_invalid/postgresql/pgx/v5/exec.json new file mode 100644 index 0000000000..2e996ca79d --- /dev/null +++ b/internal/endtoend/testdata/merge_invalid/postgresql/pgx/v5/exec.json @@ -0,0 +1,3 @@ +{ + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/merge_invalid/postgresql/pgx/v5/query.sql b/internal/endtoend/testdata/merge_invalid/postgresql/pgx/v5/query.sql new file mode 100644 index 0000000000..ad8997c472 --- /dev/null +++ b/internal/endtoend/testdata/merge_invalid/postgresql/pgx/v5/query.sql @@ -0,0 +1,6 @@ +-- name: MergeWithoutReturning :one +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET quantity = u.delta; diff --git a/internal/endtoend/testdata/merge_invalid/postgresql/pgx/v5/schema.sql b/internal/endtoend/testdata/merge_invalid/postgresql/pgx/v5/schema.sql new file mode 100644 index 0000000000..4e6e21d8cb --- /dev/null +++ b/internal/endtoend/testdata/merge_invalid/postgresql/pgx/v5/schema.sql @@ -0,0 +1,9 @@ +CREATE TABLE inventory ( + sku text PRIMARY KEY, + quantity integer NOT NULL +); + +CREATE TABLE inventory_updates ( + sku text PRIMARY KEY, + delta integer NOT NULL +); diff --git a/internal/endtoend/testdata/merge_invalid/postgresql/pgx/v5/sqlc.json b/internal/endtoend/testdata/merge_invalid/postgresql/pgx/v5/sqlc.json new file mode 100644 index 0000000000..32ede07158 --- /dev/null +++ b/internal/endtoend/testdata/merge_invalid/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/merge_invalid/postgresql/pgx/v5/stderr.txt b/internal/endtoend/testdata/merge_invalid/postgresql/pgx/v5/stderr.txt new file mode 100644 index 0000000000..1adaaa08ec --- /dev/null +++ b/internal/endtoend/testdata/merge_invalid/postgresql/pgx/v5/stderr.txt @@ -0,0 +1,2 @@ +# package querytest +query.sql:1:1: query "MergeWithoutReturning" specifies parameter ":one" without containing a RETURNING clause diff --git a/internal/endtoend/testdata/merge_returning/postgresql/pgx/v4/exec.json b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v4/exec.json new file mode 100644 index 0000000000..2e996ca79d --- /dev/null +++ b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v4/exec.json @@ -0,0 +1,3 @@ +{ + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/merge_returning/postgresql/pgx/v4/go/db.go b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v4/go/db.go new file mode 100644 index 0000000000..9da6d9a49f --- /dev/null +++ b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v4/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/pgconn" + "github.com/jackc/pgx/v4" +) + +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/merge_returning/postgresql/pgx/v4/go/models.go b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v4/go/models.go new file mode 100644 index 0000000000..6543013586 --- /dev/null +++ b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v4/go/models.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "database/sql" +) + +type Inventory struct { + Sku string + Quantity int32 + UpdatedBy sql.NullString +} + +type InventoryUpdate struct { + Sku string + Delta int32 +} + +type ProductCatalog struct { + ID string + Name string + Price int32 +} + +type ProductImport struct { + ID int32 + Label string +} diff --git a/internal/endtoend/testdata/merge_returning/postgresql/pgx/v4/go/query.sql.go b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v4/go/query.sql.go new file mode 100644 index 0000000000..8028026755 --- /dev/null +++ b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v4/go/query.sql.go @@ -0,0 +1,229 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const mergeReturningAction = `-- name: MergeReturningAction :many +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET quantity = u.delta +WHEN NOT MATCHED THEN + INSERT (sku, quantity) VALUES (u.sku, u.delta) +RETURNING merge_action(), t.sku, t.quantity +` + +type MergeReturningActionRow struct { + MergeAction string + Sku string + Quantity int32 +} + +func (q *Queries) MergeReturningAction(ctx context.Context) ([]MergeReturningActionRow, error) { + rows, err := q.db.Query(ctx, mergeReturningAction) + if err != nil { + return nil, err + } + defer rows.Close() + var items []MergeReturningActionRow + for rows.Next() { + var i MergeReturningActionRow + if err := rows.Scan(&i.MergeAction, &i.Sku, &i.Quantity); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const mergeReturningMany = `-- name: MergeReturningMany :many +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET quantity = u.delta +WHEN NOT MATCHED THEN + INSERT (sku, quantity) VALUES (u.sku, u.delta) +RETURNING t.sku, t.quantity, t.updated_by +` + +func (q *Queries) MergeReturningMany(ctx context.Context) ([]Inventory, error) { + rows, err := q.db.Query(ctx, mergeReturningMany) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Inventory + for rows.Next() { + var i Inventory + if err := rows.Scan(&i.Sku, &i.Quantity, &i.UpdatedBy); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const mergeReturningNotMatchedBySource = `-- name: MergeReturningNotMatchedBySource :many +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN NOT MATCHED BY SOURCE THEN + DELETE +RETURNING u.sku, u.delta, t.sku, t.quantity +` + +type MergeReturningNotMatchedBySourceRow struct { + Sku sql.NullString + Delta sql.NullInt32 + Sku_2 string + Quantity int32 +} + +func (q *Queries) MergeReturningNotMatchedBySource(ctx context.Context) ([]MergeReturningNotMatchedBySourceRow, error) { + rows, err := q.db.Query(ctx, mergeReturningNotMatchedBySource) + if err != nil { + return nil, err + } + defer rows.Close() + var items []MergeReturningNotMatchedBySourceRow + for rows.Next() { + var i MergeReturningNotMatchedBySourceRow + if err := rows.Scan( + &i.Sku, + &i.Delta, + &i.Sku_2, + &i.Quantity, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const mergeReturningOne = `-- name: MergeReturningOne :one +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku AND t.sku = $1 +WHEN MATCHED THEN + UPDATE SET quantity = u.delta +WHEN NOT MATCHED THEN + INSERT (sku, quantity) VALUES (u.sku, u.delta) +RETURNING t.sku, t.quantity +` + +type MergeReturningOneRow struct { + Sku string + Quantity int32 +} + +func (q *Queries) MergeReturningOne(ctx context.Context, sku string) (MergeReturningOneRow, error) { + row := q.db.QueryRow(ctx, mergeReturningOne, sku) + var i MergeReturningOneRow + err := row.Scan(&i.Sku, &i.Quantity) + return i, err +} + +const mergeReturningStar = `-- name: MergeReturningStar :many +MERGE INTO product_catalog AS t +USING product_imports AS u +ON t.id = u.label +WHEN MATCHED THEN + UPDATE SET price = u.id +WHEN NOT MATCHED THEN + INSERT (id, name, price) VALUES (u.label, u.label, u.id) +RETURNING u.id, label, t.id, name, price +` + +type MergeReturningStarRow struct { + ID int32 + Label string + ID_2 string + Name string + Price int32 +} + +func (q *Queries) MergeReturningStar(ctx context.Context) ([]MergeReturningStarRow, error) { + rows, err := q.db.Query(ctx, mergeReturningStar) + if err != nil { + return nil, err + } + defer rows.Close() + var items []MergeReturningStarRow + for rows.Next() { + var i MergeReturningStarRow + if err := rows.Scan( + &i.ID, + &i.Label, + &i.ID_2, + &i.Name, + &i.Price, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const mergeReturningSubquerySource = `-- name: MergeReturningSubquerySource :many +MERGE INTO inventory AS t +USING (SELECT sku, delta FROM inventory_updates WHERE delta > 0) AS u +ON t.sku = u.sku +WHEN NOT MATCHED BY SOURCE THEN + DELETE +RETURNING u.sku, u.delta, t.sku, t.quantity +` + +type MergeReturningSubquerySourceRow struct { + Sku sql.NullString + Delta sql.NullInt32 + Sku_2 string + Quantity int32 +} + +func (q *Queries) MergeReturningSubquerySource(ctx context.Context) ([]MergeReturningSubquerySourceRow, error) { + rows, err := q.db.Query(ctx, mergeReturningSubquerySource) + if err != nil { + return nil, err + } + defer rows.Close() + var items []MergeReturningSubquerySourceRow + for rows.Next() { + var i MergeReturningSubquerySourceRow + if err := rows.Scan( + &i.Sku, + &i.Delta, + &i.Sku_2, + &i.Quantity, + ); 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/merge_returning/postgresql/pgx/v4/query.sql b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v4/query.sql new file mode 100644 index 0000000000..9133cdb21a --- /dev/null +++ b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v4/query.sql @@ -0,0 +1,55 @@ +-- name: MergeReturningOne :one +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku AND t.sku = $1 +WHEN MATCHED THEN + UPDATE SET quantity = u.delta +WHEN NOT MATCHED THEN + INSERT (sku, quantity) VALUES (u.sku, u.delta) +RETURNING t.sku, t.quantity; + +-- name: MergeReturningMany :many +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET quantity = u.delta +WHEN NOT MATCHED THEN + INSERT (sku, quantity) VALUES (u.sku, u.delta) +RETURNING t.sku, t.quantity, t.updated_by; + +-- name: MergeReturningStar :many +MERGE INTO product_catalog AS t +USING product_imports AS u +ON t.id = u.label +WHEN MATCHED THEN + UPDATE SET price = u.id +WHEN NOT MATCHED THEN + INSERT (id, name, price) VALUES (u.label, u.label, u.id) +RETURNING *; + +-- name: MergeReturningAction :many +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET quantity = u.delta +WHEN NOT MATCHED THEN + INSERT (sku, quantity) VALUES (u.sku, u.delta) +RETURNING merge_action(), t.sku, t.quantity; + +-- name: MergeReturningNotMatchedBySource :many +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN NOT MATCHED BY SOURCE THEN + DELETE +RETURNING u.sku, u.delta, t.sku, t.quantity; + +-- name: MergeReturningSubquerySource :many +MERGE INTO inventory AS t +USING (SELECT sku, delta FROM inventory_updates WHERE delta > 0) AS u +ON t.sku = u.sku +WHEN NOT MATCHED BY SOURCE THEN + DELETE +RETURNING u.sku, u.delta, t.sku, t.quantity; diff --git a/internal/endtoend/testdata/merge_returning/postgresql/pgx/v4/schema.sql b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v4/schema.sql new file mode 100644 index 0000000000..b519dd2607 --- /dev/null +++ b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v4/schema.sql @@ -0,0 +1,24 @@ +CREATE TABLE inventory ( + sku text PRIMARY KEY, + quantity integer NOT NULL, + updated_by text +); + +CREATE TABLE inventory_updates ( + sku text PRIMARY KEY, + delta integer NOT NULL +); + +-- Tables for the RETURNING * column-ordering regression. The target and +-- source share a column name ("id") with distinguishable types so the +-- generated field types prove the source-before-target expansion order. +CREATE TABLE product_catalog ( + id text PRIMARY KEY, + name text NOT NULL, + price integer NOT NULL +); + +CREATE TABLE product_imports ( + id integer NOT NULL, + label text NOT NULL +); diff --git a/internal/endtoend/testdata/merge_returning/postgresql/pgx/v4/sqlc.json b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v4/sqlc.json new file mode 100644 index 0000000000..d1244c9e7a --- /dev/null +++ b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v4/sqlc.json @@ -0,0 +1,13 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "postgresql", + "sql_package": "pgx/v4", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/merge_returning/postgresql/pgx/v5/exec.json b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v5/exec.json new file mode 100644 index 0000000000..2e996ca79d --- /dev/null +++ b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v5/exec.json @@ -0,0 +1,3 @@ +{ + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/merge_returning/postgresql/pgx/v5/go/db.go b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v5/go/db.go new file mode 100644 index 0000000000..0057c62319 --- /dev/null +++ b/internal/endtoend/testdata/merge_returning/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/merge_returning/postgresql/pgx/v5/go/models.go b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v5/go/models.go new file mode 100644 index 0000000000..78f12bf162 --- /dev/null +++ b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v5/go/models.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "github.com/jackc/pgx/v5/pgtype" +) + +type Inventory struct { + Sku string + Quantity int32 + UpdatedBy pgtype.Text +} + +type InventoryUpdate struct { + Sku string + Delta int32 +} + +type ProductCatalog struct { + ID string + Name string + Price int32 +} + +type ProductImport struct { + ID int32 + Label string +} diff --git a/internal/endtoend/testdata/merge_returning/postgresql/pgx/v5/go/query.sql.go b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v5/go/query.sql.go new file mode 100644 index 0000000000..9944e34ed9 --- /dev/null +++ b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v5/go/query.sql.go @@ -0,0 +1,230 @@ +// 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 mergeReturningAction = `-- name: MergeReturningAction :many +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET quantity = u.delta +WHEN NOT MATCHED THEN + INSERT (sku, quantity) VALUES (u.sku, u.delta) +RETURNING merge_action(), t.sku, t.quantity +` + +type MergeReturningActionRow struct { + MergeAction string + Sku string + Quantity int32 +} + +func (q *Queries) MergeReturningAction(ctx context.Context) ([]MergeReturningActionRow, error) { + rows, err := q.db.Query(ctx, mergeReturningAction) + if err != nil { + return nil, err + } + defer rows.Close() + var items []MergeReturningActionRow + for rows.Next() { + var i MergeReturningActionRow + if err := rows.Scan(&i.MergeAction, &i.Sku, &i.Quantity); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const mergeReturningMany = `-- name: MergeReturningMany :many +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET quantity = u.delta +WHEN NOT MATCHED THEN + INSERT (sku, quantity) VALUES (u.sku, u.delta) +RETURNING t.sku, t.quantity, t.updated_by +` + +func (q *Queries) MergeReturningMany(ctx context.Context) ([]Inventory, error) { + rows, err := q.db.Query(ctx, mergeReturningMany) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Inventory + for rows.Next() { + var i Inventory + if err := rows.Scan(&i.Sku, &i.Quantity, &i.UpdatedBy); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const mergeReturningNotMatchedBySource = `-- name: MergeReturningNotMatchedBySource :many +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN NOT MATCHED BY SOURCE THEN + DELETE +RETURNING u.sku, u.delta, t.sku, t.quantity +` + +type MergeReturningNotMatchedBySourceRow struct { + Sku pgtype.Text + Delta pgtype.Int4 + Sku_2 string + Quantity int32 +} + +func (q *Queries) MergeReturningNotMatchedBySource(ctx context.Context) ([]MergeReturningNotMatchedBySourceRow, error) { + rows, err := q.db.Query(ctx, mergeReturningNotMatchedBySource) + if err != nil { + return nil, err + } + defer rows.Close() + var items []MergeReturningNotMatchedBySourceRow + for rows.Next() { + var i MergeReturningNotMatchedBySourceRow + if err := rows.Scan( + &i.Sku, + &i.Delta, + &i.Sku_2, + &i.Quantity, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const mergeReturningOne = `-- name: MergeReturningOne :one +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku AND t.sku = $1 +WHEN MATCHED THEN + UPDATE SET quantity = u.delta +WHEN NOT MATCHED THEN + INSERT (sku, quantity) VALUES (u.sku, u.delta) +RETURNING t.sku, t.quantity +` + +type MergeReturningOneRow struct { + Sku string + Quantity int32 +} + +func (q *Queries) MergeReturningOne(ctx context.Context, sku string) (MergeReturningOneRow, error) { + row := q.db.QueryRow(ctx, mergeReturningOne, sku) + var i MergeReturningOneRow + err := row.Scan(&i.Sku, &i.Quantity) + return i, err +} + +const mergeReturningStar = `-- name: MergeReturningStar :many +MERGE INTO product_catalog AS t +USING product_imports AS u +ON t.id = u.label +WHEN MATCHED THEN + UPDATE SET price = u.id +WHEN NOT MATCHED THEN + INSERT (id, name, price) VALUES (u.label, u.label, u.id) +RETURNING u.id, label, t.id, name, price +` + +type MergeReturningStarRow struct { + ID int32 + Label string + ID_2 string + Name string + Price int32 +} + +func (q *Queries) MergeReturningStar(ctx context.Context) ([]MergeReturningStarRow, error) { + rows, err := q.db.Query(ctx, mergeReturningStar) + if err != nil { + return nil, err + } + defer rows.Close() + var items []MergeReturningStarRow + for rows.Next() { + var i MergeReturningStarRow + if err := rows.Scan( + &i.ID, + &i.Label, + &i.ID_2, + &i.Name, + &i.Price, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const mergeReturningSubquerySource = `-- name: MergeReturningSubquerySource :many +MERGE INTO inventory AS t +USING (SELECT sku, delta FROM inventory_updates WHERE delta > 0) AS u +ON t.sku = u.sku +WHEN NOT MATCHED BY SOURCE THEN + DELETE +RETURNING u.sku, u.delta, t.sku, t.quantity +` + +type MergeReturningSubquerySourceRow struct { + Sku pgtype.Text + Delta pgtype.Int4 + Sku_2 string + Quantity int32 +} + +func (q *Queries) MergeReturningSubquerySource(ctx context.Context) ([]MergeReturningSubquerySourceRow, error) { + rows, err := q.db.Query(ctx, mergeReturningSubquerySource) + if err != nil { + return nil, err + } + defer rows.Close() + var items []MergeReturningSubquerySourceRow + for rows.Next() { + var i MergeReturningSubquerySourceRow + if err := rows.Scan( + &i.Sku, + &i.Delta, + &i.Sku_2, + &i.Quantity, + ); 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/merge_returning/postgresql/pgx/v5/query.sql b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v5/query.sql new file mode 100644 index 0000000000..9133cdb21a --- /dev/null +++ b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v5/query.sql @@ -0,0 +1,55 @@ +-- name: MergeReturningOne :one +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku AND t.sku = $1 +WHEN MATCHED THEN + UPDATE SET quantity = u.delta +WHEN NOT MATCHED THEN + INSERT (sku, quantity) VALUES (u.sku, u.delta) +RETURNING t.sku, t.quantity; + +-- name: MergeReturningMany :many +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET quantity = u.delta +WHEN NOT MATCHED THEN + INSERT (sku, quantity) VALUES (u.sku, u.delta) +RETURNING t.sku, t.quantity, t.updated_by; + +-- name: MergeReturningStar :many +MERGE INTO product_catalog AS t +USING product_imports AS u +ON t.id = u.label +WHEN MATCHED THEN + UPDATE SET price = u.id +WHEN NOT MATCHED THEN + INSERT (id, name, price) VALUES (u.label, u.label, u.id) +RETURNING *; + +-- name: MergeReturningAction :many +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET quantity = u.delta +WHEN NOT MATCHED THEN + INSERT (sku, quantity) VALUES (u.sku, u.delta) +RETURNING merge_action(), t.sku, t.quantity; + +-- name: MergeReturningNotMatchedBySource :many +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN NOT MATCHED BY SOURCE THEN + DELETE +RETURNING u.sku, u.delta, t.sku, t.quantity; + +-- name: MergeReturningSubquerySource :many +MERGE INTO inventory AS t +USING (SELECT sku, delta FROM inventory_updates WHERE delta > 0) AS u +ON t.sku = u.sku +WHEN NOT MATCHED BY SOURCE THEN + DELETE +RETURNING u.sku, u.delta, t.sku, t.quantity; diff --git a/internal/endtoend/testdata/merge_returning/postgresql/pgx/v5/schema.sql b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v5/schema.sql new file mode 100644 index 0000000000..b519dd2607 --- /dev/null +++ b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v5/schema.sql @@ -0,0 +1,24 @@ +CREATE TABLE inventory ( + sku text PRIMARY KEY, + quantity integer NOT NULL, + updated_by text +); + +CREATE TABLE inventory_updates ( + sku text PRIMARY KEY, + delta integer NOT NULL +); + +-- Tables for the RETURNING * column-ordering regression. The target and +-- source share a column name ("id") with distinguishable types so the +-- generated field types prove the source-before-target expansion order. +CREATE TABLE product_catalog ( + id text PRIMARY KEY, + name text NOT NULL, + price integer NOT NULL +); + +CREATE TABLE product_imports ( + id integer NOT NULL, + label text NOT NULL +); diff --git a/internal/endtoend/testdata/merge_returning/postgresql/pgx/v5/sqlc.json b/internal/endtoend/testdata/merge_returning/postgresql/pgx/v5/sqlc.json new file mode 100644 index 0000000000..32ede07158 --- /dev/null +++ b/internal/endtoend/testdata/merge_returning/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/merge_returning/postgresql/stdlib/exec.json b/internal/endtoend/testdata/merge_returning/postgresql/stdlib/exec.json new file mode 100644 index 0000000000..2e996ca79d --- /dev/null +++ b/internal/endtoend/testdata/merge_returning/postgresql/stdlib/exec.json @@ -0,0 +1,3 @@ +{ + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/merge_returning/postgresql/stdlib/go/db.go b/internal/endtoend/testdata/merge_returning/postgresql/stdlib/go/db.go new file mode 100644 index 0000000000..80dd6ab1f6 --- /dev/null +++ b/internal/endtoend/testdata/merge_returning/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/merge_returning/postgresql/stdlib/go/models.go b/internal/endtoend/testdata/merge_returning/postgresql/stdlib/go/models.go new file mode 100644 index 0000000000..6543013586 --- /dev/null +++ b/internal/endtoend/testdata/merge_returning/postgresql/stdlib/go/models.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "database/sql" +) + +type Inventory struct { + Sku string + Quantity int32 + UpdatedBy sql.NullString +} + +type InventoryUpdate struct { + Sku string + Delta int32 +} + +type ProductCatalog struct { + ID string + Name string + Price int32 +} + +type ProductImport struct { + ID int32 + Label string +} diff --git a/internal/endtoend/testdata/merge_returning/postgresql/stdlib/go/query.sql.go b/internal/endtoend/testdata/merge_returning/postgresql/stdlib/go/query.sql.go new file mode 100644 index 0000000000..912de23251 --- /dev/null +++ b/internal/endtoend/testdata/merge_returning/postgresql/stdlib/go/query.sql.go @@ -0,0 +1,244 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const mergeReturningAction = `-- name: MergeReturningAction :many +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET quantity = u.delta +WHEN NOT MATCHED THEN + INSERT (sku, quantity) VALUES (u.sku, u.delta) +RETURNING merge_action(), t.sku, t.quantity +` + +type MergeReturningActionRow struct { + MergeAction string + Sku string + Quantity int32 +} + +func (q *Queries) MergeReturningAction(ctx context.Context) ([]MergeReturningActionRow, error) { + rows, err := q.db.QueryContext(ctx, mergeReturningAction) + if err != nil { + return nil, err + } + defer rows.Close() + var items []MergeReturningActionRow + for rows.Next() { + var i MergeReturningActionRow + if err := rows.Scan(&i.MergeAction, &i.Sku, &i.Quantity); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const mergeReturningMany = `-- name: MergeReturningMany :many +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET quantity = u.delta +WHEN NOT MATCHED THEN + INSERT (sku, quantity) VALUES (u.sku, u.delta) +RETURNING t.sku, t.quantity, t.updated_by +` + +func (q *Queries) MergeReturningMany(ctx context.Context) ([]Inventory, error) { + rows, err := q.db.QueryContext(ctx, mergeReturningMany) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Inventory + for rows.Next() { + var i Inventory + if err := rows.Scan(&i.Sku, &i.Quantity, &i.UpdatedBy); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const mergeReturningNotMatchedBySource = `-- name: MergeReturningNotMatchedBySource :many +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN NOT MATCHED BY SOURCE THEN + DELETE +RETURNING u.sku, u.delta, t.sku, t.quantity +` + +type MergeReturningNotMatchedBySourceRow struct { + Sku sql.NullString + Delta sql.NullInt32 + Sku_2 string + Quantity int32 +} + +func (q *Queries) MergeReturningNotMatchedBySource(ctx context.Context) ([]MergeReturningNotMatchedBySourceRow, error) { + rows, err := q.db.QueryContext(ctx, mergeReturningNotMatchedBySource) + if err != nil { + return nil, err + } + defer rows.Close() + var items []MergeReturningNotMatchedBySourceRow + for rows.Next() { + var i MergeReturningNotMatchedBySourceRow + if err := rows.Scan( + &i.Sku, + &i.Delta, + &i.Sku_2, + &i.Quantity, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const mergeReturningOne = `-- name: MergeReturningOne :one +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku AND t.sku = $1 +WHEN MATCHED THEN + UPDATE SET quantity = u.delta +WHEN NOT MATCHED THEN + INSERT (sku, quantity) VALUES (u.sku, u.delta) +RETURNING t.sku, t.quantity +` + +type MergeReturningOneRow struct { + Sku string + Quantity int32 +} + +func (q *Queries) MergeReturningOne(ctx context.Context, sku string) (MergeReturningOneRow, error) { + row := q.db.QueryRowContext(ctx, mergeReturningOne, sku) + var i MergeReturningOneRow + err := row.Scan(&i.Sku, &i.Quantity) + return i, err +} + +const mergeReturningStar = `-- name: MergeReturningStar :many +MERGE INTO product_catalog AS t +USING product_imports AS u +ON t.id = u.label +WHEN MATCHED THEN + UPDATE SET price = u.id +WHEN NOT MATCHED THEN + INSERT (id, name, price) VALUES (u.label, u.label, u.id) +RETURNING u.id, label, t.id, name, price +` + +type MergeReturningStarRow struct { + ID int32 + Label string + ID_2 string + Name string + Price int32 +} + +func (q *Queries) MergeReturningStar(ctx context.Context) ([]MergeReturningStarRow, error) { + rows, err := q.db.QueryContext(ctx, mergeReturningStar) + if err != nil { + return nil, err + } + defer rows.Close() + var items []MergeReturningStarRow + for rows.Next() { + var i MergeReturningStarRow + if err := rows.Scan( + &i.ID, + &i.Label, + &i.ID_2, + &i.Name, + &i.Price, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const mergeReturningSubquerySource = `-- name: MergeReturningSubquerySource :many +MERGE INTO inventory AS t +USING (SELECT sku, delta FROM inventory_updates WHERE delta > 0) AS u +ON t.sku = u.sku +WHEN NOT MATCHED BY SOURCE THEN + DELETE +RETURNING u.sku, u.delta, t.sku, t.quantity +` + +type MergeReturningSubquerySourceRow struct { + Sku sql.NullString + Delta sql.NullInt32 + Sku_2 string + Quantity int32 +} + +func (q *Queries) MergeReturningSubquerySource(ctx context.Context) ([]MergeReturningSubquerySourceRow, error) { + rows, err := q.db.QueryContext(ctx, mergeReturningSubquerySource) + if err != nil { + return nil, err + } + defer rows.Close() + var items []MergeReturningSubquerySourceRow + for rows.Next() { + var i MergeReturningSubquerySourceRow + if err := rows.Scan( + &i.Sku, + &i.Delta, + &i.Sku_2, + &i.Quantity, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/merge_returning/postgresql/stdlib/query.sql b/internal/endtoend/testdata/merge_returning/postgresql/stdlib/query.sql new file mode 100644 index 0000000000..9133cdb21a --- /dev/null +++ b/internal/endtoend/testdata/merge_returning/postgresql/stdlib/query.sql @@ -0,0 +1,55 @@ +-- name: MergeReturningOne :one +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku AND t.sku = $1 +WHEN MATCHED THEN + UPDATE SET quantity = u.delta +WHEN NOT MATCHED THEN + INSERT (sku, quantity) VALUES (u.sku, u.delta) +RETURNING t.sku, t.quantity; + +-- name: MergeReturningMany :many +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET quantity = u.delta +WHEN NOT MATCHED THEN + INSERT (sku, quantity) VALUES (u.sku, u.delta) +RETURNING t.sku, t.quantity, t.updated_by; + +-- name: MergeReturningStar :many +MERGE INTO product_catalog AS t +USING product_imports AS u +ON t.id = u.label +WHEN MATCHED THEN + UPDATE SET price = u.id +WHEN NOT MATCHED THEN + INSERT (id, name, price) VALUES (u.label, u.label, u.id) +RETURNING *; + +-- name: MergeReturningAction :many +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN MATCHED THEN + UPDATE SET quantity = u.delta +WHEN NOT MATCHED THEN + INSERT (sku, quantity) VALUES (u.sku, u.delta) +RETURNING merge_action(), t.sku, t.quantity; + +-- name: MergeReturningNotMatchedBySource :many +MERGE INTO inventory AS t +USING inventory_updates AS u +ON t.sku = u.sku +WHEN NOT MATCHED BY SOURCE THEN + DELETE +RETURNING u.sku, u.delta, t.sku, t.quantity; + +-- name: MergeReturningSubquerySource :many +MERGE INTO inventory AS t +USING (SELECT sku, delta FROM inventory_updates WHERE delta > 0) AS u +ON t.sku = u.sku +WHEN NOT MATCHED BY SOURCE THEN + DELETE +RETURNING u.sku, u.delta, t.sku, t.quantity; diff --git a/internal/endtoend/testdata/merge_returning/postgresql/stdlib/schema.sql b/internal/endtoend/testdata/merge_returning/postgresql/stdlib/schema.sql new file mode 100644 index 0000000000..b519dd2607 --- /dev/null +++ b/internal/endtoend/testdata/merge_returning/postgresql/stdlib/schema.sql @@ -0,0 +1,24 @@ +CREATE TABLE inventory ( + sku text PRIMARY KEY, + quantity integer NOT NULL, + updated_by text +); + +CREATE TABLE inventory_updates ( + sku text PRIMARY KEY, + delta integer NOT NULL +); + +-- Tables for the RETURNING * column-ordering regression. The target and +-- source share a column name ("id") with distinguishable types so the +-- generated field types prove the source-before-target expansion order. +CREATE TABLE product_catalog ( + id text PRIMARY KEY, + name text NOT NULL, + price integer NOT NULL +); + +CREATE TABLE product_imports ( + id integer NOT NULL, + label text NOT NULL +); diff --git a/internal/endtoend/testdata/merge_returning/postgresql/stdlib/sqlc.json b/internal/endtoend/testdata/merge_returning/postgresql/stdlib/sqlc.json new file mode 100644 index 0000000000..f717ca2e66 --- /dev/null +++ b/internal/endtoend/testdata/merge_returning/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/catalog.go b/internal/engine/postgresql/catalog.go index e11fe21222..2f30093ab7 100644 --- a/internal/engine/postgresql/catalog.go +++ b/internal/engine/postgresql/catalog.go @@ -1,6 +1,21 @@ package postgresql -import "github.com/sqlc-dev/sqlc/internal/sql/catalog" +import ( + "github.com/sqlc-dev/sqlc/internal/sql/ast" + "github.com/sqlc-dev/sqlc/internal/sql/catalog" +) + +// supplementalPGCatalogFuncs holds pg_catalog functions not covered by the +// generated catalog (pg_catalog.go). merge_action() is a PostgreSQL 17 +// function that is only valid inside a MERGE ... RETURNING clause and returns +// the action taken ('INSERT', 'UPDATE', or 'DELETE'). +var supplementalPGCatalogFuncs = []*catalog.Function{ + { + Name: "merge_action", + Args: []*catalog.Argument{}, + ReturnType: &ast.TypeName{Name: "text"}, + }, +} // toPointer converts an int to a pointer without a temporary // variable at the call-site, and is used by the generated schemas @@ -13,7 +28,14 @@ func toPointer(x int) *int { func NewCatalog() *catalog.Catalog { c := catalog.New("public") c.Schemas = append(c.Schemas, pgTemp()) - c.Schemas = append(c.Schemas, genPGCatalog()) + pgCatalog := genPGCatalog() + // genPGCatalog aliases the package-global funcsgenPGCatalog slice; build a + // fresh slice rather than appending into it so the global is never mutated. + funcs := make([]*catalog.Function, 0, len(pgCatalog.Funcs)+len(supplementalPGCatalogFuncs)) + funcs = append(funcs, pgCatalog.Funcs...) + funcs = append(funcs, supplementalPGCatalogFuncs...) + pgCatalog.Funcs = funcs + c.Schemas = append(c.Schemas, pgCatalog) c.Schemas = append(c.Schemas, genInformationSchema()) c.SearchPath = []string{"pg_catalog"} c.LoadExtension = loadExtension diff --git a/internal/engine/postgresql/convert.go b/internal/engine/postgresql/convert.go index 321294c59e..1c06bf0dd2 100644 --- a/internal/engine/postgresql/convert.go +++ b/internal/engine/postgresql/convert.go @@ -1892,6 +1892,50 @@ func convertLockingClause(n *pg.LockingClause) *ast.LockingClause { } } +func convertMergeStmt(n *pg.MergeStmt) *ast.MergeStmt { + if n == nil { + return nil + } + return &ast.MergeStmt{ + Relation: convertRangeVar(n.Relation), + SourceRelation: convertNode(n.SourceRelation), + JoinCondition: convertNode(n.JoinCondition), + MergeWhenClauses: convertSlice(n.MergeWhenClauses), + ReturningList: convertSlice(n.ReturningList), + WithClause: convertWithClause(n.WithClause), + } +} + +func convertMergeWhenClause(n *pg.MergeWhenClause) *ast.MergeWhenClause { + if n == nil { + return nil + } + return &ast.MergeWhenClause{ + MatchKind: ast.MergeMatchKind(n.MatchKind), + CommandType: ast.CmdType(n.CommandType), + Override: ast.OverridingKind(n.Override), + Condition: convertNode(n.Condition), + TargetList: convertSlice(n.TargetList), + Values: convertSlice(n.Values), + } +} + +func convertMergeSupportFunc(n *pg.MergeSupportFunc) *ast.FuncCall { + if n == nil { + return nil + } + // PostgreSQL 17 parses merge_action() as a dedicated MergeSupportFunc node + // rather than a generic function call. Represent it as a FuncCall so the + // catalog can resolve its return type (text) and it formats back to + // merge_action(). + return &ast.FuncCall{ + Func: &ast.FuncName{Name: "merge_action"}, + Funcname: &ast.List{Items: []ast.Node{&ast.String{Str: "merge_action"}}}, + Args: &ast.List{}, + Location: int(n.Location), + } +} + func convertMinMaxExpr(n *pg.MinMaxExpr) *ast.MinMaxExpr { if n == nil { return nil @@ -3418,6 +3462,15 @@ func convertNode(node *pg.Node) ast.Node { case *pg.Node_LockingClause: return convertLockingClause(n.LockingClause) + case *pg.Node_MergeStmt: + return convertMergeStmt(n.MergeStmt) + + case *pg.Node_MergeWhenClause: + return convertMergeWhenClause(n.MergeWhenClause) + + case *pg.Node_MergeSupportFunc: + return convertMergeSupportFunc(n.MergeSupportFunc) + case *pg.Node_MinMaxExpr: return convertMinMaxExpr(n.MinMaxExpr) diff --git a/internal/sql/ast/cmd_type.go b/internal/sql/ast/cmd_type.go index dfe9019e4c..7f68d40fa8 100644 --- a/internal/sql/ast/cmd_type.go +++ b/internal/sql/ast/cmd_type.go @@ -2,6 +2,18 @@ package ast type CmdType uint +// Values match the PostgreSQL CmdType enum +const ( + CmdTypeUnknown CmdType = 1 + CmdTypeSelect CmdType = 2 + CmdTypeUpdate CmdType = 3 + CmdTypeInsert CmdType = 4 + CmdTypeDelete CmdType = 5 + CmdTypeMerge CmdType = 6 + CmdTypeUtility CmdType = 7 + CmdTypeNothing CmdType = 8 +) + func (n *CmdType) Pos() int { return 0 } diff --git a/internal/sql/ast/merge_stmt.go b/internal/sql/ast/merge_stmt.go new file mode 100644 index 0000000000..9c1da0dd64 --- /dev/null +++ b/internal/sql/ast/merge_stmt.go @@ -0,0 +1,54 @@ +package ast + +import "github.com/sqlc-dev/sqlc/internal/sql/format" + +type MergeStmt struct { + Relation *RangeVar + SourceRelation Node + JoinCondition Node + MergeWhenClauses *List + ReturningList *List + WithClause *WithClause +} + +func (n *MergeStmt) Pos() int { + return 0 +} + +func (n *MergeStmt) Format(buf *TrackedBuffer, d format.Dialect) { + if n == nil { + return + } + + if n.WithClause != nil { + buf.astFormat(n.WithClause, d) + buf.WriteString(" ") + } + + buf.WriteString("MERGE INTO ") + if n.Relation != nil { + buf.astFormat(n.Relation, d) + } + + if set(n.SourceRelation) { + buf.WriteString(" USING ") + buf.astFormat(n.SourceRelation, d) + } + + if set(n.JoinCondition) { + buf.WriteString(" ON ") + buf.astFormat(n.JoinCondition, d) + } + + if n.MergeWhenClauses != nil { + for _, item := range n.MergeWhenClauses.Items { + buf.WriteString(" ") + buf.astFormat(item, d) + } + } + + if items(n.ReturningList) { + buf.WriteString(" RETURNING ") + buf.astFormat(n.ReturningList, d) + } +} diff --git a/internal/sql/ast/merge_when_clause.go b/internal/sql/ast/merge_when_clause.go new file mode 100644 index 0000000000..e1f1e92126 --- /dev/null +++ b/internal/sql/ast/merge_when_clause.go @@ -0,0 +1,77 @@ +package ast + +import "github.com/sqlc-dev/sqlc/internal/sql/format" + +type MergeMatchKind uint + +// Values match the PostgreSQL MergeMatchKind enum +const ( + MergeWhenMatched MergeMatchKind = 1 + MergeWhenNotMatchedBySource MergeMatchKind = 2 + MergeWhenNotMatchedByTarget MergeMatchKind = 3 +) + +type MergeWhenClause struct { + MatchKind MergeMatchKind + CommandType CmdType + Override OverridingKind + Condition Node + TargetList *List + Values *List +} + +func (n *MergeWhenClause) Pos() int { + return 0 +} + +func (n *MergeWhenClause) Format(buf *TrackedBuffer, d format.Dialect) { + if n == nil { + return + } + + switch n.MatchKind { + case MergeWhenNotMatchedBySource: + buf.WriteString("WHEN NOT MATCHED BY SOURCE") + case MergeWhenNotMatchedByTarget: + buf.WriteString("WHEN NOT MATCHED") + case MergeWhenMatched: + buf.WriteString("WHEN MATCHED") + } + + if set(n.Condition) { + buf.WriteString(" AND ") + buf.astFormat(n.Condition, d) + } + + buf.WriteString(" THEN ") + + switch n.CommandType { + case CmdTypeUpdate: + buf.WriteString("UPDATE SET ") + formatSetClause(buf, d, n.TargetList) + case CmdTypeInsert: + buf.WriteString("INSERT") + if items(n.TargetList) { + buf.WriteString(" (") + buf.astFormat(n.TargetList, d) + buf.WriteString(")") + } + switch n.Override { + case OverridingSystemValue: + buf.WriteString(" OVERRIDING SYSTEM VALUE") + case OverridingUserValue: + buf.WriteString(" OVERRIDING USER VALUE") + } + if items(n.Values) { + buf.WriteString(" VALUES (") + buf.join(n.Values, d, ", ") + buf.WriteString(")") + } else { + buf.WriteString(" DEFAULT VALUES") + } + case CmdTypeDelete: + buf.WriteString("DELETE") + case CmdTypeNothing: + buf.WriteString("DO NOTHING") + } +} diff --git a/internal/sql/ast/on_conflict_clause.go b/internal/sql/ast/on_conflict_clause.go index a71bae0a23..9815695791 100644 --- a/internal/sql/ast/on_conflict_clause.go +++ b/internal/sql/ast/on_conflict_clause.go @@ -36,23 +36,7 @@ func (n *OnConflictClause) Format(buf *TrackedBuffer, d format.Dialect) { buf.WriteString("DO NOTHING") case OnConflictActionUpdate: buf.WriteString("DO UPDATE SET ") - // Format as assignment list: name = val - if n.TargetList != nil { - for i, item := range n.TargetList.Items { - if i > 0 { - buf.WriteString(", ") - } - if rt, ok := item.(*ResTarget); ok { - if rt.Name != nil { - buf.WriteString(*rt.Name) - } - buf.WriteString(" = ") - buf.astFormat(rt.Val, d) - } else { - buf.astFormat(item, d) - } - } - } + formatSetClause(buf, d, n.TargetList) if set(n.WhereClause) { buf.WriteString(" WHERE ") buf.astFormat(n.WhereClause, d) diff --git a/internal/sql/ast/overriding_kind.go b/internal/sql/ast/overriding_kind.go index 622893c2de..00b4e052ee 100644 --- a/internal/sql/ast/overriding_kind.go +++ b/internal/sql/ast/overriding_kind.go @@ -2,6 +2,13 @@ package ast type OverridingKind uint +// Values match the PostgreSQL OverridingKind enum +const ( + OverridingNotSet OverridingKind = 1 + OverridingUserValue OverridingKind = 2 + OverridingSystemValue OverridingKind = 3 +) + func (n *OverridingKind) Pos() int { return 0 } diff --git a/internal/sql/ast/update_stmt.go b/internal/sql/ast/update_stmt.go index 5376a8c6ce..dfe9601781 100644 --- a/internal/sql/ast/update_stmt.go +++ b/internal/sql/ast/update_stmt.go @@ -20,6 +20,78 @@ func (n *UpdateStmt) Pos() int { return 0 } +// formatSetClause renders an UPDATE-style SET assignment list (without the +// leading "SET ") for the given target list. It handles both single-column +// assignments (col = val) and multi-column assignments ((a, b) = (x, y)), and +// is shared by UPDATE and MERGE ... WHEN MATCHED THEN UPDATE. +func formatSetClause(buf *TrackedBuffer, d format.Dialect, targetList *List) { + if !items(targetList) { + return + } + + multi := false + for _, item := range targetList.Items { + switch nn := item.(type) { + case *ResTarget: + if _, ok := nn.Val.(*MultiAssignRef); ok { + multi = true + } + } + } + if multi { + names := []string{} + vals := &List{} + for _, item := range targetList.Items { + res, ok := item.(*ResTarget) + if !ok { + continue + } + if res.Name != nil { + names = append(names, *res.Name) + } + multi, ok := res.Val.(*MultiAssignRef) + if !ok { + vals.Items = append(vals.Items, res.Val) + continue + } + row, ok := multi.Source.(*RowExpr) + if !ok { + vals.Items = append(vals.Items, res.Val) + continue + } + vals.Items = append(vals.Items, row.Args.Items[multi.Colno-1]) + } + + buf.WriteString("(") + buf.WriteString(strings.Join(names, ",")) + buf.WriteString(") = (") + buf.join(vals, d, ",") + buf.WriteString(")") + } else { + for i, item := range targetList.Items { + if i > 0 { + buf.WriteString(", ") + } + switch nn := item.(type) { + case *ResTarget: + if nn.Name != nil { + buf.WriteString(d.QuoteIdent(*nn.Name)) + } + // Handle array subscript indirection (e.g., names[$1]) + if items(nn.Indirection) { + for _, ind := range nn.Indirection.Items { + buf.astFormat(ind, d) + } + } + buf.WriteString(" = ") + buf.astFormat(nn.Val, d) + default: + buf.astFormat(item, d) + } + } + } +} + func (n *UpdateStmt) Format(buf *TrackedBuffer, d format.Dialect) { if n == nil { return @@ -36,68 +108,7 @@ func (n *UpdateStmt) Format(buf *TrackedBuffer, d format.Dialect) { if items(n.TargetList) { buf.WriteString(" SET ") - - multi := false - for _, item := range n.TargetList.Items { - switch nn := item.(type) { - case *ResTarget: - if _, ok := nn.Val.(*MultiAssignRef); ok { - multi = true - } - } - } - if multi { - names := []string{} - vals := &List{} - for _, item := range n.TargetList.Items { - res, ok := item.(*ResTarget) - if !ok { - continue - } - if res.Name != nil { - names = append(names, *res.Name) - } - multi, ok := res.Val.(*MultiAssignRef) - if !ok { - vals.Items = append(vals.Items, res.Val) - continue - } - row, ok := multi.Source.(*RowExpr) - if !ok { - vals.Items = append(vals.Items, res.Val) - continue - } - vals.Items = append(vals.Items, row.Args.Items[multi.Colno-1]) - } - - buf.WriteString("(") - buf.WriteString(strings.Join(names, ",")) - buf.WriteString(") = (") - buf.join(vals, d, ",") - buf.WriteString(")") - } else { - for i, item := range n.TargetList.Items { - if i > 0 { - buf.WriteString(", ") - } - switch nn := item.(type) { - case *ResTarget: - if nn.Name != nil { - buf.WriteString(d.QuoteIdent(*nn.Name)) - } - // Handle array subscript indirection (e.g., names[$1]) - if items(nn.Indirection) { - for _, ind := range nn.Indirection.Items { - buf.astFormat(ind, d) - } - } - buf.WriteString(" = ") - buf.astFormat(nn.Val, d) - default: - buf.astFormat(item, d) - } - } - } + formatSetClause(buf, d, n.TargetList) } if items(n.FromClause) { diff --git a/internal/sql/astutils/rewrite.go b/internal/sql/astutils/rewrite.go index 25b274a16c..e21c7abf1c 100644 --- a/internal/sql/astutils/rewrite.go +++ b/internal/sql/astutils/rewrite.go @@ -849,6 +849,19 @@ func (a *application) apply(parent ast.Node, name string, iter *iterator, n ast. case *ast.LockingClause: a.apply(n, "LockedRels", nil, n.LockedRels) + case *ast.MergeStmt: + a.apply(n, "Relation", nil, n.Relation) + a.apply(n, "SourceRelation", nil, n.SourceRelation) + a.apply(n, "JoinCondition", nil, n.JoinCondition) + a.apply(n, "MergeWhenClauses", nil, n.MergeWhenClauses) + a.apply(n, "ReturningList", nil, n.ReturningList) + a.apply(n, "WithClause", nil, n.WithClause) + + case *ast.MergeWhenClause: + a.apply(n, "Condition", nil, n.Condition) + a.apply(n, "TargetList", nil, n.TargetList) + a.apply(n, "Values", nil, n.Values) + case *ast.MinMaxExpr: a.apply(n, "Xpr", nil, n.Xpr) a.apply(n, "Args", nil, n.Args) diff --git a/internal/sql/astutils/walk.go b/internal/sql/astutils/walk.go index 6d5e80bdc3..f7159726b5 100644 --- a/internal/sql/astutils/walk.go +++ b/internal/sql/astutils/walk.go @@ -1383,6 +1383,37 @@ func Walk(f Visitor, node ast.Node) { Walk(f, n.LockedRels) } + case *ast.MergeStmt: + if n.Relation != nil { + Walk(f, n.Relation) + } + if n.SourceRelation != nil { + Walk(f, n.SourceRelation) + } + if n.JoinCondition != nil { + Walk(f, n.JoinCondition) + } + if n.MergeWhenClauses != nil { + Walk(f, n.MergeWhenClauses) + } + if n.ReturningList != nil { + Walk(f, n.ReturningList) + } + if n.WithClause != nil { + Walk(f, n.WithClause) + } + + case *ast.MergeWhenClause: + if n.Condition != nil { + Walk(f, n.Condition) + } + if n.TargetList != nil { + Walk(f, n.TargetList) + } + if n.Values != nil { + Walk(f, n.Values) + } + case *ast.MinMaxExpr: if n.Xpr != nil { Walk(f, n.Xpr) diff --git a/internal/sql/validate/cmd.go b/internal/sql/validate/cmd.go index 66e849de6c..bb6938accd 100644 --- a/internal/sql/validate/cmd.go +++ b/internal/sql/validate/cmd.go @@ -84,6 +84,8 @@ func Cmd(n ast.Node, name, cmd string) error { list = stmt.ReturningList case *ast.UpdateStmt: list = stmt.ReturningList + case *ast.MergeStmt: + list = stmt.ReturningList default: return nil } diff --git a/internal/x/expander/expander.go b/internal/x/expander/expander.go index af0cab26e8..f5d911caa8 100644 --- a/internal/x/expander/expander.go +++ b/internal/x/expander/expander.go @@ -82,6 +82,8 @@ func (e *Expander) expandNode(ctx context.Context, node ast.Node) error { return e.expandInsertStmt(ctx, n) case *ast.UpdateStmt: return e.expandUpdateStmt(ctx, n) + case *ast.MergeStmt: + return e.expandMergeStmt(ctx, n) case *ast.DeleteStmt: return e.expandDeleteStmt(ctx, n) case *ast.CommonTableExpr: @@ -204,15 +206,39 @@ func (e *Expander) getCTEColumnNames(ctx context.Context, stmt *ast.SelectStmt, return e.getColumnNames(ctx, tempQuery) } +// expandCTEs expands * in each CTE of a WITH clause. +func (e *Expander) expandCTEs(ctx context.Context, with *ast.WithClause) error { + if with == nil || with.Ctes == nil { + return nil + } + for _, cte := range with.Ctes.Items { + if err := e.expandNode(ctx, cte); err != nil { + return err + } + } + return nil +} + +// expandReturning expands a * in the RETURNING list of stmt. stmt is the +// enclosing statement (used to build the query for column-name lookup) and +// returning is its RETURNING list; the possibly-rewritten list is returned. +func (e *Expander) expandReturning(ctx context.Context, stmt ast.Node, returning *ast.List) (*ast.List, error) { + if !hasStarInList(returning) { + return returning, nil + } + tempRaw := &ast.RawStmt{Stmt: stmt} + tempQuery := ast.Format(tempRaw, e.dialect) + columns, err := e.getColumnNames(ctx, tempQuery) + if err != nil { + return nil, fmt.Errorf("failed to get column names: %w", err) + } + return rewriteTargetList(returning, columns), nil +} + // expandInsertStmt expands * in an INSERT statement's RETURNING clause func (e *Expander) expandInsertStmt(ctx context.Context, stmt *ast.InsertStmt) error { - // Expand CTEs first - if stmt.WithClause != nil && stmt.WithClause.Ctes != nil { - for _, cte := range stmt.WithClause.Ctes.Items { - if err := e.expandNode(ctx, cte); err != nil { - return err - } - } + if err := e.expandCTEs(ctx, stmt.WithClause); err != nil { + return err } // Expand the SELECT part if present @@ -222,67 +248,50 @@ func (e *Expander) expandInsertStmt(ctx context.Context, stmt *ast.InsertStmt) e } } - // Expand RETURNING clause - if hasStarInList(stmt.ReturningList) { - tempRaw := &ast.RawStmt{Stmt: stmt} - tempQuery := ast.Format(tempRaw, e.dialect) - columns, err := e.getColumnNames(ctx, tempQuery) - if err != nil { - return fmt.Errorf("failed to get column names: %w", err) - } - stmt.ReturningList = rewriteTargetList(stmt.ReturningList, columns) + returning, err := e.expandReturning(ctx, stmt, stmt.ReturningList) + if err != nil { + return err } - + stmt.ReturningList = returning return nil } // expandUpdateStmt expands * in an UPDATE statement's RETURNING clause func (e *Expander) expandUpdateStmt(ctx context.Context, stmt *ast.UpdateStmt) error { - // Expand CTEs first - if stmt.WithClause != nil && stmt.WithClause.Ctes != nil { - for _, cte := range stmt.WithClause.Ctes.Items { - if err := e.expandNode(ctx, cte); err != nil { - return err - } - } + if err := e.expandCTEs(ctx, stmt.WithClause); err != nil { + return err } - - // Expand RETURNING clause - if hasStarInList(stmt.ReturningList) { - tempRaw := &ast.RawStmt{Stmt: stmt} - tempQuery := ast.Format(tempRaw, e.dialect) - columns, err := e.getColumnNames(ctx, tempQuery) - if err != nil { - return fmt.Errorf("failed to get column names: %w", err) - } - stmt.ReturningList = rewriteTargetList(stmt.ReturningList, columns) + returning, err := e.expandReturning(ctx, stmt, stmt.ReturningList) + if err != nil { + return err } + stmt.ReturningList = returning + return nil +} +// expandMergeStmt expands * in a MERGE statement's RETURNING clause +func (e *Expander) expandMergeStmt(ctx context.Context, stmt *ast.MergeStmt) error { + if err := e.expandCTEs(ctx, stmt.WithClause); err != nil { + return err + } + returning, err := e.expandReturning(ctx, stmt, stmt.ReturningList) + if err != nil { + return err + } + stmt.ReturningList = returning return nil } // expandDeleteStmt expands * in a DELETE statement's RETURNING clause func (e *Expander) expandDeleteStmt(ctx context.Context, stmt *ast.DeleteStmt) error { - // Expand CTEs first - if stmt.WithClause != nil && stmt.WithClause.Ctes != nil { - for _, cte := range stmt.WithClause.Ctes.Items { - if err := e.expandNode(ctx, cte); err != nil { - return err - } - } + if err := e.expandCTEs(ctx, stmt.WithClause); err != nil { + return err } - - // Expand RETURNING clause - if hasStarInList(stmt.ReturningList) { - tempRaw := &ast.RawStmt{Stmt: stmt} - tempQuery := ast.Format(tempRaw, e.dialect) - columns, err := e.getColumnNames(ctx, tempQuery) - if err != nil { - return fmt.Errorf("failed to get column names: %w", err) - } - stmt.ReturningList = rewriteTargetList(stmt.ReturningList, columns) + returning, err := e.expandReturning(ctx, stmt, stmt.ReturningList) + if err != nil { + return err } - + stmt.ReturningList = returning return nil }