From affb6afeb92d84300a4e2fdc70e1c283569c5a90 Mon Sep 17 00:00:00 2001 From: jmgomez Date: Fri, 10 Apr 2026 19:58:07 +0100 Subject: [PATCH 1/2] Fix row lifetime bug: use fresh RowData in safe query paths instead of reusing connection buffer --- async_postgres/pg_client.nim | 46 ++++++++++++++++++++---------------- tests/test_e2e.nim | 15 ++++++++++++ 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/async_postgres/pg_client.nim b/async_postgres/pg_client.nim index 37244d1..a062aef 100644 --- a/async_postgres/pg_client.nim +++ b/async_postgres/pg_client.nim @@ -381,6 +381,7 @@ template queryRecvLoop( cachedColOids: seq[int32], qr: var QueryResult, timeout: Duration = ZeroDuration, + reuseBuffer: bool = true, ) = var queryError: ref PgQueryError @@ -390,14 +391,18 @@ template queryRecvLoop( for i in 0 ..< qr.fields.len: qr.fields[i].formatCode = cachedColFmts[i] if qr.fields.len > 0: - if conn.rowDataBuf != nil: - conn.rowDataBuf = conn.rowDataBuf.reuseRowData( - int16(qr.fields.len), cachedColFmts, cachedColOids - ) + if reuseBuffer: + if conn.rowDataBuf != nil: + conn.rowDataBuf = conn.rowDataBuf.reuseRowData( + int16(qr.fields.len), cachedColFmts, cachedColOids + ) + else: + conn.rowDataBuf = newRowData(int16(qr.fields.len), cachedColFmts, cachedColOids) + conn.rowDataBuf.fields = qr.fields + qr.data = conn.rowDataBuf else: - conn.rowDataBuf = newRowData(int16(qr.fields.len), cachedColFmts, cachedColOids) - conn.rowDataBuf.fields = qr.fields - qr.data = conn.rowDataBuf + qr.data = newRowData(int16(qr.fields.len), cachedColFmts, cachedColOids) + qr.data.fields = qr.fields block recvLoop: while true: @@ -427,12 +432,16 @@ template queryRecvLoop( cf[i] = resultFormats[i] else: qr.fields = msg.fields - if conn.rowDataBuf != nil: - conn.rowDataBuf = conn.rowDataBuf.reuseRowData(int16(qr.fields.len), cf, co) + if reuseBuffer: + if conn.rowDataBuf != nil: + conn.rowDataBuf = conn.rowDataBuf.reuseRowData(int16(qr.fields.len), cf, co) + else: + conn.rowDataBuf = newRowData(int16(qr.fields.len), cf, co) + conn.rowDataBuf.fields = qr.fields + qr.data = conn.rowDataBuf else: - conn.rowDataBuf = newRowData(int16(qr.fields.len), cf, co) - conn.rowDataBuf.fields = qr.fields - qr.data = conn.rowDataBuf + qr.data = newRowData(int16(qr.fields.len), cf, co) + qr.data.fields = qr.fields of bmkNoData: discard of bmkCommandComplete: @@ -520,7 +529,7 @@ proc queryImpl( var qr = QueryResult() queryRecvLoop( conn, sql, effectiveResultFormats, cacheHit, cacheMiss, stmtName, cachedFields, - cachedColFmts, cachedColOids, qr, timeout, + cachedColFmts, cachedColOids, qr, timeout, reuseBuffer = false, ) return qr @@ -581,7 +590,7 @@ proc queryImpl( var qr = QueryResult() queryRecvLoop( conn, sql, effectiveResultFormats, cacheHit, cacheMiss, stmtName, cachedFields, - cachedColFmts, cachedColOids, qr, timeout, + cachedColFmts, cachedColOids, qr, timeout, reuseBuffer = false, ) return qr @@ -1116,13 +1125,8 @@ proc executeImpl( for i in 0 ..< qr.fields.len: colFmts[i] = qr.fields[i].formatCode colOids[i] = qr.fields[i].typeOid - if conn.rowDataBuf != nil: - conn.rowDataBuf = - conn.rowDataBuf.reuseRowData(int16(qr.fields.len), colFmts, colOids) - else: - conn.rowDataBuf = newRowData(int16(qr.fields.len), colFmts, colOids) - conn.rowDataBuf.fields = qr.fields - qr.data = conn.rowDataBuf + qr.data = newRowData(int16(qr.fields.len), colFmts, colOids) + qr.data.fields = qr.fields var queryError: ref PgQueryError block recvLoop: diff --git a/tests/test_e2e.nim b/tests/test_e2e.nim index 4a2fe6e..d8397b6 100644 --- a/tests/test_e2e.nim +++ b/tests/test_e2e.nim @@ -4749,6 +4749,21 @@ suite "E2E: Convenience Query Methods": waitFor t() + test "query Row survives subsequent queries (lifetime bug)": + proc t() {.async.} = + let conn = await connect(plainConfig()) + let qr1 = await conn.query("SELECT 'x'") + let qr2 = await conn.query("SELECT 'y'") + doAssert qr1.rowCount == 1 + doAssert qr2.rowCount == 1 + let row1 = Row(data: qr1.data, rowIdx: 0) + let row2 = Row(data: qr2.data, rowIdx: 0) + doAssert row1.getStr(0) == "x", "qr1 data was invalidated by qr2" + doAssert row2.getStr(0) == "y" + await conn.close() + + waitFor t() + test "pool queryOne": proc t() {.async.} = let pool = await newPool(initPoolConfig(plainConfig(), minSize = 1, maxSize = 2)) From adaee5d165dabc7fc692dce3688c8f6810e7d944 Mon Sep 17 00:00:00 2001 From: jmgomez Date: Fri, 10 Apr 2026 21:25:50 +0100 Subject: [PATCH 2/2] Format using nph --- async_postgres/pg_client.nim | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/async_postgres/pg_client.nim b/async_postgres/pg_client.nim index a062aef..23218aa 100644 --- a/async_postgres/pg_client.nim +++ b/async_postgres/pg_client.nim @@ -397,7 +397,8 @@ template queryRecvLoop( int16(qr.fields.len), cachedColFmts, cachedColOids ) else: - conn.rowDataBuf = newRowData(int16(qr.fields.len), cachedColFmts, cachedColOids) + conn.rowDataBuf = + newRowData(int16(qr.fields.len), cachedColFmts, cachedColOids) conn.rowDataBuf.fields = qr.fields qr.data = conn.rowDataBuf else: @@ -434,7 +435,8 @@ template queryRecvLoop( qr.fields = msg.fields if reuseBuffer: if conn.rowDataBuf != nil: - conn.rowDataBuf = conn.rowDataBuf.reuseRowData(int16(qr.fields.len), cf, co) + conn.rowDataBuf = + conn.rowDataBuf.reuseRowData(int16(qr.fields.len), cf, co) else: conn.rowDataBuf = newRowData(int16(qr.fields.len), cf, co) conn.rowDataBuf.fields = qr.fields @@ -528,8 +530,18 @@ proc queryImpl( var qr = QueryResult() queryRecvLoop( - conn, sql, effectiveResultFormats, cacheHit, cacheMiss, stmtName, cachedFields, - cachedColFmts, cachedColOids, qr, timeout, reuseBuffer = false, + conn, + sql, + effectiveResultFormats, + cacheHit, + cacheMiss, + stmtName, + cachedFields, + cachedColFmts, + cachedColOids, + qr, + timeout, + reuseBuffer = false, ) return qr @@ -589,8 +601,18 @@ proc queryImpl( var qr = QueryResult() queryRecvLoop( - conn, sql, effectiveResultFormats, cacheHit, cacheMiss, stmtName, cachedFields, - cachedColFmts, cachedColOids, qr, timeout, reuseBuffer = false, + conn, + sql, + effectiveResultFormats, + cacheHit, + cacheMiss, + stmtName, + cachedFields, + cachedColFmts, + cachedColOids, + qr, + timeout, + reuseBuffer = false, ) return qr