v2-0013-Replace-statistics-import-SPI-calls-with-new-impo.patch

application/octet-stream

Filename: v2-0013-Replace-statistics-import-SPI-calls-with-new-impo.patch
Type: application/octet-stream
Part: 11
Message: Re: use of SPI by postgresImportForeignStatistics

Patch

Format: format-patch
Series: patch v2-0013
Subject: Replace statistics import SPI calls with new import functions.
File+
contrib/postgres_fdw/expected/postgres_fdw.out 52 0
contrib/postgres_fdw/postgres_fdw.c 93 301
contrib/postgres_fdw/sql/postgres_fdw.sql 40 0
From 188b5f7d80cfa5af8affd82f6579591d3f6d43dc Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 29 Jun 2026 02:15:28 -0500
Subject: [PATCH v2 13/13] Replace statistics import SPI calls with new import
 functions.

This patch removes the SPI calls in postgres_fdw.c that were related to
relation statistics and attribute statistics. Instead, the functions
relation_statistics_update() and attribute_statistics_update() have been
create specifically for this purpose.
---
 .../postgres_fdw/expected/postgres_fdw.out    |  52 +++
 contrib/postgres_fdw/postgres_fdw.c           | 394 +++++-------------
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  40 ++
 3 files changed, 185 insertions(+), 301 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 0805c56cb1b..80b927d7f0b 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -13027,6 +13027,58 @@ ANALYZE dtest_table;
 ANALYZE VERBOSE dtest_ftable;             -- should work
 INFO:  importing statistics for foreign table "public.dtest_ftable"
 INFO:  finished importing statistics for foreign table "public.dtest_ftable"
+-- dtest_ftables stats should now exactly match dtest_table
+-- compare the rowcounts, should get 0 rows back
+SELECT COUNT(*) FROM pg_stats
+WHERE schemaname = 'public' AND tablename = 'dtest_table'
+EXCEPT
+SELECT COUNT(*) FROM pg_stats
+WHERE schemaname = 'public' AND tablename = 'dtest_ftable';
+ count 
+-------
+(0 rows)
+
+-- compare values, should match
+SELECT relpages, reltuples
+FROM pg_class
+WHERE oid = 'public.dtest_table'::regclass
+EXCEPT
+SELECT relpages, reltuples
+FROM pg_class
+WHERE oid = 'public.dtest_ftable'::regclass;
+ relpages | reltuples 
+----------+-----------
+(0 rows)
+
+SELECT relpages, reltuples
+FROM pg_class
+WHERE oid = 'public.dtest_ftable'::regclass
+EXCEPT
+SELECT relpages, reltuples
+FROM pg_class
+WHERE oid = 'public.dtest_table'::regclass;
+ relpages | reltuples 
+----------+-----------
+(0 rows)
+
+-- test only a few stats columns common to integer types
+SELECT attname, inherited, null_frac, avg_width, n_distinct,
+  most_common_vals::text as mcv, most_common_freqs,
+  histogram_bounds::text as hb, correlation
+FROM pg_stats
+WHERE schemaname = 'public'
+AND tablename = 'dtest_table'
+EXCEPT
+SELECT attname, inherited, null_frac, avg_width, n_distinct,
+  most_common_vals::text as mcv, most_common_freqs,
+  histogram_bounds::text as hb, correlation
+FROM pg_stats
+WHERE schemaname = 'public'
+AND tablename = 'dtest_ftable';
+ attname | inherited | null_frac | avg_width | n_distinct | mcv | most_common_freqs | hb | correlation 
+---------+-----------+-----------+-----------+------------+-----+-------------------+----+-------------
+(0 rows)
+
 -- cleanup
 DROP FOREIGN TABLE simport_ftable;
 DROP FOREIGN TABLE simport_fview;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 6dbae583ecc..677a821eb10 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -24,7 +24,6 @@
 #include "commands/vacuum.h"
 #include "executor/execAsync.h"
 #include "executor/instrument.h"
-#include "executor/spi.h"
 #include "foreign/fdwapi.h"
 #include "funcapi.h"
 #include "miscadmin.h"
@@ -43,6 +42,8 @@
 #include "parser/parsetree.h"
 #include "postgres_fdw.h"
 #include "statistics/statistics.h"
+#include "statistics/attribute_stats.h"
+#include "statistics/relation_stats.h"
 #include "storage/latch.h"
 #include "utils/builtins.h"
 #include "utils/float.h"
@@ -341,160 +342,30 @@ typedef struct
 /* Column order in relation stats query */
 enum RelStatsColumns
 {
-	RELSTATS_RELPAGES = 0,
-	RELSTATS_RELTUPLES,
-	RELSTATS_RELKIND,
-	RELSTATS_NUM_FIELDS,
+	RELCOL_RELPAGES = 0,
+	RELCOL_RELTUPLES,
+	RELCOL_RELKIND,
+	RELCOL_NUM_FIELDS,
 };
 
 /* Column order in attribute stats query */
 enum AttStatsColumns
 {
-	ATTSTATS_ATTNAME = 0,
-	ATTSTATS_NULL_FRAC,
-	ATTSTATS_AVG_WIDTH,
-	ATTSTATS_N_DISTINCT,
-	ATTSTATS_MOST_COMMON_VALS,
-	ATTSTATS_MOST_COMMON_FREQS,
-	ATTSTATS_HISTOGRAM_BOUNDS,
-	ATTSTATS_CORRELATION,
-	ATTSTATS_MOST_COMMON_ELEMS,
-	ATTSTATS_MOST_COMMON_ELEM_FREQS,
-	ATTSTATS_ELEM_COUNT_HISTOGRAM,
-	ATTSTATS_RANGE_LENGTH_HISTOGRAM,
-	ATTSTATS_RANGE_EMPTY_FRAC,
-	ATTSTATS_RANGE_BOUNDS_HISTOGRAM,
-	ATTSTATS_NUM_FIELDS,
-};
-
-/* Relation stats import query */
-static const char *relimport_sql =
-"SELECT pg_catalog.pg_restore_relation_stats(\n"
-"\t'version', $1,\n"
-"\t'schemaname', $2,\n"
-"\t'relname', $3,\n"
-"\t'relpages', $4::integer,\n"
-"\t'reltuples', $5::real)";
-
-/* Argument order in relation stats import query */
-enum RelImportSqlArgs
-{
-	RELIMPORT_SQL_VERSION = 0,
-	RELIMPORT_SQL_SCHEMANAME,
-	RELIMPORT_SQL_RELNAME,
-	RELIMPORT_SQL_RELPAGES,
-	RELIMPORT_SQL_RELTUPLES,
-	RELIMPORT_SQL_NUM_FIELDS
-};
-
-/* Argument types in relation stats import query */
-static const Oid relimport_argtypes[RELIMPORT_SQL_NUM_FIELDS] =
-{
-	INT4OID, TEXTOID, TEXTOID, TEXTOID,
-	TEXTOID,
-};
-
-/* Attribute stats import query */
-static const char *attimport_sql =
-"SELECT pg_catalog.pg_restore_attribute_stats(\n"
-"\t'version', $1,\n"
-"\t'schemaname', $2,\n"
-"\t'relname', $3,\n"
-"\t'attnum', $4,\n"
-"\t'inherited', false::boolean,\n"
-"\t'null_frac', $5::real,\n"
-"\t'avg_width', $6::integer,\n"
-"\t'n_distinct', $7::real,\n"
-"\t'most_common_vals', $8,\n"
-"\t'most_common_freqs', $9::real[],\n"
-"\t'histogram_bounds', $10,\n"
-"\t'correlation', $11::real,\n"
-"\t'most_common_elems', $12,\n"
-"\t'most_common_elem_freqs', $13::real[],\n"
-"\t'elem_count_histogram', $14::real[],\n"
-"\t'range_length_histogram', $15,\n"
-"\t'range_empty_frac', $16::real,\n"
-"\t'range_bounds_histogram', $17)";
-
-/* Argument order in attribute stats import query */
-enum AttImportSqlArgs
-{
-	ATTIMPORT_SQL_VERSION = 0,
-	ATTIMPORT_SQL_SCHEMANAME,
-	ATTIMPORT_SQL_RELNAME,
-	ATTIMPORT_SQL_ATTNUM,
-	ATTIMPORT_SQL_NULL_FRAC,
-	ATTIMPORT_SQL_AVG_WIDTH,
-	ATTIMPORT_SQL_N_DISTINCT,
-	ATTIMPORT_SQL_MOST_COMMON_VALS,
-	ATTIMPORT_SQL_MOST_COMMON_FREQS,
-	ATTIMPORT_SQL_HISTOGRAM_BOUNDS,
-	ATTIMPORT_SQL_CORRELATION,
-	ATTIMPORT_SQL_MOST_COMMON_ELEMS,
-	ATTIMPORT_SQL_MOST_COMMON_ELEM_FREQS,
-	ATTIMPORT_SQL_ELEM_COUNT_HISTOGRAM,
-	ATTIMPORT_SQL_RANGE_LENGTH_HISTOGRAM,
-	ATTIMPORT_SQL_RANGE_EMPTY_FRAC,
-	ATTIMPORT_SQL_RANGE_BOUNDS_HISTOGRAM,
-	ATTIMPORT_SQL_NUM_FIELDS
-};
-
-/* Argument types in attribute stats import query */
-static const Oid attimport_argtypes[ATTIMPORT_SQL_NUM_FIELDS] =
-{
-	INT4OID, TEXTOID, TEXTOID, INT2OID,
-	TEXTOID, TEXTOID, TEXTOID, TEXTOID,
-	TEXTOID, TEXTOID, TEXTOID, TEXTOID,
-	TEXTOID, TEXTOID, TEXTOID, TEXTOID,
-	TEXTOID,
-};
-
-/*
- * The mapping of attribute stats query columns to the positional arguments in
- * the prepared pg_restore_attribute_stats() statement.
- */
-typedef struct
-{
-	enum AttStatsColumns res_field;
-	enum AttImportSqlArgs arg_num;
-} AttrResultArgMap;
-
-#define NUM_MAPPED_ATTIMPORT_ARGS 13
-
-static const AttrResultArgMap attr_result_arg_map[NUM_MAPPED_ATTIMPORT_ARGS] =
-{
-	{ATTSTATS_NULL_FRAC, ATTIMPORT_SQL_NULL_FRAC},
-	{ATTSTATS_AVG_WIDTH, ATTIMPORT_SQL_AVG_WIDTH},
-	{ATTSTATS_N_DISTINCT, ATTIMPORT_SQL_N_DISTINCT},
-	{ATTSTATS_MOST_COMMON_VALS, ATTIMPORT_SQL_MOST_COMMON_VALS},
-	{ATTSTATS_MOST_COMMON_FREQS, ATTIMPORT_SQL_MOST_COMMON_FREQS},
-	{ATTSTATS_HISTOGRAM_BOUNDS, ATTIMPORT_SQL_HISTOGRAM_BOUNDS},
-	{ATTSTATS_CORRELATION, ATTIMPORT_SQL_CORRELATION},
-	{ATTSTATS_MOST_COMMON_ELEMS, ATTIMPORT_SQL_MOST_COMMON_ELEMS},
-	{ATTSTATS_MOST_COMMON_ELEM_FREQS, ATTIMPORT_SQL_MOST_COMMON_ELEM_FREQS},
-	{ATTSTATS_ELEM_COUNT_HISTOGRAM, ATTIMPORT_SQL_ELEM_COUNT_HISTOGRAM},
-	{ATTSTATS_RANGE_LENGTH_HISTOGRAM, ATTIMPORT_SQL_RANGE_LENGTH_HISTOGRAM},
-	{ATTSTATS_RANGE_EMPTY_FRAC, ATTIMPORT_SQL_RANGE_EMPTY_FRAC},
-	{ATTSTATS_RANGE_BOUNDS_HISTOGRAM, ATTIMPORT_SQL_RANGE_BOUNDS_HISTOGRAM},
-};
-
-/* Attribute stats clear query */
-static const char *attclear_sql =
-"SELECT pg_catalog.pg_clear_attribute_stats($1, $2, $3, false)";
-
-/* Argument order in attribute stats clear query */
-enum AttClearSqlArgs
-{
-	ATTCLEAR_SQL_SCHEMANAME = 0,
-	ATTCLEAR_SQL_RELNAME,
-	ATTCLEAR_SQL_ATTNAME,
-	ATTCLEAR_SQL_NUM_FIELDS
-};
-
-/* Argument types in attribute stats clear query */
-static const Oid attclear_argtypes[ATTCLEAR_SQL_NUM_FIELDS] =
-{
-	TEXTOID, TEXTOID, TEXTOID,
+	ATTCOL_ATTNAME = 0,
+	ATTCOL_NULL_FRAC,
+	ATTCOL_AVG_WIDTH,
+	ATTCOL_N_DISTINCT,
+	ATTCOL_MOST_COMMON_VALS,
+	ATTCOL_MOST_COMMON_FREQS,
+	ATTCOL_HISTOGRAM_BOUNDS,
+	ATTCOL_CORRELATION,
+	ATTCOL_MOST_COMMON_ELEMS,
+	ATTCOL_MOST_COMMON_ELEM_FREQS,
+	ATTCOL_ELEM_COUNT_HISTOGRAM,
+	ATTCOL_RANGE_LENGTH_HISTOGRAM,
+	ATTCOL_RANGE_EMPTY_FRAC,
+	ATTCOL_RANGE_BOUNDS_HISTOGRAM,
+	ATTCOL_NUM_FIELDS,
 };
 
 /*
@@ -714,14 +585,12 @@ static bool match_attrmap(PGresult *res,
 						  const char *remote_relname,
 						  int attrcnt,
 						  RemoteAttributeMapping *remattrmap);
-static bool import_fetched_statistics(const char *schemaname,
+static bool import_fetched_statistics(Relation relation,
+									  const char *schemaname,
 									  const char *relname,
 									  int attrcnt,
 									  const RemoteAttributeMapping *remattrmap,
 									  RemoteStatsResults *remstats);
-static void map_field_to_arg(PGresult *res, int row, int field,
-							 int arg, Datum *values, char *nulls);
-static bool import_spi_query_ok(void);
 static void produce_tuple_asynchronously(AsyncRequest *areq, bool fetch);
 static void fetch_more_data_begin(AsyncRequest *areq);
 static void complete_pending_request(AsyncRequest *areq);
@@ -5190,11 +5059,11 @@ postgresGetAnalyzeInfoForForeignTable(Relation relation, bool *can_tablesample)
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pgfdw_report_error(res, conn, sql.data);
 
-	if (PQntuples(res) != 1 || PQnfields(res) != RELSTATS_NUM_FIELDS)
+	if (PQntuples(res) != 1 || PQnfields(res) != RELCOL_NUM_FIELDS)
 		elog(ERROR, "unexpected result from deparseAnalyzeInfoSql query");
 	/* We don't use relpages here */
-	reltuples = strtod(PQgetvalue(res, 0, RELSTATS_RELTUPLES), NULL);
-	relkind = *(PQgetvalue(res, 0, RELSTATS_RELKIND));
+	reltuples = strtod(PQgetvalue(res, 0, RELCOL_RELTUPLES), NULL);
+	relkind = *(PQgetvalue(res, 0, RELCOL_RELKIND));
 	PQclear(res);
 
 	ReleaseConnection(conn);
@@ -5661,7 +5530,7 @@ postgresImportForeignStatistics(Relation relation, List *va_cols, int elevel)
 								 &attrcnt, &remattrmap, &remstats);
 
 	if (ok)
-		ok = import_fetched_statistics(schemaname, relname,
+		ok = import_fetched_statistics(relation, schemaname, relname,
 									   attrcnt, remattrmap, &remstats);
 
 	if (ok)
@@ -5738,7 +5607,7 @@ fetch_remote_statistics(Relation relation,
 	 * RELKIND_PARTITIONED_INDEX can have rows in pg_stats, they obviously
 	 * can't support a foreign table.
 	 */
-	relkind = *PQgetvalue(relstats, 0, RELSTATS_RELKIND);
+	relkind = *PQgetvalue(relstats, 0, RELCOL_RELKIND);
 	switch (relkind)
 	{
 		case RELKIND_RELATION:
@@ -5769,7 +5638,7 @@ fetch_remote_statistics(Relation relation,
 	 * way, we wouldn't expect to find the stats for the table, so we fallback
 	 * to sampling.
 	 */
-	reltuples = strtod(PQgetvalue(relstats, 0, RELSTATS_RELTUPLES), NULL);
+	reltuples = strtod(PQgetvalue(relstats, 0, RELCOL_RELTUPLES), NULL);
 	if (((server_version_num < 140000) && (reltuples == 0)) ||
 		((server_version_num >= 140000) && (reltuples == -1)))
 	{
@@ -5830,7 +5699,7 @@ fetch_relstats(PGconn *conn, Relation relation)
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pgfdw_report_error(res, conn, sql.data);
 
-	if (PQntuples(res) != 1 || PQnfields(res) != RELSTATS_NUM_FIELDS)
+	if (PQntuples(res) != 1 || PQnfields(res) != RELCOL_NUM_FIELDS)
 		elog(ERROR, "unexpected result from deparseAnalyzeInfoSql query");
 
 	return res;
@@ -5901,7 +5770,7 @@ fetch_attstats(PGconn *conn, int server_version_num,
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pgfdw_report_error(res, conn, sql.data);
 
-	if (PQnfields(res) != ATTSTATS_NUM_FIELDS)
+	if (PQnfields(res) != ATTCOL_NUM_FIELDS)
 		elog(ERROR, "unexpected result from fetch_attstats query");
 
 	return res;
@@ -6068,7 +5937,7 @@ match_attrmap(PGresult *res,
 		 */
 		if (row >= 0 &&
 			strcmp(remattrmap[mapidx].remote_attname,
-				   PQgetvalue(res, row, ATTSTATS_ATTNAME)) == 0)
+				   PQgetvalue(res, row, ATTCOL_ATTNAME)) == 0)
 		{
 			remattrmap[mapidx].res_index = row;
 			continue;
@@ -6096,7 +5965,7 @@ match_attrmap(PGresult *res,
 		 * row, it means the stats for the entry are missing.
 		 */
 		if (strcmp(remattrmap[mapidx].remote_attname,
-				   PQgetvalue(res, row, ATTSTATS_ATTNAME)) < 0)
+				   PQgetvalue(res, row, ATTCOL_ATTNAME)) < 0)
 		{
 			ereport(WARNING,
 					errmsg("could not import statistics for foreign table \"%s.%s\" --- no attribute statistics found for column \"%s\" of remote table \"%s.%s\"",
@@ -6108,12 +5977,12 @@ match_attrmap(PGresult *res,
 
 		/* We should not have got a stats row we didn't expect. */
 		if (strcmp(remattrmap[mapidx].remote_attname,
-				   PQgetvalue(res, row, ATTSTATS_ATTNAME)) > 0)
+				   PQgetvalue(res, row, ATTCOL_ATTNAME)) > 0)
 			elog(ERROR, "unexpected result from fetch_attstats query");
 
 		/* We found a match. */
 		Assert(strcmp(remattrmap[mapidx].remote_attname,
-					  PQgetvalue(res, row, ATTSTATS_ATTNAME)) == 0);
+					  PQgetvalue(res, row, ATTCOL_ATTNAME)) == 0);
 		remattrmap[mapidx].res_index = row;
 	}
 
@@ -6124,35 +5993,37 @@ match_attrmap(PGresult *res,
 	return true;
 }
 
+
+/*
+ * Conenience routine to map a given cell of a result set to the input arguments
+ * of a call to relation_statistics_update() or attribute_statistics_update().
+ */
+static void
+map_res_to_stat(PGresult *res, int row, int col,
+				bool *isnull, char **values, int statnum)
+{
+	isnull[statnum] = PQgetisnull(res, row, col);
+	values[statnum] = (isnull[statnum]) ? NULL : PQgetvalue(res, row, col);
+}
+
+#define map_attstat(name) \
+	map_res_to_stat(remstats->att, row, ATTCOL_##name, attisnull, attvalues, ATTSTAT_##name)
+
 /*
  * Import fetched statistics into the local statistics tables.
  */
 static bool
-import_fetched_statistics(const char *schemaname,
+import_fetched_statistics(Relation relation,
+						  const char *schemaname,
 						  const char *relname,
 						  int attrcnt,
 						  const RemoteAttributeMapping *remattrmap,
 						  RemoteStatsResults *remstats)
 {
-	SPIPlanPtr	attimport_plan = NULL;
-	SPIPlanPtr	attclear_plan = NULL;
-	Datum		values[ATTIMPORT_SQL_NUM_FIELDS];
-	char		nulls[ATTIMPORT_SQL_NUM_FIELDS];
-	int			spirc;
+	bool		relisnull[RELSTAT_NUM_RELSTATS];
+	char	   *relvalues[RELSTAT_NUM_RELSTATS];
 	bool		ok = false;
 
-	/* Assign all the invariant parameters common to relation/attribute stats */
-	values[ATTIMPORT_SQL_VERSION] = Int32GetDatum(remstats->server_version_num);
-	nulls[ATTIMPORT_SQL_VERSION] = ' ';
-
-	values[ATTIMPORT_SQL_SCHEMANAME] = CStringGetTextDatum(schemaname);
-	nulls[ATTIMPORT_SQL_SCHEMANAME] = ' ';
-
-	values[ATTIMPORT_SQL_RELNAME] = CStringGetTextDatum(relname);
-	nulls[ATTIMPORT_SQL_RELNAME] = ' ';
-
-	SPI_connect();
-
 	/*
 	 * We import attribute statistics first, if any, because those are more
 	 * prone to errors.  This avoids making a modification of pg_class that
@@ -6160,26 +6031,16 @@ import_fetched_statistics(const char *schemaname,
 	 */
 	if (remstats->att != NULL)
 	{
-		Assert(PQnfields(remstats->att) == ATTSTATS_NUM_FIELDS);
+		Assert(PQnfields(remstats->att) == ATTCOL_NUM_FIELDS);
 		Assert(PQntuples(remstats->att) >= 1);
 
-		attimport_plan = SPI_prepare(attimport_sql, ATTIMPORT_SQL_NUM_FIELDS,
-									 (Oid *) attimport_argtypes);
-		if (attimport_plan == NULL)
-			elog(ERROR, "failed to prepare attimport_sql query");
-
-		attclear_plan = SPI_prepare(attclear_sql, ATTCLEAR_SQL_NUM_FIELDS,
-									(Oid *) attclear_argtypes);
-		if (attclear_plan == NULL)
-			elog(ERROR, "failed to prepare attclear_sql query");
-
-		nulls[ATTIMPORT_SQL_ATTNUM] = ' ';
-
 		for (int mapidx = 0; mapidx < attrcnt; mapidx++)
 		{
+			bool		attisnull[ATTSTAT_NUM_ATTSTATS];
+			char	   *attvalues[ATTSTAT_NUM_ATTSTATS];
 			int			row = remattrmap[mapidx].res_index;
-			Datum	   *values2 = values + 1;
-			char	   *nulls2 = nulls + 1;
+			AttrNumber	attnum = remattrmap[mapidx].local_attnum;
+			const char *attname = remattrmap[mapidx].local_attname;
 
 			/* All mappings should have been assigned a result set row. */
 			Assert(row >= 0);
@@ -6189,71 +6050,50 @@ import_fetched_statistics(const char *schemaname,
 			 */
 			CHECK_FOR_INTERRUPTS();
 
-			/*
-			 * First, clear existing attribute stats.
-			 *
-			 * We can re-use the values/nulls because the number of parameters
-			 * is less and the first two params are the same as the second and
-			 * third ones in attimport_sql.
-			 */
-			values2[ATTCLEAR_SQL_ATTNAME] =
-				CStringGetTextDatum(remattrmap[mapidx].local_attname);
-
-			spirc = SPI_execute_plan(attclear_plan, values2, nulls2, false, 1);
-			if (spirc != SPI_OK_SELECT)
-				elog(ERROR, "failed to execute attclear_sql query for column \"%s\" of foreign table \"%s.%s\"",
-					 remattrmap[mapidx].local_attname, schemaname, relname);
-
-			values[ATTIMPORT_SQL_ATTNUM] =
-				Int16GetDatum(remattrmap[mapidx].local_attnum);
-
-			/* Loop through all mappable columns to set remaining arguments */
-			for (int i = 0; i < NUM_MAPPED_ATTIMPORT_ARGS; i++)
-				map_field_to_arg(remstats->att, row,
-								 attr_result_arg_map[i].res_field,
-								 attr_result_arg_map[i].arg_num,
-								 values, nulls);
-
-			spirc = SPI_execute_plan(attimport_plan, values, nulls, false, 1);
-			if (spirc != SPI_OK_SELECT)
-				elog(ERROR, "failed to execute attimport_sql query for column \"%s\" of foreign table \"%s.%s\"",
-					 remattrmap[mapidx].local_attname, schemaname, relname);
-
-			if (!import_spi_query_ok())
+			attribute_statistics_delete(relation, attnum, false);
+
+			map_attstat(NULL_FRAC);
+			map_attstat(AVG_WIDTH);
+			map_attstat(N_DISTINCT);
+			map_attstat(MOST_COMMON_VALS);
+			map_attstat(MOST_COMMON_FREQS);
+			map_attstat(HISTOGRAM_BOUNDS);
+			map_attstat(CORRELATION);
+			map_attstat(MOST_COMMON_ELEMS);
+			map_attstat(MOST_COMMON_ELEM_FREQS);
+			map_attstat(ELEM_COUNT_HISTOGRAM);
+			map_attstat(RANGE_LENGTH_HISTOGRAM);
+			map_attstat(RANGE_EMPTY_FRAC);
+			map_attstat(RANGE_BOUNDS_HISTOGRAM);
+
+			if (!attribute_statistics_update(relation, attname, attnum, false,
+											 attisnull,
+											 (const char **) attvalues))
 			{
 				ereport(WARNING,
 						errmsg("could not import statistics for foreign table \"%s.%s\" --- attribute statistics import failed for column \"%s\" of this foreign table",
-							   schemaname, relname,
-							   remattrmap[mapidx].local_attname));
+							   schemaname, relname, attname));
 				goto import_cleanup;
 			}
 		}
 	}
 
 	/*
-	 * Import relation stats.  We only perform this once, so there is no point
-	 * in preparing the statement.
-	 *
-	 * We can re-use the values/nulls because the number of parameters is less
-	 * and the first three params are the same as attimport_sql.
+	 * Import relation stats.
 	 */
 	Assert(remstats->rel != NULL);
-	Assert(PQnfields(remstats->rel) == RELSTATS_NUM_FIELDS);
-	Assert(PQntuples(remstats->rel) == 1);
-	map_field_to_arg(remstats->rel, 0, RELSTATS_RELPAGES,
-					 RELIMPORT_SQL_RELPAGES, values, nulls);
-	map_field_to_arg(remstats->rel, 0, RELSTATS_RELTUPLES,
-					 RELIMPORT_SQL_RELTUPLES, values, nulls);
-
-	spirc = SPI_execute_with_args(relimport_sql,
-								  RELIMPORT_SQL_NUM_FIELDS,
-								  (Oid *) relimport_argtypes,
-								  values, nulls, false, 1);
-	if (spirc != SPI_OK_SELECT)
-		elog(ERROR, "failed to execute relimport_sql query for foreign table \"%s.%s\"",
-			 schemaname, relname);
-
-	if (!import_spi_query_ok())
+
+	map_res_to_stat(remstats->rel, 0, RELCOL_RELPAGES,
+					relisnull, relvalues, RELSTAT_RELPAGES);
+	map_res_to_stat(remstats->rel, 0, RELCOL_RELTUPLES,
+					relisnull, relvalues, RELSTAT_RELTUPLES);
+	relisnull[RELSTAT_RELALLVISIBLE] = true;
+	relvalues[RELSTAT_RELALLVISIBLE] = NULL;
+	relisnull[RELSTAT_RELALLFROZEN] = true;
+	relvalues[RELSTAT_RELALLFROZEN] = NULL;
+
+	if (!relation_statistics_update(relation, relisnull,
+									(const char **) relvalues))
 	{
 		ereport(WARNING,
 				errmsg("could not import statistics for foreign table \"%s.%s\" --- relation statistics import failed for this foreign table",
@@ -6264,57 +6104,9 @@ import_fetched_statistics(const char *schemaname,
 	ok = true;
 
 import_cleanup:
-	if (attimport_plan)
-		SPI_freeplan(attimport_plan);
-	if (attclear_plan)
-		SPI_freeplan(attclear_plan);
-	SPI_finish();
 	return ok;
 }
 
-/*
- * Move a string value from a result set to a Text value of a Datum array.
- */
-static void
-map_field_to_arg(PGresult *res, int row, int field,
-				 int arg, Datum *values, char *nulls)
-{
-	if (PQgetisnull(res, row, field))
-	{
-		values[arg] = (Datum) 0;
-		nulls[arg] = 'n';
-	}
-	else
-	{
-		const char *s = PQgetvalue(res, row, field);
-
-		values[arg] = CStringGetTextDatum(s);
-		nulls[arg] = ' ';
-	}
-}
-
-/*
- * Check the 1x1 result set of a pg_restore_*_stats() command for success.
- */
-static bool
-import_spi_query_ok(void)
-{
-	TupleDesc	tupdesc;
-	Datum		dat;
-	bool		isnull;
-
-	Assert(SPI_tuptable != NULL);
-	Assert(SPI_processed == 1);
-
-	tupdesc = SPI_tuptable->tupdesc;
-	Assert(tupdesc->natts == 1);
-	Assert(TupleDescAttr(tupdesc, 0)->atttypid == BOOLOID);
-	dat = SPI_getbinval(SPI_tuptable->vals[0], tupdesc, 1, &isnull);
-	Assert(!isnull);
-
-	return DatumGetBool(dat);
-}
-
 /*
  * Import a foreign schema
  */
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 8162c5496bf..201cebfd2c2 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -4642,6 +4642,46 @@ ANALYZE dtest_table;
 
 ANALYZE VERBOSE dtest_ftable;             -- should work
 
+-- dtest_ftables stats should now exactly match dtest_table
+-- compare the rowcounts, should get 0 rows back
+SELECT COUNT(*) FROM pg_stats
+WHERE schemaname = 'public' AND tablename = 'dtest_table'
+EXCEPT
+SELECT COUNT(*) FROM pg_stats
+WHERE schemaname = 'public' AND tablename = 'dtest_ftable';
+
+-- compare values, should match
+SELECT relpages, reltuples
+FROM pg_class
+WHERE oid = 'public.dtest_table'::regclass
+EXCEPT
+SELECT relpages, reltuples
+FROM pg_class
+WHERE oid = 'public.dtest_ftable'::regclass;
+
+SELECT relpages, reltuples
+FROM pg_class
+WHERE oid = 'public.dtest_ftable'::regclass
+EXCEPT
+SELECT relpages, reltuples
+FROM pg_class
+WHERE oid = 'public.dtest_table'::regclass;
+
+-- test only a few stats columns common to integer types
+SELECT attname, inherited, null_frac, avg_width, n_distinct,
+  most_common_vals::text as mcv, most_common_freqs,
+  histogram_bounds::text as hb, correlation
+FROM pg_stats
+WHERE schemaname = 'public'
+AND tablename = 'dtest_table'
+EXCEPT
+SELECT attname, inherited, null_frac, avg_width, n_distinct,
+  most_common_vals::text as mcv, most_common_freqs,
+  histogram_bounds::text as hb, correlation
+FROM pg_stats
+WHERE schemaname = 'public'
+AND tablename = 'dtest_ftable';
+
 -- cleanup
 DROP FOREIGN TABLE simport_ftable;
 DROP FOREIGN TABLE simport_fview;
-- 
2.50.1 (Apple Git-155)