v2-0012-Add-attribute_statistics_update-refactor-update_a.patch

application/octet-stream

Filename: v2-0012-Add-attribute_statistics_update-refactor-update_a.patch
Type: application/octet-stream
Part: 10
Message: Re: use of SPI by postgresImportForeignStatistics

Patch

Format: format-patch
Series: patch v2-0012
Subject: Add attribute_statistics_update, refactor update_attstats.
File+
src/backend/statistics/attribute_stats.c 311 108
src/include/statistics/attribute_stats.h 45 0
From 4b832c5c733f6a919547287c9faf15ffa45d23c4 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
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)