]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Reject non-finite reltuples when restoring stats
authorTomas Vondra <tomas.vondra@postgresql.org>
Thu, 30 Jul 2026 13:30:19 +0000 (15:30 +0200)
committerTomas Vondra <tomas.vondra@postgresql.org>
Thu, 30 Jul 2026 14:33:24 +0000 (16:33 +0200)
When restoring relation stats, pg_restore_relation_stats() rejected
calls with (reltuples < -1.0). But that is insufficient - Infinity and
NaN values both pass that check, and get stored in pg_class verbatim.
This can have various undesirable consequences.

Fixed by rejecting non-finite reltuple values, in the same non-fatal way
as for the existing checks (emit WARNING and skip the update). Adds a
regression test to stats_import for these non-finite values, and to
check the -1.0 special value is still accepted.

Backpatch to 18, where pg_restore_relation_stats() was introduced.

Patch by Jan Nidzwetzki, minor commit message tweaks by me.

Author: Jan Nidzwetzki <jan@planetscale.com>
Discussion: https://postgr.es/m/518BA772-8026-412A-AA8F-A7FE4C6B3717@planetscale.com
Backpatch-through: 18

src/backend/statistics/relation_stats.c
src/test/regress/expected/stats_import.out
src/test/regress/sql/stats_import.sql

index fbaab92284fa3803df3ef5b008035abdbfd0912a..9de5d64c38400840d39638f3521b259d8c284a3d 100644 (file)
@@ -17,6 +17,8 @@
 
 #include "postgres.h"
 
+#include <math.h>
+
 #include "access/heapam.h"
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
@@ -123,7 +125,14 @@ relation_statistics_update_internal(Oid reloid, FunctionCallInfo fcinfo)
        if (!PG_ARGISNULL(RELTUPLES_ARG))
        {
                reltuples = PG_GETARG_FLOAT4(RELTUPLES_ARG);
-               if (reltuples < -1.0)
+               if (isnan(reltuples) || isinf(reltuples))
+               {
+                       ereport(WARNING,
+                                       (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                                        errmsg("argument \"%s\" must be a finite value", "reltuples")));
+                       result = false;
+               }
+               else if (reltuples < -1.0)
                {
                        ereport(WARNING,
                                        (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
index f2ccb80cf62fb92f118157935998c5b2e00ea4cd..8dcec57cc30fa756d16b7fbf0b1555c9d30cbbb0 100644 (file)
@@ -440,6 +440,85 @@ WHERE oid = 'stats_import.test'::regclass;
        16 |       500 |             4 |            2
 (1 row)
 
+-- error: reltuples must be finite (rejected with WARNING, returns false)
+SELECT pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relname', 'test',
+        'reltuples', 'Infinity'::real);
+WARNING:  argument "reltuples" must be a finite value
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+SELECT pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relname', 'test',
+        'reltuples', '-Infinity'::real);
+WARNING:  argument "reltuples" must be a finite value
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+SELECT pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relname', 'test',
+        'reltuples', 'NaN'::real);
+WARNING:  argument "reltuples" must be a finite value
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- error: reltuples must not be less than -1.0 (rejected with WARNING, returns false)
+SELECT pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relname', 'test',
+        'reltuples', '-5'::real);
+WARNING:  argument "reltuples" must not be less than -1.0
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- reltuples is unchanged (still 500) after the rejected values above
+SELECT relpages, reltuples, relallvisible, relallfrozen
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible | relallfrozen 
+----------+-----------+---------------+--------------
+       16 |       500 |             4 |            2
+(1 row)
+
+-- ok: -1 (the "unknown" sentinel) is still accepted
+SELECT pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relname', 'test',
+        'reltuples', '-1'::real);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
+SELECT relpages, reltuples, relallvisible, relallfrozen
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible | relallfrozen 
+----------+-----------+---------------+--------------
+       16 |        -1 |             4 |            2
+(1 row)
+
+-- restore reltuples to 500 for the following tests
+SELECT pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relname', 'test',
+        'reltuples', '500'::real);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
 -- ok: set just relallvisible, rest stay same
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',
index 650ce324c7e47492992ed0be4f9f8c6dfedb3b3f..10843f124477f1ae585aedc3040a421c296f14d3 100644 (file)
@@ -365,6 +365,49 @@ SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
+-- error: reltuples must be finite (rejected with WARNING, returns false)
+SELECT pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relname', 'test',
+        'reltuples', 'Infinity'::real);
+
+SELECT pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relname', 'test',
+        'reltuples', '-Infinity'::real);
+
+SELECT pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relname', 'test',
+        'reltuples', 'NaN'::real);
+
+-- error: reltuples must not be less than -1.0 (rejected with WARNING, returns false)
+SELECT pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relname', 'test',
+        'reltuples', '-5'::real);
+
+-- reltuples is unchanged (still 500) after the rejected values above
+SELECT relpages, reltuples, relallvisible, relallfrozen
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- ok: -1 (the "unknown" sentinel) is still accepted
+SELECT pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relname', 'test',
+        'reltuples', '-1'::real);
+
+SELECT relpages, reltuples, relallvisible, relallfrozen
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- restore reltuples to 500 for the following tests
+SELECT pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relname', 'test',
+        'reltuples', '500'::real);
+
 -- ok: set just relallvisible, rest stay same
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',