From 751162fb6701d84747c753457a995833ff8313bb Mon Sep 17 00:00:00 2001
From: Melanie Plageman <melanieplageman@gmail.com>
Date: Fri, 13 Nov 2026 15:43:23 -0500
Subject: [PATCH v1] Fix pgstattuple_approx handling of unknown tuple estimates

vac_estimate_reltuples() can return -1 when there is not enough
information to estimate the number of live tuples. pgstattuple_approx()
assigned that value to an unsigned counter before attempting to clamp
it, making the clamp ineffective and exposing -1 through the bigint
result.

Clamp the signed estimate before assigning it to the unsigned counter.

Reported-by: Zsolt Parragi <zsolt.parragi@percona.com>
Author: Melanie Plageman <melanieplageman@gmail.com>
Discussion: https://postgr.es/m/CAN4CZFM0y8t%2BAQ8PWTeuBXnFtRDUNvkfVzzA_yo6uJ2bceK%3DxA%40mail.gmail.com
Backpatch-through: 15
---
 contrib/pgstattuple/expected/pgstattuple.out | 12 ++++++++++++
 contrib/pgstattuple/pgstatapprox.c           | 11 ++++++-----
 contrib/pgstattuple/sql/pgstattuple.sql      | 10 ++++++++++
 doc/src/sgml/pgstattuple.sgml                |  3 ++-
 4 files changed, 30 insertions(+), 6 deletions(-)

diff --git a/contrib/pgstattuple/expected/pgstattuple.out b/contrib/pgstattuple/expected/pgstattuple.out
index 9176dc98b6a..32f055b2d53 100644
--- a/contrib/pgstattuple/expected/pgstattuple.out
+++ b/contrib/pgstattuple/expected/pgstattuple.out
@@ -250,6 +250,18 @@ select pg_relpages((select reltoastrelid from pg_class where relname = 'test'));
            0
 (1 row)
 
+-- An unknown approximate tuple count is reported as zero.
+begin;
+create temp table test_approx_unknown (id int);
+copy test_approx_unknown from stdin with (freeze);
+commit;
+select scanned_percent, approx_tuple_count
+  from pgstattuple_approx('test_approx_unknown');
+ scanned_percent | approx_tuple_count 
+-----------------+--------------------
+               0 |                  0
+(1 row)
+
 -- not for the index calls though, of course
 select pgstatindex('test_partition');
 ERROR:  relation "test_partition" is not a btree index
diff --git a/contrib/pgstattuple/pgstatapprox.c b/contrib/pgstattuple/pgstatapprox.c
index bed904206d7..8fd5e8d877e 100644
--- a/contrib/pgstattuple/pgstatapprox.c
+++ b/contrib/pgstattuple/pgstatapprox.c
@@ -119,6 +119,7 @@ statapprox_heap(Relation rel, output_type *stat)
 {
 	BlockNumber nblocks;
 	BufferAccessStrategy bstrategy;
+	double		estimated_tuple_count;
 	TransactionId OldestXmin;
 	StatApproxReadStreamPrivate p;
 	ReadStream *stream;
@@ -254,12 +255,12 @@ statapprox_heap(Relation rel, output_type *stat)
 	 * tuples in all-visible pages, so no correction is needed for that, and
 	 * we already accounted for the space in those pages, too.
 	 */
-	stat->tuple_count = vac_estimate_reltuples(rel, nblocks, p.scanned,
-											   p.scanned_new,
-											   stat->tuple_count);
+	estimated_tuple_count = vac_estimate_reltuples(rel, nblocks, p.scanned,
+												   p.scanned_new,
+												   stat->tuple_count);
 
-	/* It's not clear if we could get -1 here, but be safe. */
-	stat->tuple_count = Max(stat->tuple_count, 0);
+	/* Report zero when there is not enough information for an estimate. */
+	stat->tuple_count = Max(estimated_tuple_count, 0);
 
 	/*
 	 * Calculate percentages if the relation has one or more pages.
diff --git a/contrib/pgstattuple/sql/pgstattuple.sql b/contrib/pgstattuple/sql/pgstattuple.sql
index 7e72c567a06..0853a23c985 100644
--- a/contrib/pgstattuple/sql/pgstattuple.sql
+++ b/contrib/pgstattuple/sql/pgstattuple.sql
@@ -107,6 +107,16 @@ select pgstattuple((select reltoastrelid from pg_class where relname = 'test'));
 select pgstattuple_approx((select reltoastrelid from pg_class where relname = 'test'));
 select pg_relpages((select reltoastrelid from pg_class where relname = 'test'));
 
+-- An unknown approximate tuple count is reported as zero.
+begin;
+create temp table test_approx_unknown (id int);
+copy test_approx_unknown from stdin with (freeze);
+1
+\.
+commit;
+select scanned_percent, approx_tuple_count
+  from pgstattuple_approx('test_approx_unknown');
+
 -- not for the index calls though, of course
 select pgstatindex('test_partition');
 select pgstatginindex('test_partition');
diff --git a/doc/src/sgml/pgstattuple.sgml b/doc/src/sgml/pgstattuple.sgml
index 54d8f90245e..8828f4f1272 100644
--- a/doc/src/sgml/pgstattuple.sgml
+++ b/doc/src/sgml/pgstattuple.sgml
@@ -572,7 +572,8 @@ approx_free_percent  | 2.09
         <row>
          <entry><structfield>approx_tuple_count</structfield></entry>
          <entry><type>bigint</type></entry>
-         <entry>Number of live tuples (estimated)</entry>
+         <entry>Number of live tuples (estimated), or zero if there is not
+         enough information to estimate it</entry>
         </row>
         <row>
          <entry><structfield>approx_tuple_len</structfield></entry>
-- 
2.43.0

