@@ -90,6 +90,61 @@ co_await tx.commit();
9090The stream returned from a transaction does **not** commit on `close()` —
9191lifetime is bound to `tx`.
9292
93+ ### Bind parameters (`$1, $2, …`)
94+
95+ `stream` and `stream_reflect` accept variadic arguments after `batch_size`,
96+ forwarded to the cursor's underlying `SELECT` via libpq's extended protocol
97+ (`PQsendQueryParams`). Postgres binds them inside the `DECLARE … CURSOR FOR …`
98+ in a single round trip — no manual `PREPARE`, no string interpolation.
99+
100+ ```cpp
101+ const int64_t lower = 100000;
102+ const int64_t upper = 100100;
103+
104+ auto sres = co_await pool.stream_reflect<BigRow>(
105+ "SELECT id, name, tag FROM big_table "
106+ "WHERE id BETWEEN $1 AND $2 ORDER BY id",
107+ 1000, lower, upper);
108+ ```
109+
110+ The first non-SQL argument is ` batch_size ` (required, no default in this
111+ overload — pass ` 1000 ` or whatever you'd otherwise default to). Everything
112+ after it is passed positionally as ` $1 ` , ` $2 ` , …. Argument count is checked
113+ at runtime via ` assert ` against ` count_pg_params(sql) ` — same path the
114+ existing ` query_awaitable(sql, args...) ` uses.
115+
116+ Mixing with the pipeline works the same as without parameters — the
117+ returned stream is still a normal ` PgRowStream ` / ` PgTypedRowStream<T> ` :
118+
119+ ``` cpp
120+ namespace ps = usub::pg::stream;
121+
122+ auto s = co_await pool.stream_reflect<BigRow >(
123+ "SELECT id, name, tag FROM big_table "
124+ "WHERE id > $1 AND tag IS NOT NULL ORDER BY id",
125+ 2000, int64_t{50000});
126+
127+ int64_t sum = co_await(
128+ std::move(* s)
129+ | ps::filter([ ] (const BigRow& r) { return r.id % 2 == 0; })
130+ | ps::take(1000)
131+ | ps::reduce(int64_t{0}, [ ] (int64_t acc, BigRow r) { return acc + r.id; })
132+ ) ;
133+ ```
134+
135+ The same templated overloads are also on ` PgTransaction::stream ` and
136+ ` PgTransaction::stream_reflect ` .
137+
138+ #### What about ` copy_to ` / ` copy_from ` ?
139+
140+ ` COPY tablename TO STDOUT ` does not accept parameters in its grammar, and
141+ ` COPY (SELECT ... WHERE x = $1) TO STDOUT ` cannot be combined cleanly with
142+ libpq's extended-protocol path because libpq does not switch into COPY mode
143+ after ` PQsendQueryParams ` . If you need a parameterised export, use
144+ ` pool.stream(sql, batch_size, args…) ` instead — the row-by-row path is the
145+ intended substitute. ` COPY FROM STDIN ` has no place for parameters at all,
146+ so ` copy_from ` / ` copy_from_buffer ` only take SQL.
147+
93148---
94149
95150## 2. ` copy_to ` — streaming COPY OUT
@@ -452,18 +507,26 @@ what `pg_dump` uses for a reason.
452507task::Awaitable<expected<PgRowStream, PgOpError>>
453508 pool.stream(sql, batch_size = 1000 );
454509
510+ template <typename ... Args>
511+ task::Awaitable<expected<PgRowStream, PgOpError>>
512+ pool.stream(sql, batch_size, args...); // $1, $2, ... bound via PQsendQueryParams
513+
455514template <class T >
456515task::Awaitable<expected<PgTypedRowStream<T>, PgOpError>>
457516 pool.stream_reflect<T>(sql, batch_size = 1000 );
458517
459- // COPY OUT
518+ template <class T , typename... Args>
519+ task::Awaitable<expected<PgTypedRowStream<T >, PgOpError>>
520+ pool.stream_reflect<T >(sql, batch_size, args...);
521+
522+ // COPY OUT (no parameter binding — see section 1)
460523template<class Sink > // (string_view) -> Awaitable<bool >
461524task::Awaitable<PgCopyResult > pool.copy_to(sql, sink);
462525
463526template<class LineSink > // (string_view line) -> Awaitable<bool >
464527task::Awaitable<PgCopyResult > pool.copy_to_lines(sql, line_sink);
465528
466- // COPY IN
529+ // COPY IN (no parameter binding — grammar does not allow it)
467530template<class Source > // () -> Awaitable<string_view> (empty = EOF)
468531task::Awaitable<PgCopyResult > pool.copy_from(sql, source);
469532
0 commit comments