Trailing Comma Bug in postgres_fdw fetch_attstats() — Cross-Version Statistics Import
Context and Architectural Background
PostgreSQL 17 introduced the ability to import/export planner statistics (pg_statistic contents exposed via pg_stats) as part of the broader effort to make pg_dump/pg_restore preserve optimizer statistics, and to allow ANALYZE on foreign tables to pull pre-computed statistics from the remote side rather than re-sampling through the FDW. In postgres_fdw, this materialized as a fetch_attstats() helper that builds a SELECT against the remote pg_catalog.pg_stats view and maps the returned columns back into local pg_statistic rows.
Because the set of columns in pg_stats has grown over releases — notably with range-type statistics (range_length_histogram, range_empty_frac, range_bounds_histogram) that were added in v17 — postgres_fdw must issue a version-aware query. When the remote server is older than v17, those three columns do not exist, so the code substitutes three literal NULL placeholders in the select list so that the column positions on the client side still line up with the TupleDesc the executor expects.
The Core Problem
The bug is trivial in mechanics but has real operational impact: the string literal containing the three NULL placeholders was written with a trailing comma:
... NULL, NULL, NULL, FROM pg_catalog.pg_stats ...
PostgreSQL's SQL grammar does not permit a trailing comma in a SELECT target list, so the remote server immediately rejects the statement with a syntax error at FROM. This means that any ANALYZE or explicit IMPORT STATISTICS issued against a foreign table whose remote side is a pre-v17 PostgreSQL instance fails outright. Ironically, the code path specifically designed to provide backward compatibility with older servers is the one that is broken — the v17+ path (which includes the range columns and therefore does not need the placeholder branch) works fine.
This is a classic "untested else branch" defect: the happy path of same-version remote is exercised heavily in CI, but the cross-version compatibility shim is not covered by the postgres_fdw regression suite, which only spins up a single server version.
Nature of the Fix
The fix is a one-character change — removing the stray comma after the last NULL in the emitted SQL fragment. There is no design discussion because there is no design choice: the string simply needs to be well-formed SQL. The patch was contributed by the reporter (Satya Narlapuram) alongside the bug report.
Why It Was Treated as an Open Item
Fujita-san, as the committer who introduced the fetch_attstats() code path, immediately claimed ownership ("My bad, I will take care of this. I added it to the open item."). Adding it to the PostgreSQL open items list is the standard procedure for defects in features committed during the current development cycle that must be resolved before release. Given the timing (April 2026, shortly before the presumed v18 feature freeze stabilization period), ensuring the fix lands before the release is important: shipping a GA with broken cross-version statistics import would regress a headline interoperability feature for postgres_fdw.
Broader Implications
Two takeaways are worth flagging for reviewers of similar code:
-
Version-conditional SQL construction is fragile. Building SQL by string concatenation with version branches is a recurring source of bugs in
postgres_fdw(see also earlier issues withdeparseExplicitTargetListand collation handling). AStringInfo-based accumulator that appends columns with a leading separator (or usesappendStringInfoString(&buf, first ? "" : ", ")) structurally prevents trailing-comma bugs. The current code appears to hardcode the placeholder block as a literal. -
Cross-version test coverage gap. The
postgres_fdwTAP/regress infrastructure does not routinely exercise a mixed-version topology. Catching this kind of defect automatically would require a buildfarm-style matrix test that stands up, e.g., a v16 remote alongside the dev-branch local server. This has been discussed periodically on -hackers but remains unimplemented; the present bug is a concrete argument for that investment.
Resolution
Fujita-san reviewed the patch, announced intent to push absent objections, waited the customary period, and pushed the fix on 2026-05-08. No dissent was raised — unsurprising given the unambiguous nature of the defect.