The pg_class fields relpages, relallvisible, and relallfrozen are of
type int32, but the statistics restoration code internally dealt with
them as uint32 for a little bit, after which they would turn back into
int32. This all happens to work, but it doesn't make sense, so I
propose the attached fix.
While researching this, I noticed that we don't appear to document how
page/block counts larger than INT32_MAX are represented in the catalogs.
A C programmer would have a certain expectation, but we shouldn't
expect everyone to guess that. So I'm proposing a small addition to the
catalog documentation to clarify this.
From 1c4efffcb2e8b78da84d5c3fa47d11f3a68dd2a0 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Tue, 11 Aug 2026 15:48:27 +0200
Subject: [PATCH 1/2] Fix signed/unsigned integer handling in
pg_restore_relation_stats()
The pg_class fields relpages, relallvisible, and relallfrozen are of
type int32, but the statistics restoration code internally dealt with
them as uint32 for a little bit, after which they would turn back into
int32. This doesn't make sense, so fix it to use int32 consistently
throughout.
---
contrib/postgres_fdw/postgres_fdw.c | 23 +----------------------
src/backend/statistics/relation_stats.c | 22 +++++++++++-----------
2 files changed, 12 insertions(+), 33 deletions(-)
diff --git a/contrib/postgres_fdw/postgres_fdw.c
b/contrib/postgres_fdw/postgres_fdw.c
index 479a40719bd..fb20bd7969b 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -597,7 +597,6 @@ static bool import_fetched_statistics(Relation relation,
static char *get_opt_value(PGresult *res, int row, int col);
static void set_text_arg(NullableDatum *arg, const char *s);
static void set_int32_arg(NullableDatum *arg, const char *s);
-static void set_uint32_arg(NullableDatum *arg, const char *s);
static void set_float_arg(NullableDatum *arg, const char *s);
static void set_floatarr_arg(NullableDatum *arg, const char *s);
static void produce_tuple_asynchronously(AsyncRequest *areq, bool fetch);
@@ -6105,7 +6104,7 @@ import_fetched_statistics(Relation relation,
Assert(PQntuples(res) == 1);
/* Set the remaining parameters. */
- set_uint32_arg(&args[1], get_opt_value(res, 0, RELSTATS_RELPAGES));
+ set_int32_arg(&args[1], get_opt_value(res, 0, RELSTATS_RELPAGES));
Assert(!args[1].isnull);
set_float_arg(&args[2], get_opt_value(res, 0, RELSTATS_RELTUPLES));
Assert(!args[2].isnull);
@@ -6176,26 +6175,6 @@ set_int32_arg(NullableDatum *arg, const char *s)
}
}
-/*
- * Convenience routine for setting optional uint32 arguments
- */
-static void
-set_uint32_arg(NullableDatum *arg, const char *s)
-{
- if (s)
- {
- uint32 val = uint32in_subr(s, NULL, "uint32", NULL);
-
- arg->value = UInt32GetDatum(val);
- arg->isnull = false;
- }
- else
- {
- arg->value = (Datum) 0;
- arg->isnull = true;
- }
-}
-
/*
* Convenience routine for setting optional float arguments
*/
diff --git a/src/backend/statistics/relation_stats.c
b/src/backend/statistics/relation_stats.c
index 9de5d64c384..d9bc77e369c 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -99,13 +99,13 @@ relation_statistics_update(FunctionCallInfo fcinfo)
static bool
relation_statistics_update_internal(Oid reloid, FunctionCallInfo fcinfo)
{
- BlockNumber relpages = 0;
+ int32 relpages = 0;
bool update_relpages = false;
float reltuples = 0;
bool update_reltuples = false;
- BlockNumber relallvisible = 0;
+ int32 relallvisible = 0;
bool update_relallvisible = false;
- BlockNumber relallfrozen = 0;
+ int32 relallfrozen = 0;
bool update_relallfrozen = false;
Relation crel;
HeapTuple ctup;
@@ -145,13 +145,13 @@ relation_statistics_update_internal(Oid reloid,
FunctionCallInfo fcinfo)
if (!PG_ARGISNULL(RELALLVISIBLE_ARG))
{
- relallvisible = PG_GETARG_UINT32(RELALLVISIBLE_ARG);
+ relallvisible = PG_GETARG_INT32(RELALLVISIBLE_ARG);
update_relallvisible = true;
}
if (!PG_ARGISNULL(RELALLFROZEN_ARG))
{
- relallfrozen = PG_GETARG_UINT32(RELALLFROZEN_ARG);
+ relallfrozen = PG_GETARG_INT32(RELALLFROZEN_ARG);
update_relallfrozen = true;
}
@@ -170,7 +170,7 @@ relation_statistics_update_internal(Oid reloid,
FunctionCallInfo fcinfo)
if (update_relpages && relpages != pgcform->relpages)
{
replaces[nreplaces] = Anum_pg_class_relpages;
- values[nreplaces] = UInt32GetDatum(relpages);
+ values[nreplaces] = Int32GetDatum(relpages);
nreplaces++;
}
@@ -184,14 +184,14 @@ relation_statistics_update_internal(Oid reloid,
FunctionCallInfo fcinfo)
if (update_relallvisible && relallvisible != pgcform->relallvisible)
{
replaces[nreplaces] = Anum_pg_class_relallvisible;
- values[nreplaces] = UInt32GetDatum(relallvisible);
+ values[nreplaces] = Int32GetDatum(relallvisible);
nreplaces++;
}
if (update_relallfrozen && relallfrozen != pgcform->relallfrozen)
{
replaces[nreplaces] = Anum_pg_class_relallfrozen;
- values[nreplaces] = UInt32GetDatum(relallfrozen);
+ values[nreplaces] = Int32GetDatum(relallfrozen);
nreplaces++;
}
@@ -231,13 +231,13 @@ pg_clear_relation_stats(PG_FUNCTION_ARGS)
newfcinfo->args[0].isnull = PG_ARGISNULL(0);
newfcinfo->args[1].value = PG_GETARG_DATUM(1);
newfcinfo->args[1].isnull = PG_ARGISNULL(1);
- newfcinfo->args[2].value = UInt32GetDatum(0);
+ newfcinfo->args[2].value = Int32GetDatum(0);
newfcinfo->args[2].isnull = false;
newfcinfo->args[3].value = Float4GetDatum(-1.0);
newfcinfo->args[3].isnull = false;
- newfcinfo->args[4].value = UInt32GetDatum(0);
+ newfcinfo->args[4].value = Int32GetDatum(0);
newfcinfo->args[4].isnull = false;
- newfcinfo->args[5].value = UInt32GetDatum(0);
+ newfcinfo->args[5].value = Int32GetDatum(0);
newfcinfo->args[5].isnull = false;
relation_statistics_update(newfcinfo);
--
2.55.0
From 8d5a733fa48dbf5a97ca48be8a71af7c1431f8d5 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Tue, 11 Aug 2026 15:47:27 +0200
Subject: [PATCH 2/2] doc: Document overflow handling of pg_class.relpages etc.
The pg_class fields relpages, relallvisible, and relallfrozen are
stored as int32 in the catalog but block numbers are internally of
type uint32. This all works correctly because the values are
implicitly converted back and forth according to C conversion rules.
But this was apparently never explicitly documented, so add this.
---
doc/src/sgml/catalogs.sgml | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 6066c4784f4..b6e61196922 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2057,6 +2057,10 @@ <title><structname>pg_class</structname> Columns</title>
planner. It is updated by <link
linkend="sql-vacuum"><command>VACUUM</command></link>,
<link linkend="sql-analyze"><command>ANALYZE</command></link>, and a
few DDL commands such as
<link linkend="sql-createindex"><command>CREATE INDEX</command></link>.
+ </para>
+ <para>
+ This is effectively an unsigned 32-bit integer. Values larger than
+ 2^31−1 are stored as negative values.
</para></entry>
</row>
@@ -2086,6 +2090,10 @@ <title><structname>pg_class</structname> Columns</title>
planner. It is updated by <link
linkend="sql-vacuum"><command>VACUUM</command></link>,
<link linkend="sql-analyze"><command>ANALYZE</command></link>, and a
few DDL commands such as
<link linkend="sql-createindex"><command>CREATE INDEX</command></link>.
+ </para>
+ <para>
+ This is effectively an unsigned 32-bit integer. Values larger than
+ 2^31−1 are stored as negative values.
</para></entry>
</row>
@@ -2106,6 +2114,10 @@ <title><structname>pg_class</structname> Columns</title>
<link linkend="sql-analyze"><command>ANALYZE</command></link>,
and a few DDL commands such as
<link linkend="sql-createindex"><command>CREATE INDEX</command></link>.
+ </para>
+ <para>
+ This is effectively an unsigned 32-bit integer. Values larger than
+ 2^31−1 are stored as negative values.
</para></entry>
</row>
--
2.55.0