From 233a59a5f4edf8821b8278d40243cb5b04251f1c Mon Sep 17 00:00:00 2001 From: Adesh Deshmukh Date: Sat, 12 Sep 2026 13:03:21 +0530 Subject: [PATCH] get_data: fix SPI fetch order and portal leak on zero-row input Check SPI_processed before touching SPI_tuptable->tupdesc, with a defensive null guard for PG-version variance. On zero-row batches, validate columns when a descriptor is available so schema typos still raise a clean Column not Found error, then exit cleanly and let the drivers emit the standard No edges found notice. Wrap validation and row iteration in try/catch so the tuple table is freed and the cursor closed exactly once before rethrowing. Add zero-row regression tests to pgtap/dijkstra/dijkstra/no_crash_test.pg (empty set on valid schema, XX000 on missing column). Fixes #3146 Signed-off-by: Adesh Deshmukh --- include/cpp_common/get_data.hpp | 44 ++++++++++++++++++++---- pgtap/dijkstra/dijkstra/no_crash_test.pg | 9 ++++- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/include/cpp_common/get_data.hpp b/include/cpp_common/get_data.hpp index b4fb65fd042..92f61c78105 100644 --- a/include/cpp_common/get_data.hpp +++ b/include/cpp_common/get_data.hpp @@ -134,14 +134,42 @@ std::vector get_data( while (moredata == true) { SPI_cursor_fetch(SPIportal, true, tuple_limit); + long fetched = SPI_processed; auto tuptable = SPI_tuptable; - auto tupdesc = SPI_tuptable->tupdesc; - if (total_tuples == 0) fetch_column_info(tupdesc, info); + auto tupdesc = tuptable ? tuptable->tupdesc : nullptr; + + if (fetched <= 0) { + /* + * Zero-row batch: validate schema when a descriptor is + * available so typos still raise a clean error, then exit + * with whatever was collected so far (possibly empty). + */ + if (tupdesc && total_tuples == 0) { + try { + fetch_column_info(tupdesc, info); + } catch (...) { + if (tuptable) SPI_freetuptable(tuptable); + SPI_cursor_close(SPIportal); + throw; + } + } + if (tuptable) SPI_freetuptable(tuptable); + moredata = false; + break; + } - size_t ntuples = SPI_processed; - total_tuples += ntuples; + if (!tuptable || !tupdesc) { + if (tuptable) SPI_freetuptable(tuptable); + moredata = false; + break; + } + + try { + if (total_tuples == 0) fetch_column_info(tupdesc, info); + + size_t ntuples = static_cast(fetched); + total_tuples += ntuples; - if (ntuples > 0) { tuples.reserve(total_tuples); for (size_t t = 0; t < ntuples; t++) { tuples.push_back(func(tuptable->vals[t], tupdesc, info, @@ -149,8 +177,10 @@ std::vector get_data( &valid_pgtuples, flag)); } SPI_freetuptable(tuptable); - } else { - moredata = false; + } catch (...) { + SPI_freetuptable(tuptable); + SPI_cursor_close(SPIportal); + throw; } } diff --git a/pgtap/dijkstra/dijkstra/no_crash_test.pg b/pgtap/dijkstra/dijkstra/no_crash_test.pg index 37cd298ee69..1b5e4a2982d 100644 --- a/pgtap/dijkstra/dijkstra/no_crash_test.pg +++ b/pgtap/dijkstra/dijkstra/no_crash_test.pg @@ -7,10 +7,17 @@ BEGIN; UPDATE edges SET cost = sign(cost), reverse_cost = sign(reverse_cost); -SELECT CASE WHEN min_version('3.1.0') THEN plan(82) ELSE plan(69) END; +SELECT CASE WHEN min_version('3.1.0') THEN plan(84) ELSE plan(71) END; SELECT no_crash_dijkstra('pgr_dijkstra'); SELECT throw_on_empty_edges_sql('pgr_dijkstra', ',1,2'); +SELECT is_empty( + $$SELECT * FROM pgr_dijkstra('SELECT id, source, target, cost FROM edges WHERE false', 1, 2)$$, + 'zero-row edges_sql returns empty set without crashing'); +SELECT throws_ok( + $$SELECT * FROM pgr_dijkstra('SELECT id, source FROM edges WHERE false', 1, 2)$$, + 'XX000', 'Column ''cost'' not Found', + 'zero-row edges_sql with missing column raises a clean error'); SELECT finish();