From 1c8a042f385ca8fbc5d8873b297224ab3a494ca9 Mon Sep 17 00:00:00 2001 From: Nik Samokhvalov Date: Mon, 14 Sep 2026 19:53:26 -0700 Subject: [PATCH] postgres_fdw: Make statistics import atomic --- .../postgres_fdw/expected/postgres_fdw.out | 34 +++++++++++++++ contrib/postgres_fdw/postgres_fdw.c | 43 ++++++++++++++++++- contrib/postgres_fdw/sql/postgres_fdw.sql | 29 +++++++++++++ 3 files changed, 104 insertions(+), 2 deletions(-) diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 0cf84fee8..923b8d456 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -13270,6 +13270,38 @@ WHERE schemaname = 'public' AND tablename = 'dtest_ftable'; ---------+-----------+-----------+-----------+------------+-----+-------------------+----+------------- (0 rows) +-- A failed import must not leave partial attribute statistics behind for an +-- empty sampling fallback. +CREATE TABLE simport_atomicity_table (a int, b text); +CREATE FOREIGN TABLE simport_atomicity_ftable (a int, b int) + SERVER loopback OPTIONS (table_name 'simport_atomicity_table', + import_stats 'true'); +INSERT INTO simport_atomicity_table +SELECT CASE WHEN i <= 9 THEN 11 ELSE 12 END, + CASE WHEN i <= 8 THEN '21' ELSE '22' END +FROM generate_series(1, 10) i; +ANALYZE simport_atomicity_table; +ANALYZE simport_atomicity_ftable; +TRUNCATE simport_atomicity_table; +INSERT INTO simport_atomicity_table +SELECT CASE WHEN i <= 7 THEN 111 ELSE 112 END, + CASE WHEN i <= 6 THEN 'not-an-integer-x' ELSE 'not-an-integer-y' END +FROM generate_series(1, 10) i; +ANALYZE simport_atomicity_table; +DELETE FROM simport_atomicity_table; +ANALYZE simport_atomicity_ftable; +WARNING: invalid input syntax for type integer: "not-an-integer-x" +WARNING: could not import statistics for foreign table "public.simport_atomicity_ftable" --- attribute statistics import failed for column "b" of this foreign table +SELECT attname, most_common_vals::text +FROM pg_stats +WHERE schemaname = 'public' AND tablename = 'simport_atomicity_ftable' +ORDER BY attname; + attname | most_common_vals +---------+------------------ + a | {11} + b | {21,22} +(2 rows) + -- cleanup DROP FOREIGN TABLE simport_ftable; DROP FOREIGN TABLE simport_fview; @@ -13279,6 +13311,8 @@ DROP FOREIGN TABLE simport_fpt; DROP TABLE simport_pt; DROP FOREIGN TABLE dtest_ftable; DROP TABLE dtest_table; +DROP FOREIGN TABLE simport_atomicity_ftable; +DROP TABLE simport_atomicity_table; -- =================================================================== -- test for postgres_fdw_get_connections function with check_conn = true -- =================================================================== diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index 61e4e6fb2..775c407db 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -17,6 +17,7 @@ #include "access/htup_details.h" #include "access/sysattr.h" #include "access/table.h" +#include "access/xact.h" #include "catalog/pg_opfamily.h" #include "commands/defrem.h" #include "commands/explain_format.h" @@ -51,6 +52,7 @@ #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/rel.h" +#include "utils/resowner.h" #include "utils/sampling.h" #include "utils/selfuncs.h" #include "utils/timestamp.h" @@ -5538,8 +5540,45 @@ postgresImportForeignStatistics(Relation relation, List *va_cols, int elevel) &remstats, &remattrmap, &attrcnt); if (ok) - ok = import_fetched_statistics(relation, schemaname, relname, - &remstats, remattrmap, attrcnt); + { + MemoryContext oldcontext = CurrentMemoryContext; + ResourceOwner oldowner = CurrentResourceOwner; + + /* + * Import the fetched statistics atomically. An attribute conversion + * failure is reported by returning false, after possibly updating + * that attribute and any preceding attributes. Roll those changes + * back so they cannot leak into the sampling fallback. + */ + BeginInternalSubTransaction(NULL); + MemoryContextSwitchTo(oldcontext); + + PG_TRY(); + { + ok = import_fetched_statistics(relation, schemaname, relname, + &remstats, remattrmap, attrcnt); + + if (ok) + ReleaseCurrentSubTransaction(); + else + RollbackAndReleaseCurrentSubTransaction(); + MemoryContextSwitchTo(oldcontext); + CurrentResourceOwner = oldowner; + } + PG_CATCH(); + { + ErrorData *edata; + + MemoryContextSwitchTo(oldcontext); + edata = CopyErrorData(); + FlushErrorState(); + RollbackAndReleaseCurrentSubTransaction(); + MemoryContextSwitchTo(oldcontext); + CurrentResourceOwner = oldowner; + ReThrowError(edata); + } + PG_END_TRY(); + } if (ok) { diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index 3f6ea7b13..d7652b1cf 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -4740,6 +4740,33 @@ SELECT attname, inherited, null_frac, avg_width, n_distinct, FROM pg_stats WHERE schemaname = 'public' AND tablename = 'dtest_ftable'; +-- A failed import must not leave partial attribute statistics behind for an +-- empty sampling fallback. +CREATE TABLE simport_atomicity_table (a int, b text); +CREATE FOREIGN TABLE simport_atomicity_ftable (a int, b int) + SERVER loopback OPTIONS (table_name 'simport_atomicity_table', + import_stats 'true'); +INSERT INTO simport_atomicity_table +SELECT CASE WHEN i <= 9 THEN 11 ELSE 12 END, + CASE WHEN i <= 8 THEN '21' ELSE '22' END +FROM generate_series(1, 10) i; +ANALYZE simport_atomicity_table; +ANALYZE simport_atomicity_ftable; + +TRUNCATE simport_atomicity_table; +INSERT INTO simport_atomicity_table +SELECT CASE WHEN i <= 7 THEN 111 ELSE 112 END, + CASE WHEN i <= 6 THEN 'not-an-integer-x' ELSE 'not-an-integer-y' END +FROM generate_series(1, 10) i; +ANALYZE simport_atomicity_table; +DELETE FROM simport_atomicity_table; +ANALYZE simport_atomicity_ftable; + +SELECT attname, most_common_vals::text +FROM pg_stats +WHERE schemaname = 'public' AND tablename = 'simport_atomicity_ftable' +ORDER BY attname; + -- cleanup DROP FOREIGN TABLE simport_ftable; DROP FOREIGN TABLE simport_fview; @@ -4749,6 +4776,8 @@ DROP FOREIGN TABLE simport_fpt; DROP TABLE simport_pt; DROP FOREIGN TABLE dtest_ftable; DROP TABLE dtest_table; +DROP FOREIGN TABLE simport_atomicity_ftable; +DROP TABLE simport_atomicity_table; -- =================================================================== -- test for postgres_fdw_get_connections function with check_conn = true -- 2.50.1 (Apple Git-155)