Skip to content

fieldsCache in withPgDebug causes stale field metadata across queries (regression from #797) #802

@compilerbauer

Description

@compilerbauer

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).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions