From c0f8736dafbb6b5dcbdb7ad70407b95feea47d4d Mon Sep 17 00:00:00 2001
From: shihao zhong <zhong950419@gmail.com>
Date: Mon, 12 Oct 2026 03:13:07 -0400
Subject: [PATCH v1 1/3] Let pg_restore_extended_stats translate attribute
 numbers.

n_distinct and dependencies carry the attribute numbers of the source
table. After a restore these can point to other columns. Add stxkeys
and stxkeynames so the function can map the numbers by column name.
---
 doc/src/sgml/func/func-admin.sgml             |   9 +
 src/backend/statistics/extended_stats_funcs.c | 160 +++++++++++++++++-
 2 files changed, 167 insertions(+), 2 deletions(-)

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 64b0e7bb972..694fa1bc00a 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -2274,6 +2274,15 @@ SELECT pg_restore_extended_stats(
          This is anticipated to be helpful in porting statistics from older
          versions of <productname>PostgreSQL</productname>.
         </para>
+        <para>
+         The arguments <literal>stxkeys</literal> of type
+         <type>smallint[]</type> and <literal>stxkeynames</literal> of type
+         <type>text[]</type> give the column numbers and column names of the
+         statistics object in the source database, in the same order.  When
+         both are present, the column numbers in <literal>n_distinct</literal>
+         and <literal>dependencies</literal> are translated to the numbers of
+         the columns with the same names in this database.
+        </para>
         <para>
          Minor errors are reported as a <literal>WARNING</literal> and
          ignored, and remaining statistics will still be restored. If all
diff --git a/src/backend/statistics/extended_stats_funcs.c b/src/backend/statistics/extended_stats_funcs.c
index 00b8dbe3366..a9430deb671 100644
--- a/src/backend/statistics/extended_stats_funcs.c
+++ b/src/backend/statistics/extended_stats_funcs.c
@@ -50,6 +50,8 @@ enum extended_stats_argnum
 	STATSCHEMA_ARG,
 	STATNAME_ARG,
 	INHERITED_ARG,
+	STXKEYS_ARG,
+	STXKEYNAMES_ARG,
 	NDISTINCT_ARG,
 	DEPENDENCIES_ARG,
 	MOST_COMMON_VALS_ARG,
@@ -70,6 +72,8 @@ static struct StatsArgInfo extarginfo[] =
 	[STATSCHEMA_ARG] = {"statistics_schemaname", TEXTOID},
 	[STATNAME_ARG] = {"statistics_name", TEXTOID},
 	[INHERITED_ARG] = {"inherited", BOOLOID},
+	[STXKEYS_ARG] = {"stxkeys", INT2ARRAYOID},
+	[STXKEYNAMES_ARG] = {"stxkeynames", TEXTARRAYOID},
 	[NDISTINCT_ARG] = {"n_distinct", PG_NDISTINCTOID},
 	[DEPENDENCIES_ARG] = {"dependencies", PG_DEPENDENCIESOID},
 	[MOST_COMMON_VALS_ARG] = {"most_common_vals", TEXTARRAYOID},
@@ -122,6 +126,10 @@ static const char *extexprargname[NUM_ATTRIBUTE_STATS_ELEMS] =
 };
 
 static bool extended_statistics_update(FunctionCallInfo fcinfo);
+static bool remap_stxkey_attnums(AttrNumber *attnums, int n,
+								 const AttrNumber *srckeys,
+								 const AttrNumber *dstkeys, int nkeys,
+								 const char *typname);
 
 static HeapTuple get_pg_statistic_ext(Relation pg_stext, Oid nspoid,
 									  const char *stxname);
@@ -301,6 +309,45 @@ upsert_pg_statistic_ext_data(const Datum *values, const bool *nulls,
 	table_close(pg_stextdata, RowExclusiveLock);
 }
 
+/*
+ * Translate positive attribute numbers in "attnums" from the numbering used
+ * by the source of the statistics to the numbering of this server.  Source
+ * key srckeys[i] maps to dstkeys[i].  Negative numbers refer to expressions
+ * and are left alone.
+ * Returns false (after a WARNING) if a number is not among the source keys.
+ */
+static bool
+remap_stxkey_attnums(AttrNumber *attnums, int n,
+					 const AttrNumber *srckeys, const AttrNumber *dstkeys,
+					 int nkeys, const char *typname)
+{
+	for (int i = 0; i < n; i++)
+	{
+		int			k;
+
+		if (attnums[i] <= 0)
+			continue;
+
+		for (k = 0; k < nkeys; k++)
+		{
+			if (srckeys[k] == attnums[i])
+				break;
+		}
+
+		if (k == nkeys)
+		{
+			ereport(WARNING,
+					errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("could not validate \"%s\" object: invalid attribute number %d found",
+						   typname, attnums[i]));
+			return false;
+		}
+		attnums[i] = dstkeys[k];
+	}
+
+	return true;
+}
+
 /*
  * Insert or update an extended statistics object.
  *
@@ -344,6 +391,9 @@ extended_statistics_update(FunctionCallInfo fcinfo)
 	Oid		   *atttypcolls = NULL;
 	Oid			relid;
 	Oid			locked_table = InvalidOid;
+	AttrNumber *srckeys = NULL;
+	AttrNumber *dstkeys = NULL;
+	int			nsrckeys = 0;
 
 	/*
 	 * Fill out the StakindFlags "has" structure based on which parameters
@@ -569,6 +619,82 @@ extended_statistics_update(FunctionCallInfo fcinfo)
 		success = false;
 	}
 
+	/*
+	 * If the caller gave us the attribute numbers and names of the keys of
+	 * the statistics object it was dumped from, build the translation to our
+	 * attribute numbers, matching columns by name.  The MCV list is stored by
+	 * position in stxkeys, so it can only be used if the order is the same.
+	 */
+	if (!PG_ARGISNULL(STXKEYS_ARG) || !PG_ARGISNULL(STXKEYNAMES_ARG))
+	{
+		bool		keys_ok = !PG_ARGISNULL(STXKEYS_ARG) &&
+			!PG_ARGISNULL(STXKEYNAMES_ARG);
+		bool		order_differs = false;
+
+		if (keys_ok)
+		{
+			ArrayType  *keys_arr = PG_GETARG_ARRAYTYPE_P(STXKEYS_ARG);
+			ArrayType  *names_arr = PG_GETARG_ARRAYTYPE_P(STXKEYNAMES_ARG);
+
+			keys_ok = ARR_NDIM(keys_arr) <= 1 && ARR_NDIM(names_arr) <= 1 &&
+				!array_contains_nulls(keys_arr) &&
+				!array_contains_nulls(names_arr) &&
+				ArrayGetNItems(ARR_NDIM(keys_arr), ARR_DIMS(keys_arr)) == numattnums &&
+				ArrayGetNItems(ARR_NDIM(names_arr), ARR_DIMS(names_arr)) == numattnums;
+
+			if (keys_ok)
+			{
+				Datum	   *names;
+				int			nnames;
+
+				deconstruct_array_builtin(names_arr, TEXTOID, &names, NULL,
+										  &nnames);
+				srckeys = palloc_array(AttrNumber, numattnums);
+				dstkeys = palloc_array(AttrNumber, numattnums);
+				nsrckeys = numattnums;
+
+				for (int i = 0; i < numattnums; i++)
+				{
+					char	   *attname = TextDatumGetCString(names[i]);
+
+					srckeys[i] = ((int16 *) ARR_DATA_PTR(keys_arr))[i];
+					dstkeys[i] = get_attnum(relid, attname);
+					if (dstkeys[i] == InvalidAttrNumber)
+					{
+						keys_ok = false;
+						break;
+					}
+					if (i > 0 && dstkeys[i] < dstkeys[i - 1])
+						order_differs = true;
+				}
+			}
+		}
+
+		if (!keys_ok)
+		{
+			ereport(WARNING,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("could not use \"%s\" and \"%s\": they do not match extended statistics object \"%s.%s\"",
+						   extarginfo[STXKEYS_ARG].argname,
+						   extarginfo[STXKEYNAMES_ARG].argname,
+						   nspname, stxname));
+			has.ndistinct = false;
+			has.dependencies = false;
+			has.mcv = false;
+			success = false;
+		}
+		else if (order_differs && has.mcv)
+		{
+			ereport(WARNING,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("could not restore \"%s\" for extended statistics object \"%s.%s\": column order differs from the source",
+						   extarginfo[MOST_COMMON_VALS_ARG].argname,
+						   nspname, stxname));
+			has.mcv = false;
+			success = false;
+		}
+	}
+
 	/*
 	 * Either of these statistic types requires that we supply a semi-filled
 	 * VacAttrStatsP array.
@@ -658,8 +784,21 @@ extended_statistics_update(FunctionCallInfo fcinfo)
 		Datum		ndistinct_datum = PG_GETARG_DATUM(NDISTINCT_ARG);
 		bytea	   *data = DatumGetByteaPP(ndistinct_datum);
 		MVNDistinct *ndistinct = statext_ndistinct_deserialize(data);
+		bool		remapped = true;
+
+		if (srckeys != NULL)
+		{
+			for (uint32 i = 0; i < ndistinct->nitems && remapped; i++)
+				remapped = remap_stxkey_attnums(ndistinct->items[i].attributes,
+												ndistinct->items[i].nattributes,
+												srckeys, dstkeys, nsrckeys,
+												"pg_ndistinct");
+			if (remapped)
+				ndistinct_datum = PointerGetDatum(statext_ndistinct_serialize(ndistinct));
+		}
 
-		if (statext_ndistinct_validate(ndistinct, &stxform->stxkeys,
+		if (remapped &&
+			statext_ndistinct_validate(ndistinct, &stxform->stxkeys,
 									   numexprs, WARNING))
 		{
 			values[Anum_pg_statistic_ext_data_stxdndistinct - 1] = ndistinct_datum;
@@ -677,8 +816,21 @@ extended_statistics_update(FunctionCallInfo fcinfo)
 		Datum		dependencies_datum = PG_GETARG_DATUM(DEPENDENCIES_ARG);
 		bytea	   *data = DatumGetByteaPP(dependencies_datum);
 		MVDependencies *dependencies = statext_dependencies_deserialize(data);
+		bool		remapped = true;
+
+		if (srckeys != NULL)
+		{
+			for (uint32 i = 0; i < dependencies->ndeps && remapped; i++)
+				remapped = remap_stxkey_attnums(dependencies->deps[i]->attributes,
+												dependencies->deps[i]->nattributes,
+												srckeys, dstkeys, nsrckeys,
+												"pg_dependencies");
+			if (remapped)
+				dependencies_datum = PointerGetDatum(statext_dependencies_serialize(dependencies));
+		}
 
-		if (statext_dependencies_validate(dependencies, &stxform->stxkeys,
+		if (remapped &&
+			statext_dependencies_validate(dependencies, &stxform->stxkeys,
 										  numexprs, WARNING))
 		{
 			values[Anum_pg_statistic_ext_data_stxddependencies - 1] = dependencies_datum;
@@ -756,6 +908,10 @@ cleanup:
 		heap_freetuple(tup);
 	if (pg_stext != NULL)
 		table_close(pg_stext, RowExclusiveLock);
+	if (srckeys != NULL)
+		pfree(srckeys);
+	if (dstkeys != NULL)
+		pfree(dstkeys);
 	if (atttypids != NULL)
 		pfree(atttypids);
 	if (atttypmods != NULL)
-- 
2.37.1 (Apple Git-137.1)

