Bug
PR #797 introduced a one-entry fieldsCache in withPgDebug to avoid redundant getFields / nfields / ftype calls within a cursor batch. The cache is an IORef (Maybe (Vector Pg.Field)) with no key:
case cached of
Just fs -> pure fs -- returns stale fields regardless of which Pg.Result produced them
When two queries with different column types are issued within the same runBeamPostgres call, the second query reads field OID metadata from the first. postgresql-simple's fromField rejects the wrong OID and throws, producing:
BeamRowReadError {brreColumn = Just 0, brreError = ColumnErrorInternal "Column parse failed with unknown exception"}
The existing comment at the call site already describes the intended behavior ("Caching by Pg.Result equality") — the implementation just didn't do it.
Reproduction
execute_ conn "CREATE TABLE alpha (name TEXT NOT NULL)"
execute_ conn "CREATE TABLE beta (value INT4 NOT NULL)"
execute_ conn "INSERT INTO alpha VALUES ('hello')"
execute_ conn "INSERT INTO beta VALUES (42)"
-- Fails with BeamRowReadError on the second query:
(names, values) <- runBeamPostgres conn $ do
ns <- runSelectReturningList $ select $ all_ (dbAlpha db)
vs <- runSelectReturningList $ select $ all_ (dbBeta db)
pure (ns, vs)
Root cause
renderExecReturningList (the AtOnce path used by runReturningList) calls cachedGetFields res and then frees res via Pg.unsafeFreeResult after the rows are consumed. libpq may reuse the freed pointer address for the next Pg.exec, meaning the cache key comparison cachedRes == res yields a false hit on the next query even with a keyed cache.
The cursor-batch path (stepProcess) does not call unsafeFreeResult, so pointer-keyed caching is safe there.
Fix
renderExecReturningList calls getFields res directly (no cache). The cache remains in place for stepProcess where it is both safe and beneficial (avoids repeated getFields calls across rows within a 256-row batch).
Bug
PR #797 introduced a one-entry
fieldsCacheinwithPgDebugto avoid redundantgetFields/nfields/ftypecalls within a cursor batch. The cache is anIORef (Maybe (Vector Pg.Field))with no key:When two queries with different column types are issued within the same
runBeamPostgrescall, the second query reads field OID metadata from the first.postgresql-simple'sfromFieldrejects the wrong OID and throws, producing:The existing comment at the call site already describes the intended behavior ("Caching by
Pg.Resultequality") — the implementation just didn't do it.Reproduction
Root cause
renderExecReturningList(theAtOncepath used byrunReturningList) callscachedGetFields resand then freesresviaPg.unsafeFreeResultafter the rows are consumed. libpq may reuse the freed pointer address for the nextPg.exec, meaning the cache key comparisoncachedRes == resyields a false hit on the next query even with a keyed cache.The cursor-batch path (
stepProcess) does not callunsafeFreeResult, so pointer-keyed caching is safe there.Fix
renderExecReturningListcallsgetFields resdirectly (no cache). The cache remains in place forstepProcesswhere it is both safe and beneficial (avoids repeatedgetFieldscalls across rows within a 256-row batch).