From b6539658ed2c4bba1f9d503843077c99ea4ec5c2 Mon Sep 17 00:00:00 2001 From: jotikrishna <213903218+jotikrishna@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:09:52 +0530 Subject: [PATCH] docs: explain how to use the batch object The :batchexec, :batchmany and :batchone sections listed the batch object's methods but never said what the callback receives, what its int argument means, what Close does to statements that have not run yet, or when Close is even needed. Issue #4631 reports exactly this: readers cannot tell what to do with the returned batch object. Add a 'Using the batch object' section that documents the callback contract and Close semantics, and point each batch annotation at it: - the callback is invoked once per queued statement, in order, and the int is the statement's 0-based position in the batch - a failing statement does not stop iteration; errors must be handled inside the callback - Close before or during iteration skips the remaining statements and delivers ErrBatchAlreadyClosed once per remaining statement - Close is called automatically when the iteration method returns, so a fully iterated batch needs no explicit Close The behaviour described is taken from the generated code itself (internal/codegen/golang/templates/pgx/batchCode.tmpl and the batch endtoend golden files), not paraphrased from other docs. Fixes #4631 --- docs/reference/query-annotations.md | 82 +++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/docs/reference/query-annotations.md b/docs/reference/query-annotations.md index 4fabe05aae..46e7c466ad 100644 --- a/docs/reference/query-annotations.md +++ b/docs/reference/query-annotations.md @@ -122,6 +122,9 @@ the following methods: - `Exec`, that takes a `func(int, error)` parameter, - `Close`, to close the batch operation early. +See [using the batch object](#using-the-batch-object) below for what these +methods do and a complete example. + ```sql -- name: DeleteBook :batchexec DELETE FROM books @@ -154,6 +157,9 @@ the following methods: - `Query`, that takes a `func(int, []T, error)` parameter, where `T` is your query's return type - `Close`, to close the batch operation early. +See [using the batch object](#using-the-batch-object) below for what these +methods do and a complete example. + ```sql -- name: BooksByTitleYear :batchmany SELECT * FROM books @@ -190,6 +196,9 @@ the following methods: - `QueryRow`, that takes a `func(int, T, error)` parameter, where `T` is your query's return type - `Close`, to close the batch operation early. +See [using the batch object](#using-the-batch-object) below for what these +methods do and a complete example. + ```sql -- name: CreateBook :batchone INSERT INTO books ( @@ -223,6 +232,79 @@ func (b *CreateBookBatchResults) Close() error { } ``` +## Using the batch object + +The batch object returned by a `:batch*` method wraps a single +`pgx.Batch`: one statement is queued per element of the slice argument, and +all of them are sent to PostgreSQL together when the method is called. You +then iterate the results with one of `Exec`, `Query` or `QueryRow` (depending +on the annotation), and finish with `Close`. + +The iteration method takes a callback and calls it **once per queued +statement, in order**: + +- The `int` argument is the 0-based position of the statement within the + batch, matching the index of the element you passed in. +- A failing statement does not stop the loop. If the third statement of ten + fails, the callback still receives results for the fourth through tenth. + Handle errors inside the callback; returning an error from it has no effect + on iteration. +- `Exec` delivers `func(index int, err error)`. +- `Query` delivers `func(index int, rows []T, err error)` with all rows that + the statement returned. +- `QueryRow` delivers `func(index int, value T, err error)` with the single + row the statement returned. A statement that matched no rows reports an + error, just like a standalone `QueryRow`. + +Calling `Close` before or during iteration marks the batch as closed: the +remaining queued statements are not executed, and their callbacks instead +receive `ErrBatchAlreadyClosed` — once per remaining statement, so your +callback always sees the full batch. `Close` is also called automatically +when the iteration method returns, so a batch that you iterate fully does +not require an explicit `Close`. + +To stop early on purpose, return early from the callback for the remaining +statements after recording what you need, or `panic` through the iteration +loop and recover around the call site. + +Putting that together, a `:batchone` insert looks like this: + +```go +res := q.CreateBook(ctx, books) + +inserted := make([]Book, len(books)) +res.QueryRow(func(i int, b Book, err error) { + if err != nil { + // log and continue: the batch keeps going regardless + return + } + inserted[i] = b +}) +// the batch is closed automatically when QueryRow returns +``` + +And stopping early once something irrecoverable happens: + +```go +res := q.DeleteBook(ctx, ids) + +res.Exec(func(i int, err error) { + if err != nil { + if errors.Is(err, ErrBatchAlreadyClosed) { + return // we closed the batch ourselves + } + // fatal: abandon the rest of the batch + res.Close() + } +}) +``` + +> [!NOTE] +> `pgx` executes the queued statements in order, but they are sent as one +> network round trip. The batch either way is a single unit from the +> connection's point of view: an error partway through does not roll back +> statements that already ran. + ## `:copyfrom` __NOTE: This command is driver and package specific, see [how to insert](../howto/insert.md#using-copyfrom)