On Wed, Aug 12, 2026 at 7:00 AM Etsuro Fujita <[email protected]>
wrote:

> On Wed, Aug 12, 2026 at 2:31 AM Corey Huinker <[email protected]>
> wrote:
> >> There is one thing I'd like to change for v19: the name of the option
> >> for this feature.  We named it restore_stats because it used the
> >> pg_restore_*() functions [1], but it doesn't use them anymore; it uses
> >> import_*_statistics() instead, so I think we should rename it to
> >> import_stats if not too late.  I think it also matches the names of
> >> functions added to postgres_fdw.c like import_fetched_statistics().
> >> What do you think?
> >
> > I think it's worth a quick patch...
>
> The patch looks good to me.  I will push/backpatch it if there are no
> objections from others.
>
> Thanks for the patch!
>
> Best regards,
> Etsuro Fujita
>


With that out of the way, I'm re-attaching the v2-0010 and v2-0012 patches
from my earlier set to show what the NullableDatum array endgame looks,
though those patchsets assume that some of the enumerations are externally
visible, and that would no longer be the case. The prefixes on the the
enums help the coder to avoid using the wrong one.  -- OR -- we probably
could do an index offset like we do with the Anum_relname_attname - 1 tuple
indexes, which then requires that we pack all of the actual-stat column
values at the end, which is easy. Overall I don't think either of those
patterns is better or worse a solution than explicitly named parameters, so
it comes down to preference, and I'll rework the patchset to match those
preferences.
From 34cc6ac8697c368b23583c18e9172747218b8b02 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email protected]>
Date: Sun, 28 Jun 2026 21:51:56 -0500
Subject: [PATCH v2 10/13] Add relation_statistics_update, refactor
 update_relstats.

Introduce a new enum relation_stats_argnum, which is a subset of
relation_args_argnum but contains only the statistical values.

Modify update_relstats to take a Relation argument, and index the
NullableDatum array by the new enum relation_stats_argnum. All
processing and validation of non-statistical values like schema and
relname, as well as checking for recovery mode and acquiring the lock on
the relation are now handled in the calling functions
pg_restore_relation_stats and pg_clear_relation_stats. In turn, those
functions must now pass the shorter, statistics-only array of
NullableDatums, as well as the Relation argument that is now their
responsibility to open and close.

Create a new function relation_statistics_update which takes a Relation
argument and arrays of isnull and cstring arguments, which will be
translated into the Datum values required by update_relstats().

The end result is that all three user-facing functions are able to call
the same update_relstats().
---
 src/backend/statistics/relation_stats.c | 244 +++++++++++++++++++-----
 src/include/statistics/relation_stats.h |  30 +++
 2 files changed, 223 insertions(+), 51 deletions(-)
 create mode 100644 src/include/statistics/relation_stats.h

diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index d4459981ebc..21baa0abb55 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -21,6 +21,7 @@
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
 #include "nodes/makefuncs.h"
+#include "statistics/relation_stats.h"
 #include "statistics/stat_utils.h"
 #include "utils/builtins.h"
 #include "utils/fmgroids.h"
@@ -56,18 +57,15 @@ static struct StatsArgInfo relarginfo[] =
 	[RELARG_NUM_RELARGS] = {0}
 };
 
-static bool update_relstats(const NullableDatum *args);
+static bool update_relstats(Relation rel, const NullableDatum *args);
 
 /*
  * Internal function for modifying statistics for a relation.
  */
 static bool
-update_relstats(const NullableDatum *args)
+update_relstats(Relation rel, const NullableDatum *args)
 {
 	bool		result = true;
-	char	   *nspname;
-	char	   *relname;
-	Oid			reloid;
 	Relation	crel;
 	BlockNumber relpages = 0;
 	bool		update_relpages = false;
@@ -83,33 +81,16 @@ update_relstats(const NullableDatum *args)
 	Datum		values[4] = {0};
 	bool		nulls[4] = {0};
 	int			nreplaces = 0;
-	Oid			locked_table = InvalidOid;
-
-	stats_check_required_arg(args, relarginfo, RELARG_SCHEMA);
-	stats_check_required_arg(args, relarginfo, RELARG_RELNAME);
-
-	nspname = TextDatumGetCString(args[RELARG_SCHEMA].value);
-	relname = TextDatumGetCString(args[RELARG_RELNAME].value);
-
-	if (RecoveryInProgress())
-		ereport(ERROR,
-				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
-				 errmsg("recovery is in progress"),
-				 errhint("Statistics cannot be modified during recovery.")));
-
-	reloid = RangeVarGetRelidExtended(makeRangeVar(nspname, relname, -1),
-									  ShareUpdateExclusiveLock, 0,
-									  RangeVarCallbackForStats, &locked_table);
 
-	if (!args[RELARG_RELPAGES].isnull)
+	if (!args[RELSTAT_RELPAGES].isnull)
 	{
-		relpages = DatumGetUInt32(args[RELARG_RELPAGES].value);
+		relpages = DatumGetUInt32(args[RELSTAT_RELPAGES].value);
 		update_relpages = true;
 	}
 
-	if (!args[RELARG_RELTUPLES].isnull)
+	if (!args[RELSTAT_RELTUPLES].isnull)
 	{
-		reltuples = DatumGetFloat4(args[RELARG_RELTUPLES].value);
+		reltuples = DatumGetFloat4(args[RELSTAT_RELTUPLES].value);
 		if (reltuples < -1.0)
 		{
 			ereport(WARNING,
@@ -121,15 +102,15 @@ update_relstats(const NullableDatum *args)
 			update_reltuples = true;
 	}
 
-	if (!args[RELARG_RELALLVISIBLE].isnull)
+	if (!args[RELSTAT_RELALLVISIBLE].isnull)
 	{
-		relallvisible = DatumGetUInt32(args[RELARG_RELALLVISIBLE].value);
+		relallvisible = DatumGetUInt32(args[RELSTAT_RELALLVISIBLE].value);
 		update_relallvisible = true;
 	}
 
-	if (!args[RELARG_RELALLFROZEN].isnull)
+	if (!args[RELSTAT_RELALLFROZEN].isnull)
 	{
-		relallfrozen = DatumGetUInt32(args[RELARG_RELALLFROZEN].value);
+		relallfrozen = DatumGetUInt32(args[RELSTAT_RELALLFROZEN].value);
 		update_relallfrozen = true;
 	}
 
@@ -139,9 +120,9 @@ update_relstats(const NullableDatum *args)
 	 */
 	crel = table_open(RelationRelationId, RowExclusiveLock);
 
-	ctup = SearchSysCache1(RELOID, ObjectIdGetDatum(reloid));
+	ctup = SearchSysCache1(RELOID, ObjectIdGetDatum(RelationGetRelid(rel)));
 	if (!HeapTupleIsValid(ctup))
-		elog(ERROR, "pg_class entry for relid %u not found", reloid);
+		elog(ERROR, "pg_class entry for relid %u not found", RelationGetRelid(rel));
 
 	pgcform = (Form_pg_class) GETSTRUCT(ctup);
 
@@ -201,24 +182,55 @@ update_relstats(const NullableDatum *args)
 Datum
 pg_clear_relation_stats(PG_FUNCTION_ARGS)
 {
-	LOCAL_FCINFO(newfcinfo, 6);
-
-	InitFunctionCallInfoData(*newfcinfo, NULL, 6, InvalidOid, NULL, NULL);
-
-	newfcinfo->args[0].value = PG_GETARG_DATUM(0);
-	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].isnull = false;
-	newfcinfo->args[3].value = Float4GetDatum(-1.0);
-	newfcinfo->args[3].isnull = false;
-	newfcinfo->args[4].value = UInt32GetDatum(0);
-	newfcinfo->args[4].isnull = false;
-	newfcinfo->args[5].value = UInt32GetDatum(0);
-	newfcinfo->args[5].isnull = false;
-
-	update_relstats(newfcinfo->args);
+	NullableDatum positional_args[RELARG_NUM_RELARGS];
+	NullableDatum stats[RELSTAT_NUM_RELSTATS];
+	char	   *nspname;
+	char	   *relname;
+	Oid			reloid;
+	Oid			locked_table = InvalidOid;
+	Relation	rel;
+
+	/*
+	 * Fill out just enough of positional_args to do the same required checks
+	 * as pg_restore_relation_stats
+	 */
+	positional_args[RELARG_SCHEMA].isnull = PG_ARGISNULL(0);
+	positional_args[RELARG_SCHEMA].value = PG_GETARG_DATUM(0);
+	positional_args[RELARG_RELNAME].isnull = PG_ARGISNULL(1);
+	positional_args[RELARG_RELNAME].value = PG_GETARG_DATUM(1);
+
+	stats_check_required_arg(positional_args, relarginfo, RELARG_SCHEMA);
+	stats_check_required_arg(positional_args, relarginfo, RELARG_RELNAME);
+
+	nspname = TextDatumGetCString(positional_args[RELARG_SCHEMA].value);
+	relname = TextDatumGetCString(positional_args[RELARG_RELNAME].value);
+
+	if (RecoveryInProgress())
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("recovery is in progress"),
+				 errhint("Statistics cannot be modified during recovery.")));
+
+	reloid = RangeVarGetRelidExtended(makeRangeVar(nspname, relname, -1),
+									  ShareUpdateExclusiveLock, 0,
+									  RangeVarCallbackForStats, &locked_table);
+
+	stats[RELSTAT_RELPAGES].isnull = false;
+	stats[RELSTAT_RELPAGES].value = UInt32GetDatum(0);
+
+	stats[RELSTAT_RELTUPLES].isnull = false;
+	stats[RELSTAT_RELTUPLES].value = Float4GetDatum(-1.0);
+
+	stats[RELSTAT_RELALLVISIBLE].isnull = false;
+	stats[RELSTAT_RELALLVISIBLE].value = UInt32GetDatum(0);
+
+	stats[RELSTAT_RELALLFROZEN].isnull = false;
+	stats[RELSTAT_RELALLFROZEN].value = UInt32GetDatum(0);
+
+	rel = relation_open(reloid, NoLock);
+	update_relstats(rel, stats);
+	relation_close(rel, NoLock);
+
 	PG_RETURN_VOID();
 }
 
@@ -226,14 +238,144 @@ Datum
 pg_restore_relation_stats(PG_FUNCTION_ARGS)
 {
 	NullableDatum positional_args[RELARG_NUM_RELARGS];
+	NullableDatum stats[RELSTAT_NUM_RELSTATS];
+
+	char	   *nspname;
+	char	   *relname;
+	Oid			reloid;
+	Oid			locked_table = InvalidOid;
+	Relation	rel;
 	bool		result = true;
 
 	if (!stats_fill_args_from_arg_pairs(fcinfo, positional_args,
 										relarginfo))
 		result = false;
 
-	if (!update_relstats(positional_args))
+	stats_check_required_arg(positional_args, relarginfo, RELARG_SCHEMA);
+	stats_check_required_arg(positional_args, relarginfo, RELARG_RELNAME);
+
+	nspname = TextDatumGetCString(positional_args[RELARG_SCHEMA].value);
+	relname = TextDatumGetCString(positional_args[RELARG_RELNAME].value);
+
+	if (RecoveryInProgress())
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("recovery is in progress"),
+				 errhint("Statistics cannot be modified during recovery.")));
+
+	reloid = RangeVarGetRelidExtended(makeRangeVar(nspname, relname, -1),
+									  ShareUpdateExclusiveLock, 0,
+									  RangeVarCallbackForStats, &locked_table);
+
+	/* Map RELARGs to RELSTATs */
+	stats[RELSTAT_RELPAGES].isnull = positional_args[RELARG_RELPAGES].isnull;
+	stats[RELSTAT_RELPAGES].value = positional_args[RELARG_RELPAGES].value;
+
+	stats[RELSTAT_RELTUPLES].isnull = positional_args[RELARG_RELTUPLES].isnull;
+	stats[RELSTAT_RELTUPLES].value = positional_args[RELARG_RELTUPLES].value;
+
+	stats[RELSTAT_RELALLVISIBLE].isnull = positional_args[RELARG_RELALLVISIBLE].isnull;
+	stats[RELSTAT_RELALLVISIBLE].value = positional_args[RELARG_RELALLVISIBLE].value;
+
+	stats[RELSTAT_RELALLFROZEN].isnull = positional_args[RELARG_RELALLFROZEN].isnull;
+	stats[RELSTAT_RELALLFROZEN].value = positional_args[RELARG_RELALLFROZEN].value;
+
+	rel = relation_open(reloid, NoLock);
+
+	if (!update_relstats(rel, stats))
 		result = false;
 
+	relation_close(rel, NoLock);
+
 	PG_RETURN_BOOL(result);
 }
+
+/*
+ * Convenience routine to parse BlockNumber values, and emit a warning
+ * on parse errors.
+ */
+static void
+str_to_blocknumber(NullableDatum *stats, int statnum, const bool *isnull,
+				   const char **values)
+{
+	stats[statnum].isnull = true;
+	stats[statnum].value = 0;
+
+	if (!isnull[statnum])
+	{
+		const char *s = values[statnum];
+		BlockNumber result;
+		ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+		if (s == NULL)
+			elog(ERROR, "value is null but flag is non-null");
+
+		result = uint32in_subr(s, NULL, "BlockNumber", (Node *) &escontext);
+
+		if (escontext.error_occurred)
+		{
+			escontext.error_data->elevel = WARNING;
+			ThrowErrorData(escontext.error_data);
+			FreeErrorData(escontext.error_data);
+			return;
+		}
+		else
+		{
+			stats[statnum].isnull = false;
+			stats[statnum].value = UInt32GetDatum(result);
+		}
+	}
+}
+
+/*
+ * Convenience routine to parse float values, and emit a warning on parse
+ * errors.
+ */
+static void
+str_to_float(NullableDatum *stats, int statnum, const bool *isnull,
+			 const char **values)
+{
+	stats[statnum].isnull = true;
+	stats[statnum].value = 0;
+
+	if (!isnull[statnum])
+	{
+		const char *s = values[statnum];
+		Datum		value;
+		ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+		if (s == NULL)
+			elog(ERROR, "value is null but flag is non-null");
+
+		if (DirectInputFunctionCallSafe(float4in, (char *) s, InvalidOid, -1,
+										(Node *) &escontext, &value))
+		{
+			stats[statnum].isnull = false;
+			stats[statnum].value = value;
+		}
+		else
+		{
+			escontext.error_data->elevel = WARNING;
+			ThrowErrorData(escontext.error_data);
+			FreeErrorData(escontext.error_data);
+			return;
+		}
+	}
+}
+
+/*
+ * Update statistics for an already opened Relation with a lock level of at least
+ * ShareUpdateExclusiveLock.
+ */
+bool
+relation_statistics_update(Relation rel, const bool *isnull, const char **values)
+{
+	NullableDatum stats[RELSTAT_NUM_RELSTATS];
+
+	str_to_blocknumber(stats, RELSTAT_RELPAGES, isnull, values);
+	str_to_float(stats, RELSTAT_RELTUPLES, isnull, values);
+	str_to_blocknumber(stats, RELSTAT_RELALLVISIBLE, isnull, values);
+	str_to_blocknumber(stats, RELSTAT_RELALLFROZEN, isnull, values);
+
+	return update_relstats(rel, stats);
+}
diff --git a/src/include/statistics/relation_stats.h b/src/include/statistics/relation_stats.h
new file mode 100644
index 00000000000..56ce7ef3e66
--- /dev/null
+++ b/src/include/statistics/relation_stats.h
@@ -0,0 +1,30 @@
+/*-------------------------------------------------------------------------
+ *
+ * relation_stats.h
+ *    Functions for the internal manipulation of relation statistics.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/statistics/relation_stats.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef RELATION_STATS_H
+
+#include "access/genam.h"
+
+enum relation_stats_argnum
+{
+	RELSTAT_RELPAGES,
+	RELSTAT_RELTUPLES,
+	RELSTAT_RELALLVISIBLE,
+	RELSTAT_RELALLFROZEN,
+	RELSTAT_NUM_RELSTATS
+};
+
+extern bool relation_statistics_update(Relation rel, const bool *isnull,
+									   const char **values);
+
+#define RELATION_STATS_H
+#endif
-- 
2.50.1 (Apple Git-155)

From 4b832c5c733f6a919547287c9faf15ffa45d23c4 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email protected]>
Date: Sun, 28 Jun 2026 23:32:33 -0500
Subject: [PATCH v2 12/13] Add attribute_statistics_update, refactor
 update_attstats.

Introduce a new enum attribute_stats_argnum, which is a subset of
attribute_args_argnum but contains only the statistical values.

Modify update_attstats to take a Relation argument, and index the
NullableDatum array by the new enum attribute_stats_argnum. All
processing and validation of non-statistical values like schema,
relname, attname, attnum, as well as checking for recovery mode and
acquiring the lock on the attribute are now handled in the calling
functions pg_restore_attribute_stats and pg_clear_attribute_stats. In
turn, those functions must now pass the shorter, statistics-only array
of NullableDatums, as well as the Relation argument that is now their
responsibility to open and close.

Create a new function attribute_statistics_update which takes arguments
for Relation, attname, attnum, inherited, and arrays of isnull and
cstring values arguments, which will be translated into the Datum values
required by update_attstats().

The end result is that all user-facing functions are able to call the
same update_attstats().
---
 src/backend/statistics/attribute_stats.c | 419 +++++++++++++++++------
 src/include/statistics/attribute_stats.h |  45 +++
 2 files changed, 356 insertions(+), 108 deletions(-)
 create mode 100644 src/include/statistics/attribute_stats.h

diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 24c3204e837..c557f524b9b 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -22,6 +22,7 @@
 #include "catalog/namespace.h"
 #include "catalog/pg_operator.h"
 #include "nodes/makefuncs.h"
+#include "statistics/attribute_stats.h"
 #include "statistics/statistics.h"
 #include "statistics/stat_utils.h"
 #include "utils/array.h"
@@ -81,6 +82,31 @@ static struct StatsArgInfo attarginfo[] =
 	[ATTARG_NUM_ATTARGS] = {0}
 };
 
+static struct StatsArgInfo attstatinfo[] =
+{
+	[ATTSTAT_NULL_FRAC] = {"null_frac", FLOAT4OID},
+	[ATTSTAT_AVG_WIDTH] = {"avg_width", INT4OID},
+	[ATTSTAT_N_DISTINCT] = {"n_distinct", FLOAT4OID},
+	[ATTSTAT_MOST_COMMON_VALS] = {"most_common_vals", TEXTOID},
+	[ATTSTAT_MOST_COMMON_FREQS] = {"most_common_freqs", FLOAT4ARRAYOID},
+	[ATTSTAT_HISTOGRAM_BOUNDS] = {"histogram_bounds", TEXTOID},
+	[ATTSTAT_CORRELATION] = {"correlation", FLOAT4OID},
+	[ATTSTAT_MOST_COMMON_ELEMS] = {"most_common_elems", TEXTOID},
+	[ATTSTAT_MOST_COMMON_ELEM_FREQS] = {"most_common_elem_freqs", FLOAT4ARRAYOID},
+	[ATTSTAT_ELEM_COUNT_HISTOGRAM] = {"elem_count_histogram", FLOAT4ARRAYOID},
+	[ATTSTAT_RANGE_LENGTH_HISTOGRAM] = {"range_length_histogram", TEXTOID},
+	[ATTSTAT_RANGE_EMPTY_FRAC] = {"range_empty_frac", FLOAT4OID},
+	[ATTSTAT_RANGE_BOUNDS_HISTOGRAM] = {"range_bounds_histogram", TEXTOID},
+	[ATTSTAT_NUM_ATTSTATS] = {0}
+};
+
+/*
+ * The order of statisics in attribute_args_argnum is the same as
+ * attribute_stats_argnum so when mapping Datums from one to the other we
+ * can use this offset.
+ */
+#define ARGS_STATS_OFFSET (ATTARG_NUM_ATTARGS - ATTSTAT_NUM_ATTSTATS)
+
 /*
  * Positional argument numbers, names, and types for
  * pg_clear_attribute_stats().
@@ -104,7 +130,8 @@ static struct StatsArgInfo cleararginfo[] =
 	[C_ATTARG_NUM_ATTARGS] = {0}
 };
 
-static bool update_attstats(const NullableDatum *args);
+static bool update_attstats(Relation rel, const char *attname, AttrNumber attnum,
+							bool inherited, const NullableDatum *args);
 static void upsert_pg_statistic(Relation starel, HeapTuple oldtup,
 								const Datum *values, const bool *nulls, const bool *replaces);
 static bool delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit);
@@ -126,16 +153,9 @@ static bool delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit);
  * and other statistic kinds may still be updated.
  */
 static bool
-update_attstats(const NullableDatum *args)
+update_attstats(Relation rel, const char *attname, AttrNumber attnum,
+				bool inherited, const NullableDatum *args)
 {
-	char	   *nspname;
-	char	   *relname;
-	Oid			reloid;
-	char	   *attname;
-	AttrNumber	attnum;
-	bool		inherited;
-	Oid			locked_table = InvalidOid;
-
 	Relation	starel;
 	HeapTuple	statup;
 
@@ -151,16 +171,16 @@ update_attstats(const NullableDatum *args)
 
 	FmgrInfo	array_in_fn;
 
-	bool		do_mcv = !args[ATTARG_MOST_COMMON_FREQS].isnull &&
-		!args[ATTARG_MOST_COMMON_VALS].isnull;
-	bool		do_histogram = !args[ATTARG_HISTOGRAM_BOUNDS].isnull;
-	bool		do_correlation = !args[ATTARG_CORRELATION].isnull;
-	bool		do_mcelem = !args[ATTARG_MOST_COMMON_ELEMS].isnull &&
-		!args[ATTARG_MOST_COMMON_ELEM_FREQS].isnull;
-	bool		do_dechist = !args[ATTARG_ELEM_COUNT_HISTOGRAM].isnull;
-	bool		do_bounds_histogram = !args[ATTARG_RANGE_BOUNDS_HISTOGRAM].isnull;
-	bool		do_range_length_histogram = !args[ATTARG_RANGE_LENGTH_HISTOGRAM].isnull &&
-		!args[ATTARG_RANGE_EMPTY_FRAC].isnull;
+	bool		do_mcv = !args[ATTSTAT_MOST_COMMON_FREQS].isnull &&
+		!args[ATTSTAT_MOST_COMMON_VALS].isnull;
+	bool		do_histogram = !args[ATTSTAT_HISTOGRAM_BOUNDS].isnull;
+	bool		do_correlation = !args[ATTSTAT_CORRELATION].isnull;
+	bool		do_mcelem = !args[ATTSTAT_MOST_COMMON_ELEMS].isnull &&
+		!args[ATTSTAT_MOST_COMMON_ELEM_FREQS].isnull;
+	bool		do_dechist = !args[ATTSTAT_ELEM_COUNT_HISTOGRAM].isnull;
+	bool		do_bounds_histogram = !args[ATTSTAT_RANGE_BOUNDS_HISTOGRAM].isnull;
+	bool		do_range_length_histogram = !args[ATTSTAT_RANGE_LENGTH_HISTOGRAM].isnull &&
+		!args[ATTSTAT_RANGE_EMPTY_FRAC].isnull;
 
 	Datum		values[Natts_pg_statistic] = {0};
 	bool		nulls[Natts_pg_statistic] = {0};
@@ -168,116 +188,59 @@ update_attstats(const NullableDatum *args)
 
 	bool		result = true;
 
-	stats_check_required_arg(args, attarginfo, ATTARG_ATTRELSCHEMA);
-	stats_check_required_arg(args, attarginfo, ATTARG_ATTRELNAME);
-
-	nspname = TextDatumGetCString(args[ATTARG_ATTRELSCHEMA].value);
-	relname = TextDatumGetCString(args[ATTARG_ATTRELNAME].value);
-
-	if (RecoveryInProgress())
-		ereport(ERROR,
-				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
-				 errmsg("recovery is in progress"),
-				 errhint("Statistics cannot be modified during recovery.")));
-
-	/* lock before looking up attribute */
-	reloid = RangeVarGetRelidExtended(makeRangeVar(nspname, relname, -1),
-									  ShareUpdateExclusiveLock, 0,
-									  RangeVarCallbackForStats, &locked_table);
-
-	/* user can specify either attname or attnum, but not both */
-	if (!args[ATTARG_ATTNAME].isnull)
-	{
-		if (!args[ATTARG_ATTNUM].isnull)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("cannot specify both \"%s\" and \"%s\"", "attname", "attnum")));
-		attname = TextDatumGetCString(args[ATTARG_ATTNAME].value);
-		attnum = get_attnum(reloid, attname);
-		/* note that this test covers attisdropped cases too: */
-		if (attnum == InvalidAttrNumber)
-			ereport(ERROR,
-					(errcode(ERRCODE_UNDEFINED_COLUMN),
-					 errmsg("column \"%s\" of relation \"%s\" does not exist",
-							attname, relname)));
-	}
-	else if (!args[ATTARG_ATTNUM].isnull)
-	{
-		attnum = DatumGetInt16(args[ATTARG_ATTNUM].value);
-		attname = get_attname(reloid, attnum, true);
-		/* annoyingly, get_attname doesn't check attisdropped */
-		if (attname == NULL ||
-			!SearchSysCacheExistsAttName(reloid, attname))
-			ereport(ERROR,
-					(errcode(ERRCODE_UNDEFINED_COLUMN),
-					 errmsg("column %d of relation \"%s\" does not exist",
-							attnum, relname)));
-	}
-	else
-	{
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("must specify either \"%s\" or \"%s\"", "attname", "attnum")));
-		attname = NULL;			/* keep compiler quiet */
-		attnum = 0;
-	}
-
 	if (attnum < 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot modify statistics on system column \"%s\"",
 						attname)));
 
-	stats_check_required_arg(args, attarginfo, ATTARG_INHERITED);
-	inherited = DatumGetBool(args[ATTARG_INHERITED].value);
-
 	/*
 	 * Check argument sanity. If some arguments are unusable, emit a WARNING
 	 * and set the corresponding argument to NULL in fcinfo.
 	 */
 
-	if (!stats_check_arg_array(args, attarginfo, ATTARG_MOST_COMMON_FREQS))
+	if (!stats_check_arg_array(args, attstatinfo, ATTSTAT_MOST_COMMON_FREQS))
 	{
 		do_mcv = false;
 		result = false;
 	}
 
-	if (!stats_check_arg_array(args, attarginfo, ATTARG_MOST_COMMON_ELEM_FREQS))
+	if (!stats_check_arg_array(args, attstatinfo, ATTSTAT_MOST_COMMON_ELEM_FREQS))
 	{
 		do_mcelem = false;
 		result = false;
 	}
-	if (!stats_check_arg_array(args, attarginfo, ATTARG_ELEM_COUNT_HISTOGRAM))
+	if (!stats_check_arg_array(args, attstatinfo, ATTSTAT_ELEM_COUNT_HISTOGRAM))
 	{
 		do_dechist = false;
 		result = false;
 	}
 
-	if (!stats_check_arg_pair(args, attarginfo,
-							  ATTARG_MOST_COMMON_VALS, ATTARG_MOST_COMMON_FREQS))
+	if (!stats_check_arg_pair(args, attstatinfo,
+							  ATTSTAT_MOST_COMMON_VALS, ATTSTAT_MOST_COMMON_FREQS))
 	{
 		do_mcv = false;
 		result = false;
 	}
 
-	if (!stats_check_arg_pair(args, attarginfo,
-							  ATTARG_MOST_COMMON_ELEMS,
-							  ATTARG_MOST_COMMON_ELEM_FREQS))
+	if (!stats_check_arg_pair(args, attstatinfo,
+							  ATTSTAT_MOST_COMMON_ELEMS,
+							  ATTSTAT_MOST_COMMON_ELEM_FREQS))
 	{
 		do_mcelem = false;
 		result = false;
 	}
 
-	if (!stats_check_arg_pair(args, attarginfo,
-							  ATTARG_RANGE_LENGTH_HISTOGRAM,
-							  ATTARG_RANGE_EMPTY_FRAC))
+	if (!stats_check_arg_pair(args, attstatinfo,
+							  ATTSTAT_RANGE_LENGTH_HISTOGRAM,
+							  ATTSTAT_RANGE_EMPTY_FRAC))
 	{
 		do_range_length_histogram = false;
 		result = false;
 	}
 
 	/* derive information from attribute */
-	statatt_get_type(reloid, attnum,
+	statatt_get_type(RelationGetRelid(rel), attnum,
 					 &atttypid, &atttypmod,
 					 &atttyptype, &atttypcoll,
 					 &eq_opr, &lt_opr);
@@ -334,29 +297,29 @@ update_attstats(const NullableDatum *args)
 
 	starel = table_open(StatisticRelationId, RowExclusiveLock);
 
-	statup = SearchSysCache3(STATRELATTINH, ObjectIdGetDatum(reloid), Int16GetDatum(attnum), BoolGetDatum(inherited));
+	statup = SearchSysCache3(STATRELATTINH, ObjectIdGetDatum(RelationGetRelid(rel)), Int16GetDatum(attnum), BoolGetDatum(inherited));
 
 	/* initialize from existing tuple if exists */
 	if (HeapTupleIsValid(statup))
 		heap_deform_tuple(statup, RelationGetDescr(starel), values, nulls);
 	else
-		statatt_init_empty_tuple(reloid, attnum, inherited, values, nulls,
+		statatt_init_empty_tuple(RelationGetRelid(rel), attnum, inherited, values, nulls,
 								 replaces);
 
 	/* if specified, set to argument values */
-	if (!args[ATTARG_NULL_FRAC].isnull)
+	if (!args[ATTSTAT_NULL_FRAC].isnull)
 	{
-		values[Anum_pg_statistic_stanullfrac - 1] = args[ATTARG_NULL_FRAC].value;
+		values[Anum_pg_statistic_stanullfrac - 1] = args[ATTSTAT_NULL_FRAC].value;
 		replaces[Anum_pg_statistic_stanullfrac - 1] = true;
 	}
-	if (!args[ATTARG_AVG_WIDTH].isnull)
+	if (!args[ATTSTAT_AVG_WIDTH].isnull)
 	{
-		values[Anum_pg_statistic_stawidth - 1] = args[ATTARG_AVG_WIDTH].value;
+		values[Anum_pg_statistic_stawidth - 1] = args[ATTSTAT_AVG_WIDTH].value;
 		replaces[Anum_pg_statistic_stawidth - 1] = true;
 	}
-	if (!args[ATTARG_N_DISTINCT].isnull)
+	if (!args[ATTSTAT_N_DISTINCT].isnull)
 	{
-		values[Anum_pg_statistic_stadistinct - 1] = args[ATTARG_N_DISTINCT].value;
+		values[Anum_pg_statistic_stadistinct - 1] = args[ATTSTAT_N_DISTINCT].value;
 		replaces[Anum_pg_statistic_stadistinct - 1] = true;
 	}
 
@@ -364,10 +327,10 @@ update_attstats(const NullableDatum *args)
 	if (do_mcv)
 	{
 		bool		converted;
-		Datum		stanumbers = args[ATTARG_MOST_COMMON_FREQS].value;
+		Datum		stanumbers = args[ATTSTAT_MOST_COMMON_FREQS].value;
 		Datum		stavalues = statatt_build_stavalues("most_common_vals",
 														&array_in_fn,
-														args[ATTARG_MOST_COMMON_VALS].value,
+														args[ATTSTAT_MOST_COMMON_VALS].value,
 														atttypid, atttypmod,
 														&converted);
 
@@ -407,7 +370,7 @@ update_attstats(const NullableDatum *args)
 
 		stavalues = statatt_build_stavalues("histogram_bounds",
 											&array_in_fn,
-											args[ATTARG_HISTOGRAM_BOUNDS].value,
+											args[ATTSTAT_HISTOGRAM_BOUNDS].value,
 											atttypid, atttypmod,
 											&converted);
 
@@ -425,7 +388,7 @@ update_attstats(const NullableDatum *args)
 	/* STATISTIC_KIND_CORRELATION */
 	if (do_correlation)
 	{
-		Datum		elems[] = {args[ATTARG_CORRELATION].value};
+		Datum		elems[] = {args[ATTSTAT_CORRELATION].value};
 		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
 		Datum		stanumbers = PointerGetDatum(arry);
 
@@ -438,13 +401,13 @@ update_attstats(const NullableDatum *args)
 	/* STATISTIC_KIND_MCELEM */
 	if (do_mcelem)
 	{
-		Datum		stanumbers = args[ATTARG_MOST_COMMON_ELEM_FREQS].value;
+		Datum		stanumbers = args[ATTSTAT_MOST_COMMON_ELEM_FREQS].value;
 		bool		converted = false;
 		Datum		stavalues;
 
 		stavalues = statatt_build_stavalues("most_common_elems",
 											&array_in_fn,
-											args[ATTARG_MOST_COMMON_ELEMS].value,
+											args[ATTSTAT_MOST_COMMON_ELEMS].value,
 											elemtypid, atttypmod,
 											&converted);
 
@@ -462,7 +425,7 @@ update_attstats(const NullableDatum *args)
 	/* STATISTIC_KIND_DECHIST */
 	if (do_dechist)
 	{
-		Datum		stanumbers = args[ATTARG_ELEM_COUNT_HISTOGRAM].value;
+		Datum		stanumbers = args[ATTSTAT_ELEM_COUNT_HISTOGRAM].value;
 
 		statatt_set_slot(values, nulls, replaces,
 						 STATISTIC_KIND_DECHIST,
@@ -484,7 +447,7 @@ update_attstats(const NullableDatum *args)
 
 		stavalues = statatt_build_stavalues("range_bounds_histogram",
 											&array_in_fn,
-											args[ATTARG_RANGE_BOUNDS_HISTOGRAM].value,
+											args[ATTSTAT_RANGE_BOUNDS_HISTOGRAM].value,
 											atttypid, atttypmod,
 											&converted);
 
@@ -503,7 +466,7 @@ update_attstats(const NullableDatum *args)
 	if (do_range_length_histogram)
 	{
 		/* The anyarray is always a float8[] for this stakind */
-		Datum		elems[] = {args[ATTARG_RANGE_EMPTY_FRAC].value};
+		Datum		elems[] = {args[ATTSTAT_RANGE_EMPTY_FRAC].value};
 		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
 		Datum		stanumbers = PointerGetDatum(arry);
 
@@ -512,7 +475,7 @@ update_attstats(const NullableDatum *args)
 
 		stavalues = statatt_build_stavalues("range_length_histogram",
 											&array_in_fn,
-											args[ATTARG_RANGE_LENGTH_HISTOGRAM].value,
+											args[ATTSTAT_RANGE_LENGTH_HISTOGRAM].value,
 											FLOAT8OID, 0, &converted);
 
 		if (converted)
@@ -674,14 +637,254 @@ Datum
 pg_restore_attribute_stats(PG_FUNCTION_ARGS)
 {
 	NullableDatum positional_args[ATTARG_NUM_ATTARGS];
+	NullableDatum stats[ATTSTAT_NUM_ATTSTATS];
+
+	char	   *nspname;
+	char	   *relname;
+	Oid			reloid;
+	char	   *attname;
+	AttrNumber	attnum;
+	bool		inherited;
+	Oid			locked_table = InvalidOid;
+	Relation	rel;
+
 	bool		result = true;
 
 	if (!stats_fill_args_from_arg_pairs(fcinfo, positional_args,
 										attarginfo))
 		result = false;
 
-	if (!update_attstats(positional_args))
+	stats_check_required_arg(positional_args, attarginfo, ATTARG_ATTRELSCHEMA);
+	stats_check_required_arg(positional_args, attarginfo, ATTARG_ATTRELNAME);
+
+	nspname = TextDatumGetCString(positional_args[ATTARG_ATTRELSCHEMA].value);
+	relname = TextDatumGetCString(positional_args[ATTARG_ATTRELNAME].value);
+
+	if (RecoveryInProgress())
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("recovery is in progress"),
+				 errhint("Statistics cannot be modified during recovery.")));
+
+	/* lock before looking up attribute */
+	reloid = RangeVarGetRelidExtended(makeRangeVar(nspname, relname, -1),
+									  ShareUpdateExclusiveLock, 0,
+									  RangeVarCallbackForStats, &locked_table);
+	rel = relation_open(reloid, NoLock);
+
+	/* user can specify either attname or attnum, but not both */
+	if (!positional_args[ATTARG_ATTNAME].isnull)
+	{
+		if (!positional_args[ATTARG_ATTNUM].isnull)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("cannot specify both \"%s\" and \"%s\"", "attname", "attnum")));
+		attname = TextDatumGetCString(positional_args[ATTARG_ATTNAME].value);
+		attnum = get_attnum(reloid, attname);
+		/* note that this test covers attisdropped cases too: */
+		if (attnum == InvalidAttrNumber)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_COLUMN),
+					 errmsg("column \"%s\" of relation \"%s\" does not exist",
+							attname, relname)));
+	}
+	else if (!positional_args[ATTARG_ATTNUM].isnull)
+	{
+		attnum = DatumGetInt16(positional_args[ATTARG_ATTNUM].value);
+		attname = get_attname(reloid, attnum, true);
+		/* annoyingly, get_attname doesn't check attisdropped */
+		if (attname == NULL ||
+			!SearchSysCacheExistsAttName(reloid, attname))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_COLUMN),
+					 errmsg("column %d of relation \"%s\" does not exist",
+							attnum, relname)));
+	}
+	else
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("must specify either \"%s\" or \"%s\"", "attname", "attnum")));
+		attname = NULL;			/* keep compiler quiet */
+		attnum = 0;
+	}
+
+	stats_check_required_arg(positional_args, attarginfo, ATTARG_INHERITED);
+	inherited = DatumGetBool(positional_args[ATTARG_INHERITED].value);
+
+	/* Map ATTARGs to ATTSTATs */
+	for (int i = 0; i < ATTSTAT_NUM_ATTSTATS; i++)
+	{
+		stats[i].isnull = positional_args[i + ARGS_STATS_OFFSET].isnull;
+		stats[i].value = positional_args[i + ARGS_STATS_OFFSET].value;
+	}
+
+	if (!update_attstats(rel, attname, attnum, inherited, stats))
 		result = false;
 
+	relation_close(rel, NoLock);
+
 	PG_RETURN_BOOL(result);
 }
+
+/*
+ * Delete the pg_statistic for a given relation's attnum+inherited.
+ */
+bool
+attribute_statistics_delete(Relation rel, AttrNumber attnum, bool inherited)
+{
+	return delete_pg_statistic(RelationGetRelid(rel), attnum, inherited);
+}
+
+/*
+ * Convenience routine to parse float values, and emit a warning on parse
+ * errors.
+ */
+static void
+str_to_float(NullableDatum *stats, int statnum, const bool *isnull,
+			 const char **values)
+{
+	stats[statnum].isnull = true;
+	stats[statnum].value = 0;
+
+	if (!isnull[statnum])
+	{
+		const char *s = values[statnum];
+		Datum		value;
+		ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+		if (s == NULL)
+			elog(ERROR, "value is null but flag is non-null");
+
+		if (DirectInputFunctionCallSafe(float4in, (char *) s, InvalidOid, -1,
+										(Node *) &escontext, &value))
+		{
+			stats[statnum].isnull = false;
+			stats[statnum].value = value;
+		}
+		else
+		{
+			escontext.error_data->elevel = WARNING;
+			ThrowErrorData(escontext.error_data);
+			FreeErrorData(escontext.error_data);
+			return;
+		}
+	}
+}
+
+/*
+ * Convenience routine to parse int4 values, and emit a warning on parse
+ * errors.
+ */
+static void
+str_to_int4(NullableDatum *stats, int statnum, const bool *isnull,
+			const char **values)
+{
+	stats[statnum].isnull = true;
+	stats[statnum].value = 0;
+
+	if (!isnull[statnum])
+	{
+		const char *s = values[statnum];
+		Datum		value;
+		ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+		if (s == NULL)
+			elog(ERROR, "value is null but flag is non-null");
+
+		if (DirectInputFunctionCallSafe(int4in, (char *) s, InvalidOid, -1,
+										(Node *) &escontext, &value))
+		{
+			stats[statnum].isnull = false;
+			stats[statnum].value = value;
+		}
+		else
+		{
+			escontext.error_data->elevel = WARNING;
+			ThrowErrorData(escontext.error_data);
+			FreeErrorData(escontext.error_data);
+			return;
+		}
+	}
+}
+
+/*
+ * Convenience routine to parse float values, and emit a warning on parse
+ * errors.
+ */
+static void
+str_to_text(NullableDatum *stats, int statnum, const bool *isnull,
+			const char **values)
+{
+	stats[statnum].isnull = true;
+	stats[statnum].value = 0;
+
+	if (!isnull[statnum])
+	{
+		stats[statnum].isnull = false;
+		stats[statnum].value = CStringGetTextDatum(values[statnum]);
+	}
+}
+
+/*
+ * Convenience routine to parse float array values, and emit a warning on parse
+ * errors.
+ */
+static void
+str_to_floatarray(NullableDatum *stats, int statnum, const bool *isnull,
+				  const char **values)
+{
+	stats[statnum].isnull = true;
+	stats[statnum].value = 0;
+
+	if (!isnull[statnum])
+	{
+		ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+		FmgrInfo		flinfo;
+		Datum			value;
+
+		fmgr_info(F_ARRAY_IN, &flinfo);
+
+		if (!InputFunctionCallSafe(&flinfo, (char *) values[statnum], FLOAT4OID,
+								   -1, (Node *) &escontext, &value))
+		{
+			escontext.error_data->elevel = WARNING;
+			ThrowErrorData(escontext.error_data);
+			FreeErrorData(escontext.error_data);
+		}
+		else
+		{
+			stats[statnum].isnull = false;
+			stats[statnum].value = value;
+		}
+	}
+}
+
+/*
+ * Update statistics for a given attribute+inherited of already opened Relation
+ * with a lock level of at least ShareUpdateExclusiveLock.
+ */
+bool
+attribute_statistics_update(Relation rel, const char *attname,
+							AttrNumber attnum, bool inherited,
+							const bool *isnull, const char **values)
+{
+	NullableDatum stats[ATTSTAT_NUM_ATTSTATS];
+
+	str_to_float(stats, ATTSTAT_NULL_FRAC, isnull, values);
+	str_to_int4(stats, ATTSTAT_AVG_WIDTH, isnull, values);
+	str_to_float(stats, ATTSTAT_N_DISTINCT, isnull, values);
+	str_to_text(stats, ATTSTAT_MOST_COMMON_VALS, isnull, values);
+	str_to_floatarray(stats, ATTSTAT_MOST_COMMON_FREQS, isnull, values);
+	str_to_text(stats, ATTSTAT_HISTOGRAM_BOUNDS, isnull, values);
+	str_to_float(stats, ATTSTAT_CORRELATION, isnull, values);
+	str_to_text(stats, ATTSTAT_MOST_COMMON_ELEMS, isnull, values);
+	str_to_floatarray(stats, ATTSTAT_MOST_COMMON_ELEM_FREQS, isnull, values);
+	str_to_floatarray(stats, ATTSTAT_ELEM_COUNT_HISTOGRAM, isnull, values);
+	str_to_text(stats, ATTSTAT_RANGE_LENGTH_HISTOGRAM, isnull, values);
+	str_to_float(stats, ATTSTAT_RANGE_EMPTY_FRAC, isnull, values);
+	str_to_text(stats, ATTSTAT_RANGE_BOUNDS_HISTOGRAM, isnull, values);
+
+	return update_attstats(rel, attname, attnum, inherited, stats);
+}
diff --git a/src/include/statistics/attribute_stats.h b/src/include/statistics/attribute_stats.h
new file mode 100644
index 00000000000..efbfd4222f7
--- /dev/null
+++ b/src/include/statistics/attribute_stats.h
@@ -0,0 +1,45 @@
+/*-------------------------------------------------------------------------
+ *
+ * attribute_stats.h
+ *    Functions for the internal manipulation of attribute statistics.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/statistics/attribute_stats.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "access/attnum.h"
+#ifndef ATTRIBUTE_STATS_H
+
+#include "access/genam.h"
+
+enum attribute_stats_argnum
+{
+	ATTSTAT_NULL_FRAC,
+	ATTSTAT_AVG_WIDTH,
+	ATTSTAT_N_DISTINCT,
+	ATTSTAT_MOST_COMMON_VALS,
+	ATTSTAT_MOST_COMMON_FREQS,
+	ATTSTAT_HISTOGRAM_BOUNDS,
+	ATTSTAT_CORRELATION,
+	ATTSTAT_MOST_COMMON_ELEMS,
+	ATTSTAT_MOST_COMMON_ELEM_FREQS,
+	ATTSTAT_ELEM_COUNT_HISTOGRAM,
+	ATTSTAT_RANGE_LENGTH_HISTOGRAM,
+	ATTSTAT_RANGE_EMPTY_FRAC,
+	ATTSTAT_RANGE_BOUNDS_HISTOGRAM,
+	ATTSTAT_NUM_ATTSTATS
+};
+
+extern bool attribute_statistics_delete(Relation rel, AttrNumber attnum,
+										bool inherited);
+
+extern bool attribute_statistics_update(Relation rel, const char *attname,
+										AttrNumber attnum, bool inherited,
+										const bool *isnull, const char **values);
+
+
+#define ATTRIBUTE_STATS_H
+#endif
-- 
2.50.1 (Apple Git-155)

Reply via email to