Bug
`copyFromDirect` / `copyToDirect` path in `driver/postgres/conn.go`:
```go
if c.useDirect {
select {
case <-req.doneCh:
if req.err != nil { return 0, req.err }
case <-ctx.Done():
return 0, ctx.Err() // <— ORPHAN
}
}
```
When `ctx` is canceled mid-copy:
- The caller returns `ctx.Err()`.
- `req` stays enqueued in `c.pending`.
- The `startDirectReader` goroutine is stopped by its `defer stop()` via `SetReadDeadline` — it exits without ever signaling `req.doneCh`.
- On the NEXT query on the same conn, `dispatch` pops `req` as the head of `pending` but the response bytes belong to the new query — wire-format desync, undefined behavior.
Contrast: loop-mode path is correct
In loop mode the same code path calls `c.wait(ctx, req)`, which handles `ctx.Done` properly: sends `CancelRequest`, waits bounded 30s for the server's Error + ReadyForQuery, and only returns after the event loop has driven `req` to completion via `onRecv`. No orphan.
Fix
Mirror `wait()`'s cancellation shape in direct-mode COPY:
- On `ctx.Done`, send `CancelRequest` to the server (PG side-channel).
- Continue driving the reader goroutine for up to ~30s to consume the server's Error + RFQ.
- If `req.doneCh` closes within that window, good — return `ctx.Err()`.
- If not, forcibly fail the conn (`c.failAll`) so the pool discards it on release.
Apply to both `copyFrom` direct-mode branch and `copyTo` direct-mode branch.
Reproducer (sketch)
```go
ctx, cancel := context.WithCancel(context.Background())
go func() { time.Sleep(5 * time.Millisecond); cancel() }()
_, err := pool.CopyFrom(ctx, "big_table", cols, slowSrc)
// err == context.Canceled, good.
// Now reuse the same conn for a normal query:
_ = pool.QueryRow(context.Background(), "SELECT 1").Scan(&x)
// Response will be the tail of the canceled copy's wire bytes, not SELECT 1's.
```
Scope
- driver/postgres/conn.go:copyFrom (direct-mode ctx.Done branch)
- driver/postgres/conn.go:copyTo (direct-mode ctx.Done branch)
Priority
Blocker for v1.4.0 tag. Wire-format desync is the worst class of driver bug — silent corruption that outlives the cancellation.
Bug
`copyFromDirect` / `copyToDirect` path in `driver/postgres/conn.go`:
```go
if c.useDirect {
select {
case <-req.doneCh:
if req.err != nil { return 0, req.err }
case <-ctx.Done():
return 0, ctx.Err() // <— ORPHAN
}
}
```
When `ctx` is canceled mid-copy:
Contrast: loop-mode path is correct
In loop mode the same code path calls `c.wait(ctx, req)`, which handles `ctx.Done` properly: sends `CancelRequest`, waits bounded 30s for the server's Error + ReadyForQuery, and only returns after the event loop has driven `req` to completion via `onRecv`. No orphan.
Fix
Mirror `wait()`'s cancellation shape in direct-mode COPY:
Apply to both `copyFrom` direct-mode branch and `copyTo` direct-mode branch.
Reproducer (sketch)
```go
ctx, cancel := context.WithCancel(context.Background())
go func() { time.Sleep(5 * time.Millisecond); cancel() }()
_, err := pool.CopyFrom(ctx, "big_table", cols, slowSrc)
// err == context.Canceled, good.
// Now reuse the same conn for a normal query:
_ = pool.QueryRow(context.Background(), "SELECT 1").Scan(&x)
// Response will be the tail of the canceled copy's wire bytes, not SELECT 1's.
```
Scope
Priority
Blocker for v1.4.0 tag. Wire-format desync is the worst class of driver bug — silent corruption that outlives the cancellation.