public inbox for [email protected]  
help / color / mirror / Atom feed
use of SPI by postgresImportForeignStatistics
33+ messages / 4 participants
[nested] [flat]

* use of SPI by postgresImportForeignStatistics
@ 2026-06-15 17:50 Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  0 siblings, 1 reply; 33+ messages in thread

From: Robert Haas @ 2026-06-15 17:50 UTC (permalink / raw)
  To: Etsuro Fujita <[email protected]>; pgsql-hackers; Corey Huinker <[email protected]>

Hi,

I'm concerned about the way that postgresImportForeignStatistics is
doing what it does. First, it's using SPI to execute two constant SQL
queries, attimport_sql and attclear_sql. However, it doesn't seem to
set the search_path, and these queries aren't fully robust against a
possibly-unsafe search_path (e.g. the call to
pg_restore_attribute_stats is schema-qualified, but the type names are
not!). Furthermore, the function we're calling takes a schema name and
a relation name as two separate arguments, rather than a single OID
argument. I'm not sure it's completely guaranteed that we'll end up
affecting the same relation that we locked in
postgresImportForeignStatistics, because e.g. what if the containing
schema is being concurrently renamed?

But even if it is absolutely guaranteed that we latch onto the correct
relation, this seems like the fundamentally wrong way to do this kind
of thing. It seems crazy to me that instead of exposing an interface
that would be well-suited to direct use by an FDW, the
statistics-import stuff exposes an interface that can only be
reasonably called via the FunctionCallInfo interface, which then
results in postgres_fdw needing to jump through hoops to construct and
execute SQL statements.

This doesn't look entirely easy to straighten out. A function like
attribute_statistics_update() is just not a good idea -- it mixes
together considerations that only exist when you want to call
something from SQL with general validity checking that's needed
regardless.

But I don't think we should ship this like this. Best case scenario,
it's overly complicated. Medium case scenario, it's also unreliable.
Worst case scenario, there are security vulnerabilities in here.

-- 
Robert Haas
EDB: http://www.enterprisedb.com






^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
@ 2026-06-16 12:04 ` Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  0 siblings, 1 reply; 33+ messages in thread

From: Etsuro Fujita @ 2026-06-16 12:04 UTC (permalink / raw)
  To: Robert Haas <[email protected]>; +Cc: pgsql-hackers; Corey Huinker <[email protected]>

Hi Robert,

On Tue, Jun 16, 2026 at 2:50 AM Robert Haas <[email protected]> wrote:
> I'm concerned about the way that postgresImportForeignStatistics is
> doing what it does. First, it's using SPI to execute two constant SQL
> queries, attimport_sql and attclear_sql. However, it doesn't seem to
> set the search_path, and these queries aren't fully robust against a
> possibly-unsafe search_path (e.g. the call to
> pg_restore_attribute_stats is schema-qualified, but the type names are
> not!).

Good catch!

> Furthermore, the function we're calling takes a schema name and
> a relation name as two separate arguments, rather than a single OID
> argument. I'm not sure it's completely guaranteed that we'll end up
> affecting the same relation that we locked in
> postgresImportForeignStatistics, because e.g. what if the containing
> schema is being concurrently renamed?

Ugh.  As pg_restore_attribute_stats/pg_restore_relation_stats are
volatile functions, SPI executes the queries in read-write mode,
causing an error, like "schema "foo" does not exist", or other
unexpected results in such a case.  Not sure what to do about this
issue right now.  Will think about it some more.

> But even if it is absolutely guaranteed that we latch onto the correct
> relation, this seems like the fundamentally wrong way to do this kind
> of thing. It seems crazy to me that instead of exposing an interface
> that would be well-suited to direct use by an FDW, the
> statistics-import stuff exposes an interface that can only be
> reasonably called via the FunctionCallInfo interface, which then
> results in postgres_fdw needing to jump through hoops to construct and
> execute SQL statements.

I thought it would be a good idea to use
pg_restore_attribute_stats/pg_restore_relation_stats, because future
changes in attribute/relation stats would be absorbed by these
functions, which would lower the maintenance cost of this feature.

Thanks for the comments!

Best regards,
Etsuro Fujita





^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
@ 2026-06-16 14:34   ` Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  0 siblings, 1 reply; 33+ messages in thread

From: Robert Haas @ 2026-06-16 14:34 UTC (permalink / raw)
  To: Etsuro Fujita <[email protected]>; +Cc: pgsql-hackers; Corey Huinker <[email protected]>

On Tue, Jun 16, 2026 at 8:04 AM Etsuro Fujita <[email protected]> wrote:
> I thought it would be a good idea to use
> pg_restore_attribute_stats/pg_restore_relation_stats, because future
> changes in attribute/relation stats would be absorbed by these
> functions, which would lower the maintenance cost of this feature.

I agree that we want to reuse code, but this isn't the right way to do
it. For example, when we want to look up the OID of a relation from
SQL, we can say 'whatever'::regclass::oid. But when we want to do the
same thing from C, we don't construct a SELECT statement and execute
it via SPI. Instead, we have functions like RangeVarGetRelidExtended()
which provide access to the same underlying functionality more
directly.

Another example is converting strings to integers. The user calls
int4in(), which then hands off the call to pg_strtoint32_safe(), which
can also be called via pg_strtoint32(). Hence, C code should prefer to
use pg_strtoint32(), while SQL will go through int4in(). Both
ultimately reach the underlying pg_strtoint32_safe() function, but the
interfaces are different, so that we can have it be suitable both for
SQL access and for C access.

This needs to work more like that. The underlying code that ingests
and updates the stats should be shared, but the stuff that is specific
to a FunctionCallInfo interface needs to be separated out so that we
don't need to go through that when calling from C.

-- 
Robert Haas
EDB: http://www.enterprisedb.com






^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
@ 2026-06-16 18:08     ` Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  0 siblings, 1 reply; 33+ messages in thread

From: Corey Huinker @ 2026-06-16 18:08 UTC (permalink / raw)
  To: Robert Haas <[email protected]>; +Cc: Etsuro Fujita <[email protected]>; pgsql-hackers

On Tue, Jun 16, 2026 at 10:34 AM Robert Haas <[email protected]> wrote:

> On Tue, Jun 16, 2026 at 8:04 AM Etsuro Fujita <[email protected]>
> wrote:
> > I thought it would be a good idea to use
> > pg_restore_attribute_stats/pg_restore_relation_stats, because future
> > changes in attribute/relation stats would be absorbed by these
> > functions, which would lower the maintenance cost of this feature.
>
> I agree that we want to reuse code, but this isn't the right way to do
> it. For example, when we want to look up the OID of a relation from
> SQL, we can say 'whatever'::regclass::oid. But when we want to do the
> same thing from C, we don't construct a SELECT statement and execute
> it via SPI. Instead, we have functions like RangeVarGetRelidExtended()
> which provide access to the same underlying functionality more
> directly.
>
> Another example is converting strings to integers. The user calls
> int4in(), which then hands off the call to pg_strtoint32_safe(), which
> can also be called via pg_strtoint32(). Hence, C code should prefer to
> use pg_strtoint32(), while SQL will go through int4in(). Both
> ultimately reach the underlying pg_strtoint32_safe() function, but the
> interfaces are different, so that we can have it be suitable both for
> SQL access and for C access.
>
> This needs to work more like that. The underlying code that ingests
> and updates the stats should be shared, but the stuff that is specific
> to a FunctionCallInfo interface needs to be separated out so that we
> don't need to go through that when calling from C.
>
>
Back when pg_restore_attribute_stats and pg_restore_relation_stats were
being converted from positional to variadic functions, there was a patchset
or two (possibly un-posted, because I couldn't find it just now) where the
variadic functions re-marshalled the arguments into a call to a
fixed-parameter function. If that model were revived, we could have a more
conventional DirectFunctonCall() interface.

Is it possible that we never built a direct function call interface to a
variadic because we never needed one? Perhaps that time is now.


^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
@ 2026-06-16 19:11       ` Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  0 siblings, 1 reply; 33+ messages in thread

From: Corey Huinker @ 2026-06-16 19:11 UTC (permalink / raw)
  To: Robert Haas <[email protected]>; +Cc: Etsuro Fujita <[email protected]>; pgsql-hackers

On Tue, Jun 16, 2026 at 2:08 PM Corey Huinker <[email protected]>
wrote:

>
> On Tue, Jun 16, 2026 at 10:34 AM Robert Haas <[email protected]>
> wrote:
>
>> On Tue, Jun 16, 2026 at 8:04 AM Etsuro Fujita <[email protected]>
>> wrote:
>> > I thought it would be a good idea to use
>> > pg_restore_attribute_stats/pg_restore_relation_stats, because future
>> > changes in attribute/relation stats would be absorbed by these
>> > functions, which would lower the maintenance cost of this feature.
>>
>> I agree that we want to reuse code, but this isn't the right way to do
>> it. For example, when we want to look up the OID of a relation from
>> SQL, we can say 'whatever'::regclass::oid. But when we want to do the
>> same thing from C, we don't construct a SELECT statement and execute
>> it via SPI. Instead, we have functions like RangeVarGetRelidExtended()
>> which provide access to the same underlying functionality more
>> directly.
>>
>> Another example is converting strings to integers. The user calls
>> int4in(), which then hands off the call to pg_strtoint32_safe(), which
>> can also be called via pg_strtoint32(). Hence, C code should prefer to
>> use pg_strtoint32(), while SQL will go through int4in(). Both
>> ultimately reach the underlying pg_strtoint32_safe() function, but the
>> interfaces are different, so that we can have it be suitable both for
>> SQL access and for C access.
>>
>> This needs to work more like that. The underlying code that ingests
>> and updates the stats should be shared, but the stuff that is specific
>> to a FunctionCallInfo interface needs to be separated out so that we
>> don't need to go through that when calling from C.
>>
>>
> Back when pg_restore_attribute_stats and pg_restore_relation_stats were
> being converted from positional to variadic functions, there was a patchset
> or two (possibly un-posted, because I couldn't find it just now) where the
> variadic functions re-marshalled the arguments into a call to a
> fixed-parameter function. If that model were revived, we could have a more
> conventional DirectFunctonCall() interface.
>
> Is it possible that we never built a direct function call interface to a
> variadic because we never needed one? Perhaps that time is now.
>

Obviously, even that wouldn't get us to the no-FunctionCallInfo-at-all
goal, but it would get us out of the SPI situation. If we did have a
non-fcinfo function API, that API would either have to pass char *params
almost exclusively, or else each caller would have to do the translation or
float-array strings to double[] and such, which would be a lot of work for
the caller.

Would you be ok with a function like attribute_statistics_update, but
purely with cstring args? Obviously the callers would have to modify their
calls each time a new stat param is added, but that is seemingly preferable
for you than the current situation.


^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
@ 2026-06-16 21:53         ` Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  0 siblings, 1 reply; 33+ messages in thread

From: Robert Haas @ 2026-06-16 21:53 UTC (permalink / raw)
  To: Corey Huinker <[email protected]>; +Cc: Etsuro Fujita <[email protected]>; pgsql-hackers

On Tue, Jun 16, 2026 at 3:11 PM Corey Huinker <[email protected]> wrote:
> Obviously, even that wouldn't get us to the no-FunctionCallInfo-at-all goal, but it would get us out of the SPI situation. If we did have a non-fcinfo function API, that API would either have to pass char *params almost exclusively, or else each caller would have to do the translation or float-array strings to double[] and such, which would be a lot of work for the caller.
>
> Would you be ok with a function like attribute_statistics_update, but purely with cstring args? Obviously the callers would have to modify their calls each time a new stat param is added, but that is seemingly preferable for you than the current situation.

I feel pretty strongly that we want a way to identify the relation by
OID rather than by schema and name. I'm not sure whether attributes
should be identified by name or by number in this context. Apart from
those things, if passing strings and leaving it up to the called
function to interpret them makes most sense, that's OK with me.

I agree with you that getting rid of SPI is a much higher priority
than getting rid of the FunctionCallInfo stuff altogether. I could
live with DirectFunctionCall if we don't have a better option. I'm a
bit suspicious that this will lead to code bloat in postgres_fdw or
elsewhere, but maybe not.

-- 
Robert Haas
EDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
@ 2026-06-16 22:50           ` Corey Huinker <[email protected]>
  2026-06-16 22:57             ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-17 10:23             ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  0 siblings, 2 replies; 33+ messages in thread

From: Corey Huinker @ 2026-06-16 22:50 UTC (permalink / raw)
  To: Robert Haas <[email protected]>; +Cc: Etsuro Fujita <[email protected]>; pgsql-hackers

>
> >
> > Would you be ok with a function like attribute_statistics_update, but
> purely with cstring args? Obviously the callers would have to modify their
> calls each time a new stat param is added, but that is seemingly preferable
> for you than the current situation.
>
> I feel pretty strongly that we want a way to identify the relation by
> OID rather than by schema and name.


Previous versions of pg_set_attribute_stats() were like that, but it went
away as the use-cases folded into pg_restore_attribute_stats(). So it's
do-able, but will make sharing code with the existing function harder in
the near term. We may have to settle for something that takes the oid, but
then plugs into the existing functions until such time as we can refactor
the existing callers.


> I'm not sure whether attributes
> should be identified by name or by number in this context. Apart from
> those things, if passing strings and leaving it up to the called
> function to interpret them makes most sense, that's OK with me.
>

Identifying attributes by attnum is mostly for index expressions, so
attname would be the preference here.


> I agree with you that getting rid of SPI is a much higher priority
> than getting rid of the FunctionCallInfo stuff altogether. I could
> live with DirectFunctionCall if we don't have a better option.
>

Ok, good to know that I have that as a fallback position.

I think the right first step is to create the functions
import_relation_stats() and import_attribute_stats() which take the oid of
the destination relation and the rest of the parameters are const char
pointers, which is the ideal case for a bunch of values being fetched from
PQgetValue(). Those functions will for now just make the call to
attribute_statistics_update() / relation_statistics_update(), but at least
it will be localized inside relation_stats.c and attribute_stats.c where we
can make internal refactors without disturbing anyone else.

Now, we *could* make these 2 new functions take the oid/oid+attname
followed by a variadic array of keyname+value string pairs like the
existing pg_restore_*_stats() functions, so the functions call signature
would be somewhat future-proofed. Let me know if that violates your vision
of what this should be.


>  I'm a
> bit suspicious that this will lead to code bloat in postgres_fdw or
> elsewhere, but maybe not.
>

postgres_fdw will get slimmer for not having SPI in it. relation_stats.c
and attribute_stats.c will have some temporary bloat until things can be
refactored.


^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
@ 2026-06-16 22:57             ` Robert Haas <[email protected]>
  2026-06-17 16:03               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  1 sibling, 1 reply; 33+ messages in thread

From: Robert Haas @ 2026-06-16 22:57 UTC (permalink / raw)
  To: Corey Huinker <[email protected]>; +Cc: Etsuro Fujita <[email protected]>; pgsql-hackers

On Tue, Jun 16, 2026 at 6:50 PM Corey Huinker <[email protected]> wrote:
> postgres_fdw will get slimmer for not having SPI in it. relation_stats.c and attribute_stats.c will have some temporary bloat until things can be refactored.

Whatever ends up in core can (indeed must) be reused by every FDW. So
making things simpler for the FDW and more complex for core seems
likely to be the right tradeoff in general.

-- 
Robert Haas
EDB: http://www.enterprisedb.com






^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 22:57             ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
@ 2026-06-17 16:03               ` Corey Huinker <[email protected]>
  2026-06-17 23:19                 ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  0 siblings, 1 reply; 33+ messages in thread

From: Corey Huinker @ 2026-06-17 16:03 UTC (permalink / raw)
  To: Robert Haas <[email protected]>; +Cc: Etsuro Fujita <[email protected]>; pgsql-hackers

On Tue, Jun 16, 2026 at 6:57 PM Robert Haas <[email protected]> wrote:

> On Tue, Jun 16, 2026 at 6:50 PM Corey Huinker <[email protected]>
> wrote:
> > postgres_fdw will get slimmer for not having SPI in it. relation_stats.c
> and attribute_stats.c will have some temporary bloat until things can be
> refactored.
>
> Whatever ends up in core can (indeed must) be reused by every FDW. So
> making things simpler for the FDW and more complex for core seems
> likely to be the right tradeoff in general.


I had assumed we wanted a generic C API to these two functions, but if we
want something that is specific to FDWs, that might change where the
functions land in the header files. It's an easy change to make if we
change our minds, but it would be good to know if we do or do not want
something specific to FDWs. Personally, I think FDWs will be 90-95% of the
usages outside of the existing SQL-defined functions, but 95% is not 100%,
so I'd be inclined to leave them in a statistics/stats utils header.


^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 22:57             ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-17 16:03               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
@ 2026-06-17 23:19                 ` Robert Haas <[email protected]>
  0 siblings, 0 replies; 33+ messages in thread

From: Robert Haas @ 2026-06-17 23:19 UTC (permalink / raw)
  To: Corey Huinker <[email protected]>; +Cc: Etsuro Fujita <[email protected]>; pgsql-hackers

On Wed, Jun 17, 2026 at 12:03 PM Corey Huinker <[email protected]> wrote:
> I had assumed we wanted a generic C API to these two functions, but if we want something that is specific to FDWs, that might change where the functions land in the header files. It's an easy change to make if we change our minds, but it would be good to know if we do or do not want something specific to FDWs. Personally, I think FDWs will be 90-95% of the usages outside of the existing SQL-defined functions, but 95% is not 100%, so I'd be inclined to leave them in a statistics/stats utils header.

I don't want it to be specific to FDWs, just convenient for FDWs.
Agree they should stay in a statistics header.

-- 
Robert Haas
EDB: http://www.enterprisedb.com






^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
@ 2026-06-17 10:23             ` Etsuro Fujita <[email protected]>
  2026-06-17 16:00               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  1 sibling, 1 reply; 33+ messages in thread

From: Etsuro Fujita @ 2026-06-17 10:23 UTC (permalink / raw)
  To: Corey Huinker <[email protected]>; +Cc: Robert Haas <[email protected]>; pgsql-hackers

Hi Corey,

On Wed, Jun 17, 2026 at 7:50 AM Corey Huinker <[email protected]> wrote:
> I think the right first step is to create the functions import_relation_stats() and import_attribute_stats() which take the oid of the destination relation and the rest of the parameters are const char pointers, which is the ideal case for a bunch of values being fetched from PQgetValue(). Those functions will for now just make the call to attribute_statistics_update() / relation_statistics_update(), but at least it will be localized inside relation_stats.c and attribute_stats.c where we can make internal refactors without disturbing anyone else.

+1

> Now, we *could* make these 2 new functions take the oid/oid+attname followed by a variadic array of keyname+value string pairs like the existing pg_restore_*_stats() functions, so the functions call signature would be somewhat future-proofed. Let me know if that violates your vision of what this should be.

IMO I don't think we need to go that far, because 1) the new functions
are provided for FDW authors only, and 2) we don't make changes to the
stats that often.

Thanks!

Best regards,
Etsuro Fujita






^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-17 10:23             ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
@ 2026-06-17 16:00               ` Corey Huinker <[email protected]>
  2026-06-18 10:42                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  0 siblings, 1 reply; 33+ messages in thread

From: Corey Huinker @ 2026-06-17 16:00 UTC (permalink / raw)
  To: Etsuro Fujita <[email protected]>; +Cc: Robert Haas <[email protected]>; pgsql-hackers

>
>
> IMO I don't think we need to go that far, because 1) the new functions
> are provided for FDW authors only, and 2) we don't make changes to the
> stats that often.
>

That would be simpler, to be sure. However, in the past I've received
pushback on functions that had a large number of parameters, and this would
definitely be a large number of parameters, approximately 17, so I thought
I should at least offer the variadic way.

I'm proceeding with the many-parameters model.


^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-17 10:23             ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-17 16:00               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
@ 2026-06-18 10:42                 ` Etsuro Fujita <[email protected]>
  2026-06-18 21:23                   ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  0 siblings, 1 reply; 33+ messages in thread

From: Etsuro Fujita @ 2026-06-18 10:42 UTC (permalink / raw)
  To: Corey Huinker <[email protected]>; +Cc: Robert Haas <[email protected]>; pgsql-hackers

On Thu, Jun 18, 2026 at 1:00 AM Corey Huinker <[email protected]> wrote:

>> IMO I don't think we need to go that far, because 1) the new functions
>> are provided for FDW authors only, and 2) we don't make changes to the
>> stats that often.
>
>
> That would be simpler, to be sure. However, in the past I've received pushback on functions that had a large number of parameters, and this would definitely be a large number of parameters, approximately 17, so I thought I should at least offer the variadic way.
>
> I'm proceeding with the many-parameters model.

I think that that's acceptable, considering that
heap_create_with_catalog() has 21 parameters.

Thanks!

Best regards,
Etsuro Fujita






^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-17 10:23             ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-17 16:00               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-18 10:42                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
@ 2026-06-18 21:23                   ` Corey Huinker <[email protected]>
  2026-06-19 12:30                     ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-19 13:45                     ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  0 siblings, 2 replies; 33+ messages in thread

From: Corey Huinker @ 2026-06-18 21:23 UTC (permalink / raw)
  To: Etsuro Fujita <[email protected]>; +Cc: Robert Haas <[email protected]>; pgsql-hackers

On Thu, Jun 18, 2026 at 6:43 AM Etsuro Fujita <[email protected]>
wrote:

> On Thu, Jun 18, 2026 at 1:00 AM Corey Huinker <[email protected]>
> wrote:
>
> >> IMO I don't think we need to go that far, because 1) the new functions
> >> are provided for FDW authors only, and 2) we don't make changes to the
> >> stats that often.
> >
> >
> > That would be simpler, to be sure. However, in the past I've received
> pushback on functions that had a large number of parameters, and this would
> definitely be a large number of parameters, approximately 17, so I thought
> I should at least offer the variadic way.
> >
> > I'm proceeding with the many-parameters model.
>
> I think that that's acceptable, considering that
> heap_create_with_catalog() has 21 parameters.
>
> Thanks!
>
> Best regards,
> Etsuro Fujita
>

Attached are two patches to remove SPI from the postgres_fdw.c, replaced by
local C functions.  The division into two patches is entirely for
readability. Separately, each one represents a half-measure that would
benefit no one. The first does the minimal changes needed to get the
relation-stats to stop using SPI, and the second does the same for
attribute stats, removes all vestiges of SPI from postgres_fdw.c, and adds
some tests to make sure that a foreign table has the exact stats of the
source loopback table.

The function import_attribute_stats() is a bit parameter-happy, but that's
mostly necessary. I left the non-key parameters as all type char* because
that avoids even more "bool foo_isnull" parameters, and that allows the
caller to not have to rework the various stat types into their
corresponding C types (float, int4, float[], and the anyarrays that
basically aren't cast-able by any client program anyway). I'm open to
requiring the caller to use the right datatypes for some of the parameters,
but as I said there is no way to "win" with the anyarrays, and even the
pg_restore_attribute_stats() function falls back to type text for those.

To be clear, this solves the SPI problem, but questions about the design of
attribute_statistics_update() and relation_statistics_update() remain, but
those concerns are now isolated within their respective files
attribute_stats.c and relation_stats.c. The inefficiencies therein aren't
really in a critical path, so if we wanted to leave them be until v20 they
could, but if time allows I'd at least like try unwinding some of that. But
first, let's get SPI out of postgres_fdw.c.


Attachments:

  [text/x-patch] v1-0001-Remove-SPI-in-favor-of-import_relation_statistics.patch (9.8K, ../../CADkLM=cL9ZGvO-72QWyXA5Wqv-4P3T1fp+VFJy5e6k3Hf=POMw@mail.gmail.com/3-v1-0001-Remove-SPI-in-favor-of-import_relation_statistics.patch)
  download | inline diff:
From c6cfd11ed460204cb96c5c35c7cc513af14aaa84 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email protected]>
Date: Wed, 17 Jun 2026 16:04:55 -0400
Subject: [PATCH v1 1/2] Remove SPI in favor of import_relation_statistics.

This patch removes the SPI calls in postgres_fdw.c that were related to
relation statistics. Instead, the function import_relation_statiticss()
has been created specifically for this purpose.

The main purpose of this patch is to make a simple API that is usable by
any foreign data wrapper, but is not limited to usage by foreign data
wrappers.

Presently the function marshalls the paratmeters to make a call to
relation_statistics_update(). This is not an ideal end-state, as this
creates un-necessary value translations. The solution is refactor
relation_statistics_update() to be a more conventional, non-fcinfo-style
function, and refactor pg_restore_relation_stats() and
pg_clear_relation_stats() to accommodate that. This patch removes the
difficulties that SPI presents from postgres_fdw.c and localizes the
code redundancy to relation_stats.c.
---
 src/include/statistics/relation_stats.h | 22 ++++++
 src/backend/statistics/relation_stats.c | 96 +++++++++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.c     | 62 ++++------------
 3 files changed, 133 insertions(+), 47 deletions(-)
 create mode 100644 src/include/statistics/relation_stats.h

diff --git a/src/include/statistics/relation_stats.h b/src/include/statistics/relation_stats.h
new file mode 100644
index 00000000000..f68a1a755a1
--- /dev/null
+++ b/src/include/statistics/relation_stats.h
@@ -0,0 +1,22 @@
+/*-------------------------------------------------------------------------
+ *
+ * relation_stats.h
+ *    Functions for the internal manipulation of relation statistics.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/statistics/relation_stats.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef RELATION_STATS_H
+
+#include "access/genam.h"
+
+bool import_relation_statistics(Relation rel, const char *relpages,
+								const char *reltuples, const char *relallvisible,
+								const char *relallfrozen);
+
+#define RELATION_STATS_H
+#endif
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index d6631e9a9a4..9c308ee50e5 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -22,7 +22,9 @@
 #include "catalog/namespace.h"
 #include "nodes/makefuncs.h"
 #include "statistics/stat_utils.h"
+#include "statistics/relation_stats.h"
 #include "utils/builtins.h"
+#include "utils/float.h"
 #include "utils/fmgroids.h"
 #include "utils/fmgrprotos.h"
 #include "utils/lsyscache.h"
@@ -241,3 +243,97 @@ pg_restore_relation_stats(PG_FUNCTION_ARGS)
 
 	PG_RETURN_BOOL(result);
 }
+
+/*
+ * Convenience routine to parse BlockNumber values, and emit a warning
+ * on parse errors.
+ *
+ * Returns 0 if the value is NULL or invalid.
+ */
+static BlockNumber
+str_to_blocknumber(const char *s)
+{
+	const BlockNumber default_value = 0;
+
+	BlockNumber result;
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	if (!s)
+		return default_value;
+
+	result = uint32in_subr(s, NULL, "BlockNumber", (Node *) &escontext);
+
+	if (escontext.error_occurred)
+	{
+		escontext.error_data->elevel = WARNING;
+		ThrowErrorData(escontext.error_data);
+		FreeErrorData(escontext.error_data);
+
+		return default_value;
+	}
+
+	return result;
+}
+
+/*
+ * Convenience routine to parse float values, and emit a warning on parse
+ * errors.
+ *
+ * Returns -1.0 if the value is NULL or invalid.
+ */
+static float
+str_to_float(const char *s)
+{
+	const float default_value = -1.0;
+
+	float		result;
+
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	if (!s)
+		return default_value;
+
+	result = float4in_internal((char *) s, NULL, "float", s, (Node *) &escontext);
+
+	if (escontext.error_occurred)
+	{
+		escontext.error_data->elevel = WARNING;
+		ThrowErrorData(escontext.error_data);
+		FreeErrorData(escontext.error_data);
+		return default_value;
+	}
+
+	return result;
+}
+
+/*
+ * Import relation statistics from regular string inputs.
+ */
+bool
+import_relation_statistics(Relation rel, const char *relpages,
+						   const char *reltuples, const char *relallvisible,
+						   const char *relallfrozen)
+{
+	LOCAL_FCINFO(newfcinfo, NUM_RELATION_STATS_ARGS);
+
+	InitFunctionCallInfoData(*newfcinfo, NULL, NUM_RELATION_STATS_ARGS, InvalidOid, NULL, NULL);
+
+	/*
+	 * Convert all string inputs to their required datatypes. NULL values are
+	 * left as the default.
+	 */
+	newfcinfo->args[RELSCHEMA_ARG].value = CStringGetTextDatum(get_namespace_name(RelationGetNamespace(rel)));
+	newfcinfo->args[RELSCHEMA_ARG].isnull = false;
+	newfcinfo->args[RELNAME_ARG].value = CStringGetTextDatum(RelationGetRelationName(rel));
+	newfcinfo->args[RELNAME_ARG].isnull = false;
+	newfcinfo->args[RELPAGES_ARG].value = UInt32GetDatum(str_to_blocknumber(relpages));
+	newfcinfo->args[RELPAGES_ARG].isnull = false;
+	newfcinfo->args[RELTUPLES_ARG].value = Float4GetDatum(str_to_float(reltuples));
+	newfcinfo->args[RELTUPLES_ARG].isnull = false;
+	newfcinfo->args[RELALLVISIBLE_ARG].value = UInt32GetDatum(str_to_blocknumber(relallvisible));
+	newfcinfo->args[RELALLVISIBLE_ARG].isnull = false;
+	newfcinfo->args[RELALLFROZEN_ARG].value = UInt32GetDatum(str_to_blocknumber(relallfrozen));
+	newfcinfo->args[RELALLFROZEN_ARG].isnull = false;
+
+	return relation_statistics_update(newfcinfo);
+}
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 6dbae583ecc..673d56117b8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -43,6 +43,7 @@
 #include "parser/parsetree.h"
 #include "postgres_fdw.h"
 #include "statistics/statistics.h"
+#include "statistics/relation_stats.h"
 #include "storage/latch.h"
 #include "utils/builtins.h"
 #include "utils/float.h"
@@ -367,33 +368,6 @@ enum AttStatsColumns
 	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"
@@ -714,7 +688,8 @@ 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 rel,
+									  const char *schemaname,
 									  const char *relname,
 									  int attrcnt,
 									  const RemoteAttributeMapping *remattrmap,
@@ -5661,7 +5636,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)
@@ -6128,7 +6103,8 @@ match_attrmap(PGresult *res,
  * 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,
@@ -6141,6 +6117,9 @@ import_fetched_statistics(const char *schemaname,
 	int			spirc;
 	bool		ok = false;
 
+	char	   *relpages = NULL;
+	char	   *reltuples = NULL;
+
 	/* Assign all the invariant parameters common to relation/attribute stats */
 	values[ATTIMPORT_SQL_VERSION] = Int32GetDatum(remstats->server_version_num);
 	nulls[ATTIMPORT_SQL_VERSION] = ' ';
@@ -6233,27 +6212,16 @@ import_fetched_statistics(const char *schemaname,
 	/*
 	 * 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.
 	 */
 	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);
+	if (!PQgetisnull(remstats->rel, 0, RELSTATS_RELPAGES))
+		relpages = PQgetvalue(remstats->rel, 0, RELSTATS_RELPAGES);
 
-	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 (!PQgetisnull(remstats->rel, 0, RELSTATS_RELTUPLES))
+		reltuples = PQgetvalue(remstats->rel, 0, RELSTATS_RELTUPLES);
 
-	if (!import_spi_query_ok())
+	if (!import_relation_statistics(relation, relpages, reltuples,
+									NULL, NULL))
 	{
 		ereport(WARNING,
 				errmsg("could not import statistics for foreign table \"%s.%s\" --- relation statistics import failed for this foreign table",

base-commit: f04781df5daf112840d08eab39fb95505fda6c9c
-- 
2.54.0



  [text/x-patch] v1-0002-Remove-SPI-in-favor-of-import_attribute_statistic.patch (22.9K, ../../CADkLM=cL9ZGvO-72QWyXA5Wqv-4P3T1fp+VFJy5e6k3Hf=POMw@mail.gmail.com/4-v1-0002-Remove-SPI-in-favor-of-import_attribute_statistic.patch)
  download | inline diff:
From 3b9cfbbbcda36ae2f4444b5eefe7a8f2961333f0 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email protected]>
Date: Thu, 18 Jun 2026 16:28:10 -0400
Subject: [PATCH v1 2/2] Remove SPI in favor of import_attribute_statistics.

This patch removes the SPI calls in postgres_fdw.c that were related to
attribute statistics. Instead, the functions import_attribute_statistics()
and clear_attribute_statistics() have been created for this purpose.

Like the preceding patch, the goal is to remove SPI from the workflow
and isolate all code redundancy in attribute_stats.c, where it can be
handled at a later time.

Some additional checks were added to the regression tests to ensure that
values are properly conveyed from a loopback-foreign table to the local
table.
---
 src/include/statistics/attribute_stats.h      |  36 +++
 src/backend/statistics/attribute_stats.c      | 192 +++++++++++++
 .../postgres_fdw/expected/postgres_fdw.out    |  41 +++
 contrib/postgres_fdw/postgres_fdw.c           | 258 +++---------------
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  32 +++
 5 files changed, 339 insertions(+), 220 deletions(-)
 create mode 100644 src/include/statistics/attribute_stats.h

diff --git a/src/include/statistics/attribute_stats.h b/src/include/statistics/attribute_stats.h
new file mode 100644
index 00000000000..4ebde0c7c3b
--- /dev/null
+++ b/src/include/statistics/attribute_stats.h
@@ -0,0 +1,36 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ATTRIBUTE_STATS_H
+
+#include "access/genam.h"
+
+
+bool delete_attribute_statistics(Relation rel, AttrNumber attnum, bool inherited);
+
+bool import_attribute_statistics(Relation rel, const char *attname,
+								 AttrNumber attnum, bool inherited,
+								 const char *null_frac, const char *avg_width,
+								 const char *n_distinct,
+								 const char *most_common_vals,
+								 const char *most_common_freqs,
+								 const char *histogram_bounds,
+								 const char *correlation,
+								 const char *most_common_elems,
+								 const char *most_common_elem_freqs,
+								 const char *elem_count_histogram,
+								 const char *range_length_histogram,
+								 const char *range_empty_frac,
+								 const char *range_bounds_histogram);
+
+#define ATTRIBUTE_STATS_H
+#endif
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 1cc4d657231..f67d62c0104 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -22,10 +22,12 @@
 #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"
 #include "utils/builtins.h"
+#include "utils/float.h"
 #include "utils/fmgroids.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
@@ -688,3 +690,193 @@ pg_restore_attribute_stats(PG_FUNCTION_ARGS)
 
 	PG_RETURN_BOOL(result);
 }
+
+/*
+ * Convenience routine for setting optional text arguments
+ */
+static void
+set_text_arg(NullableDatum *arg, const char *s)
+{
+	if (s)
+	{
+		arg->value = CStringGetTextDatum(s);
+		arg->isnull = false;
+	}
+	else
+	{
+		arg->value = (Datum) 0;
+		arg->isnull = true;
+	}
+}
+
+/*
+ * Convenience routine for setting optional float arguments
+ */
+static void
+set_float_arg(NullableDatum *arg, const char *s)
+{
+	arg->value = (Datum) 0;
+	arg->isnull = true;
+
+	if (s)
+	{
+		ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+		float4		val;
+
+		val = float4in_internal((char *) s, NULL, "float", s, (Node *) &escontext);
+
+		if (escontext.error_occurred)
+		{
+			escontext.error_data->elevel = WARNING;
+			ThrowErrorData(escontext.error_data);
+			FreeErrorData(escontext.error_data);
+		}
+		else
+		{
+			arg->value = Float4GetDatum(val);
+			arg->isnull = false;
+		}
+	}
+}
+
+/*
+ * Convenience routine for setting optional int32 arguments
+ */
+static void
+set_int32_arg(NullableDatum *arg, const char *s)
+{
+	arg->value = (Datum) 0;
+	arg->isnull = true;
+
+	if (s)
+	{
+		ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+		int32		val;
+
+		val = pg_strtoint32_safe(s, (Node *) &escontext);
+
+		if (escontext.error_occurred)
+		{
+			escontext.error_data->elevel = WARNING;
+			ThrowErrorData(escontext.error_data);
+			FreeErrorData(escontext.error_data);
+		}
+		else
+		{
+			arg->value = Int32GetDatum(val);
+			arg->isnull = false;
+		}
+	}
+}
+
+/*
+ * Convenience routine for setting optional float[] arguments
+ */
+static void
+set_floatarr_arg(NullableDatum *arg, const char *s)
+{
+	arg->value = (Datum) 0;
+	arg->isnull = true;
+
+	if (s)
+	{
+		ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+		FmgrInfo	flinfo;
+		Datum		val;
+
+		fmgr_info(F_ARRAY_IN, &flinfo);
+
+		if (!InputFunctionCallSafe(&flinfo, (char *) s, FLOAT4OID, -1,
+								   (Node *) &escontext, &val))
+		{
+			escontext.error_data->elevel = WARNING;
+			ThrowErrorData(escontext.error_data);
+			FreeErrorData(escontext.error_data);
+		}
+		else
+		{
+			arg->value = val;
+			arg->isnull = false;
+		}
+	}
+}
+
+/*
+ * Delete attribute statistics.
+ */
+bool
+delete_attribute_statistics(Relation rel, AttrNumber attnum, bool inherited)
+{
+	return delete_pg_statistic(RelationGetRelid(rel), attnum, inherited);
+}
+
+/*
+ * Import attribute statistics from regular string inputs for all statitical
+ * values.
+ *
+ * To convey a "null"/not-set value for attnum, use InvalidAttrNumber.
+ */
+bool
+import_attribute_statistics(Relation rel, const char *attname,
+							AttrNumber attnum, bool inherited,
+							const char *null_frac, const char *avg_width,
+							const char *n_distinct,
+							const char *most_common_vals,
+							const char *most_common_freqs,
+							const char *histogram_bounds,
+							const char *correlation,
+							const char *most_common_elems,
+							const char *most_common_elem_freqs,
+							const char *elem_count_histogram,
+							const char *range_length_histogram,
+							const char *range_empty_frac,
+							const char *range_bounds_histogram)
+{
+	LOCAL_FCINFO(newfcinfo, NUM_ATTRIBUTE_STATS_ARGS);
+
+	InitFunctionCallInfoData(*newfcinfo, NULL, NUM_ATTRIBUTE_STATS_ARGS, InvalidOid, NULL, NULL);
+
+	/*
+	 * Convert all string inputs to their required datatypes. NULL values are
+	 * left as the default.
+	 */
+	newfcinfo->args[ATTRELSCHEMA_ARG].value = CStringGetTextDatum(get_namespace_name(RelationGetNamespace(rel)));
+	newfcinfo->args[ATTRELSCHEMA_ARG].isnull = false;
+	newfcinfo->args[ATTRELNAME_ARG].value = CStringGetTextDatum(RelationGetRelationName(rel));
+	newfcinfo->args[ATTRELNAME_ARG].isnull = false;
+
+	set_text_arg(&newfcinfo->args[ATTNAME_ARG], attname);
+
+	if (attnum != InvalidAttrNumber)
+	{
+		newfcinfo->args[ATTNUM_ARG].value = Int16GetDatum(attnum);
+		newfcinfo->args[ATTNUM_ARG].isnull = false;
+	}
+	else
+	{
+		newfcinfo->args[ATTNUM_ARG].value = (Datum) 0;
+		newfcinfo->args[ATTNUM_ARG].isnull = true;
+	}
+
+	newfcinfo->args[INHERITED_ARG].value = BoolGetDatum(inherited);
+	newfcinfo->args[INHERITED_ARG].isnull = false;
+
+	set_float_arg(&newfcinfo->args[NULL_FRAC_ARG], null_frac);
+	set_int32_arg(&newfcinfo->args[AVG_WIDTH_ARG], avg_width);
+	set_float_arg(&newfcinfo->args[N_DISTINCT_ARG], n_distinct);
+	set_text_arg(&newfcinfo->args[MOST_COMMON_VALS_ARG], most_common_vals);
+	set_floatarr_arg(&newfcinfo->args[MOST_COMMON_FREQS_ARG], most_common_freqs);
+	set_text_arg(&newfcinfo->args[HISTOGRAM_BOUNDS_ARG], histogram_bounds);
+	set_float_arg(&newfcinfo->args[CORRELATION_ARG], correlation);
+	set_text_arg(&newfcinfo->args[MOST_COMMON_ELEMS_ARG], most_common_elems);
+	set_floatarr_arg(&newfcinfo->args[MOST_COMMON_ELEM_FREQS_ARG], most_common_elem_freqs);
+	set_floatarr_arg(&newfcinfo->args[ELEM_COUNT_HISTOGRAM_ARG], elem_count_histogram);
+	set_text_arg(&newfcinfo->args[RANGE_LENGTH_HISTOGRAM_ARG], range_length_histogram);
+	set_float_arg(&newfcinfo->args[RANGE_EMPTY_FRAC_ARG], range_empty_frac);
+	set_text_arg(&newfcinfo->args[RANGE_BOUNDS_HISTOGRAM_ARG], range_bounds_histogram);
+
+	return attribute_statistics_update(newfcinfo);
+}
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index e90289e4ab1..e931fa64e0d 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -12927,6 +12927,47 @@ 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)
+
+-- 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 673d56117b8..8aa589d5a56 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,7 @@
 #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"
@@ -368,109 +368,6 @@ enum AttStatsColumns
 	ATTSTATS_NUM_FIELDS,
 };
 
-/* 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,
-};
-
 /*
  * SQL functions
  */
@@ -688,15 +585,12 @@ static bool match_attrmap(PGresult *res,
 						  const char *remote_relname,
 						  int attrcnt,
 						  RemoteAttributeMapping *remattrmap);
-static bool import_fetched_statistics(Relation rel,
+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);
@@ -6099,6 +5993,19 @@ match_attrmap(PGresult *res,
 	return true;
 }
 
+
+/*
+ * Conenience routine to fetch
+ */
+static char *
+get_opt_value(PGresult *res, int row, int col)
+{
+	if (PQgetisnull(res, row, col))
+		return NULL;
+
+	return PQgetvalue(res, row, col);
+}
+
 /*
  * Import fetched statistics into the local statistics tables.
  */
@@ -6110,28 +6017,11 @@ import_fetched_statistics(Relation relation,
 						  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		ok = false;
 
 	char	   *relpages = NULL;
 	char	   *reltuples = NULL;
 
-	/* 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
@@ -6139,26 +6029,16 @@ import_fetched_statistics(Relation relation,
 	 */
 	if (remstats->att != NULL)
 	{
-		Assert(PQnfields(remstats->att) == ATTSTATS_NUM_FIELDS);
-		Assert(PQntuples(remstats->att) >= 1);
+		PGresult   *res = remstats->att;
 
-		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] = ' ';
+		Assert(PQnfields(res) == ATTSTATS_NUM_FIELDS);
+		Assert(PQntuples(res) >= 1);
 
 		for (int mapidx = 0; mapidx < attrcnt; mapidx++)
 		{
 			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);
@@ -6168,42 +6048,28 @@ import_fetched_statistics(Relation relation,
 			 */
 			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);
+			delete_attribute_statistics(relation, attnum, false);
 
-			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())
+			if (!import_attribute_statistics(relation, attname,
+											 InvalidAttrNumber,
+											 false,
+											 get_opt_value(res, row, ATTSTATS_NULL_FRAC),
+											 get_opt_value(res, row, ATTSTATS_AVG_WIDTH),
+											 get_opt_value(res, row, ATTSTATS_N_DISTINCT),
+											 get_opt_value(res, row, ATTSTATS_MOST_COMMON_VALS),
+											 get_opt_value(res, row, ATTSTATS_MOST_COMMON_FREQS),
+											 get_opt_value(res, row, ATTSTATS_HISTOGRAM_BOUNDS),
+											 get_opt_value(res, row, ATTSTATS_CORRELATION),
+											 get_opt_value(res, row, ATTSTATS_MOST_COMMON_ELEMS),
+											 get_opt_value(res, row, ATTSTATS_MOST_COMMON_ELEM_FREQS),
+											 get_opt_value(res, row, ATTSTATS_ELEM_COUNT_HISTOGRAM),
+											 get_opt_value(res, row, ATTSTATS_RANGE_LENGTH_HISTOGRAM),
+											 get_opt_value(res, row, ATTSTATS_RANGE_EMPTY_FRAC),
+											 get_opt_value(res, row, ATTSTATS_RANGE_BOUNDS_HISTOGRAM)))
 			{
 				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;
 			}
 		}
@@ -6232,57 +6098,9 @@ import_fetched_statistics(Relation relation,
 	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 dfc58beb0d2..627177123b3 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -4584,6 +4584,38 @@ 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;
+
+-- 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.54.0



^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-17 10:23             ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-17 16:00               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-18 10:42                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-18 21:23                   ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
@ 2026-06-19 12:30                     ` Etsuro Fujita <[email protected]>
  1 sibling, 0 replies; 33+ messages in thread

From: Etsuro Fujita @ 2026-06-19 12:30 UTC (permalink / raw)
  To: Corey Huinker <[email protected]>; +Cc: Robert Haas <[email protected]>; pgsql-hackers

On Fri, Jun 19, 2026 at 6:23 AM Corey Huinker <[email protected]> wrote:
> Attached are two patches to remove SPI from the postgres_fdw.c, replaced by local C functions.  The division into two patches is entirely for readability. Separately, each one represents a half-measure that would benefit no one. The first does the minimal changes needed to get the relation-stats to stop using SPI, and the second does the same for attribute stats, removes all vestiges of SPI from postgres_fdw.c, and adds some tests to make sure that a foreign table has the exact stats of the source loopback table.
>
> The function import_attribute_stats() is a bit parameter-happy, but that's mostly necessary. I left the non-key parameters as all type char* because that avoids even more "bool foo_isnull" parameters, and that allows the caller to not have to rework the various stat types into their corresponding C types (float, int4, float[], and the anyarrays that basically aren't cast-able by any client program anyway). I'm open to requiring the caller to use the right datatypes for some of the parameters, but as I said there is no way to "win" with the anyarrays, and even the pg_restore_attribute_stats() function falls back to type text for those.
>
> To be clear, this solves the SPI problem, but questions about the design of attribute_statistics_update() and relation_statistics_update() remain, but those concerns are now isolated within their respective files attribute_stats.c and relation_stats.c. The inefficiencies therein aren't really in a critical path, so if we wanted to leave them be until v20 they could, but if time allows I'd at least like try unwinding some of that. But first, let's get SPI out of postgres_fdw.c.

Ok, I'll review the patches.

Thanks!

Best regards,
Etsuro Fujita






^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-17 10:23             ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-17 16:00               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-18 10:42                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-18 21:23                   ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
@ 2026-06-19 13:45                     ` Robert Haas <[email protected]>
  2026-06-20 04:11                       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  1 sibling, 1 reply; 33+ messages in thread

From: Robert Haas @ 2026-06-19 13:45 UTC (permalink / raw)
  To: Corey Huinker <[email protected]>; +Cc: Etsuro Fujita <[email protected]>; pgsql-hackers

On Thu, Jun 18, 2026 at 5:23 PM Corey Huinker <[email protected]> wrote:
> To be clear, this solves the SPI problem, but questions about the design of attribute_statistics_update() and relation_statistics_update() remain, but those concerns are now isolated within their respective files attribute_stats.c and relation_stats.c. The inefficiencies therein aren't really in a critical path, so if we wanted to leave them be until v20 they could, but if time allows I'd at least like try unwinding some of that. But first, let's get SPI out of postgres_fdw.c.

I think that's the right order of priority, but I don't think that
having import_attribute_statistics construct an fcinfo is great.
Ideally, attribute_statistics_update would call
import_attribute_statistics rather than the other way around.

-- 
Robert Haas
EDB: http://www.enterprisedb.com






^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-17 10:23             ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-17 16:00               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-18 10:42                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-18 21:23                   ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-19 13:45                     ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
@ 2026-06-20 04:11                       ` Corey Huinker <[email protected]>
  2026-06-22 19:08                         ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  0 siblings, 1 reply; 33+ messages in thread

From: Corey Huinker @ 2026-06-20 04:11 UTC (permalink / raw)
  To: Robert Haas <[email protected]>; +Cc: Etsuro Fujita <[email protected]>; pgsql-hackers

On Fri, Jun 19, 2026 at 9:45 AM Robert Haas <[email protected]> wrote:

> On Thu, Jun 18, 2026 at 5:23 PM Corey Huinker <[email protected]>
> wrote:
> > To be clear, this solves the SPI problem, but questions about the design
> of attribute_statistics_update() and relation_statistics_update() remain,
> but those concerns are now isolated within their respective files
> attribute_stats.c and relation_stats.c. The inefficiencies therein aren't
> really in a critical path, so if we wanted to leave them be until v20 they
> could, but if time allows I'd at least like try unwinding some of that. But
> first, let's get SPI out of postgres_fdw.c.
>
> I think that's the right order of priority, but I don't think that
> having import_attribute_statistics construct an fcinfo is great.
>

Me either, I'm looking at "phase 2" already where
relation/attribute_statistics_update becomes a conventional function, and
pg_clear_attribute_stats and pg_restore_attribute_stats (and their relstats
equivalents) marshal their parameters to call that instead.


> Ideally, attribute_statistics_update would call
> import_attribute_statistics rather than the other way around.
>

I think we're mostly on the same page. If we require the caller to
understand the stats data structures to a greater level of detail, and
require it to do the transformations to the proper input types
(BlockNumbers, floats, float arrays, cstrings for anyarray, etc), then the
import_attribute_statistics functon and the new attribute_statistics_update
would be one-in-the-same. The v1 patch leaned heavily (perhaps too far)
towards letting the caller pass along string values fetched via
PQgetvalue() from a pg_stats without modification.


^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-17 10:23             ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-17 16:00               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-18 10:42                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-18 21:23                   ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-19 13:45                     ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-20 04:11                       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
@ 2026-06-22 19:08                         ` Corey Huinker <[email protected]>
  2026-06-23 11:43                           ` Re: use of SPI by postgresImportForeignStatistics jian he <[email protected]>
  2026-06-23 12:10                           ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  0 siblings, 2 replies; 33+ messages in thread

From: Corey Huinker @ 2026-06-22 19:08 UTC (permalink / raw)
  To: Robert Haas <[email protected]>; +Cc: Etsuro Fujita <[email protected]>; pgsql-hackers

On Sat, Jun 20, 2026 at 12:11 AM Corey Huinker <[email protected]>
wrote:

> On Fri, Jun 19, 2026 at 9:45 AM Robert Haas <[email protected]> wrote:
>
>> On Thu, Jun 18, 2026 at 5:23 PM Corey Huinker <[email protected]>
>> wrote:
>> > To be clear, this solves the SPI problem, but questions about the
>> design of attribute_statistics_update() and relation_statistics_update()
>> remain, but those concerns are now isolated within their respective files
>> attribute_stats.c and relation_stats.c. The inefficiencies therein aren't
>> really in a critical path, so if we wanted to leave them be until v20 they
>> could, but if time allows I'd at least like try unwinding some of that. But
>> first, let's get SPI out of postgres_fdw.c.
>>
>> I think that's the right order of priority, but I don't think that
>> having import_attribute_statistics construct an fcinfo is great.
>>
>
> Me either, I'm looking at "phase 2" already where
> relation/attribute_statistics_update becomes a conventional function, and
> pg_clear_attribute_stats and pg_restore_attribute_stats (and their relstats
> equivalents) marshal their parameters to call that instead.
>
>
>> Ideally, attribute_statistics_update would call
>> import_attribute_statistics rather than the other way around.
>>
>
> I think we're mostly on the same page. If we require the caller to
> understand the stats data structures to a greater level of detail, and
> require it to do the transformations to the proper input types
> (BlockNumbers, floats, float arrays, cstrings for anyarray, etc), then the
> import_attribute_statistics functon and the new attribute_statistics_update
> would be one-in-the-same. The v1 patch leaned heavily (perhaps too far)
> towards letting the caller pass along string values fetched via
> PQgetvalue() from a pg_stats without modification.
>

An update on my progress on Phase 2:

I was able to convert relation_statistics_update() with relatively little
fuss, but ran into trouble in doing so for attribute_statistics_update().

Initially the plan was to have a data structure
RelationStatsData/AttributeStatsData which contained a series of boolean
has_FOO flags alongside FOO of the actual stat type (int32, float, float[],
or cstring for anyarray value) and have the respective functions convert
their parameters to this common structure before calling the common
function.

The first bit of dissonance comes from the SQL-level functions having
schemaname+relname parameters, and the reloid is resolved via
RangeVarGetRelidExtended() which has a callback to check for correct
permissions and setting the proper lock level. However, the C-caller would
either have the reloid already, or an already open Relation, but no
assurance that the caller has the correct permissions for that table or the
correct lock level on the table. So either we make an equivalent to
RangeVarGetRelidExtended() that takes an oid, or the C-caller has to derive
a RangeVar, call the existing RangeVarGetRelidExtended() function, and
verify the result reloid against the supplied parameter. I went with
deriving the RangeVar and putting an Assert on the before/after reloids,
but perhaps the smarter play is to make a function that checks for
ShareUpdateExclusiveLock on the Relation, and then does the equivalent of
RangeVarCallbackForStats().

A smaller bit of dissonance was with RecoveryInProgress(), which if I
recall we're checking before RangeVarGetRelidExtended() to avoid trying to
take a lock that will fail. That check might not be meaningful if the C
call takes a relation, thus ensuring that some level of locking worked,
thus we aren't in recovery.

Next is the existing validation functions stats_check_required_arg(),
stats_check_arg_array(), and stats_check_arg_pairs() all work on values
indexed by the positional functioncallinfo and the corresponding
relstatsinfo/attstatsinfo structure, and this makes a lot of type checking
and value checking compact and uniform.  If we want to keep this sort of
uniformity, the resulting StatsData structure will end up looking a lot
like the FunctionCallInfo that we already had.

Next is the fact that the end-destination for every value passed in is a
Datum for a pg_statistic heaptuple. Most Datum values are checked only for
their null-ness and if they're the correct type, so the value itself is
usually just passed directly from fcinfo into the heap tuple values[]
array. The float[] values are checked for number of elements and whether
any elements are NULL, but that is done via array functions that take a
Datum input. Only in a few cases do we actually look at the actual internal
value of the Datum (reltuples, attname, attnum, the anyarrays), so there's
little to gain there.

There's some additional hassle in the fact that
pg_restore_attribute_stats() can take an attnum parameter OR an attname
parameter, but not both. I was able to resolve that in a semi-elegant
fashion, but the other issues have convinced me that we're probably better
off continuing to use the FunctionCallInfo version of
attribute_stats_update(), though perhaps with a different name, allowing us
to use that name for the new API call instead of
import_attribute_statistics().

One thing we *do* need to change from my v1 patch is moving the recovery
check and the RangeVar check out of relation_statistics_update() and
attribute_statsistics_update(), and having the respective callers do those
checks themselves first, passing in the now-authoritative reloid from
stats_acquire_relation_lock().

To that end, here's a new and rebased patch set:

0001 - exactly the same as before
0002 - exactly the same as before
0003 - New stat_utils.c function stats_acquire_relation_lock() which covers
the RangeVar and Recovery checks common to all the functions that modify
relation or attribute stats.
0004 - Add a small regression test to relation stats
0005 - Use stats_acquire_relation_lock() in relstats functions, and rename
the publicly-facing relstats C function.
0006 - Rename the publicly-facing attstats functions.
0007 - Use stats_acquire_relation_lock() in attstats functions.


Attachments:

  [text/x-patch] v2-0001-Remove-SPI-in-favor-of-import_relation_statistics.patch (9.8K, ../../CADkLM=dWJaw04u9FU=WkykOy7bu7B5eKmoDkfVz6JXh7mdemkw@mail.gmail.com/3-v2-0001-Remove-SPI-in-favor-of-import_relation_statistics.patch)
  download | inline diff:
From efae549731c95c36c1b11d0668ef3397854439a9 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email protected]>
Date: Wed, 17 Jun 2026 16:04:55 -0400
Subject: [PATCH v2 1/7] Remove SPI in favor of import_relation_statistics.

This patch removes the SPI calls in postgres_fdw.c that were related to
relation statistics. Instead, the function import_relation_statiticss()
has been created specifically for this purpose.

The main purpose of this patch is to make a simple API that is usable by
any foreign data wrapper, but is not limited to usage by foreign data
wrappers.

Presently the function marshalls the paratmeters to make a call to
relation_statistics_update(). This is not an ideal end-state, as this
creates un-necessary value translations. The solution is refactor
relation_statistics_update() to be a more conventional, non-fcinfo-style
function, and refactor pg_restore_relation_stats() and
pg_clear_relation_stats() to accommodate that. This patch removes the
difficulties that SPI presents from postgres_fdw.c and localizes the
code redundancy to relation_stats.c.
---
 src/include/statistics/relation_stats.h | 22 ++++++
 src/backend/statistics/relation_stats.c | 96 +++++++++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.c     | 62 ++++------------
 3 files changed, 133 insertions(+), 47 deletions(-)
 create mode 100644 src/include/statistics/relation_stats.h

diff --git a/src/include/statistics/relation_stats.h b/src/include/statistics/relation_stats.h
new file mode 100644
index 00000000000..f68a1a755a1
--- /dev/null
+++ b/src/include/statistics/relation_stats.h
@@ -0,0 +1,22 @@
+/*-------------------------------------------------------------------------
+ *
+ * relation_stats.h
+ *    Functions for the internal manipulation of relation statistics.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/statistics/relation_stats.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef RELATION_STATS_H
+
+#include "access/genam.h"
+
+bool import_relation_statistics(Relation rel, const char *relpages,
+								const char *reltuples, const char *relallvisible,
+								const char *relallfrozen);
+
+#define RELATION_STATS_H
+#endif
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index d6631e9a9a4..9c308ee50e5 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -22,7 +22,9 @@
 #include "catalog/namespace.h"
 #include "nodes/makefuncs.h"
 #include "statistics/stat_utils.h"
+#include "statistics/relation_stats.h"
 #include "utils/builtins.h"
+#include "utils/float.h"
 #include "utils/fmgroids.h"
 #include "utils/fmgrprotos.h"
 #include "utils/lsyscache.h"
@@ -241,3 +243,97 @@ pg_restore_relation_stats(PG_FUNCTION_ARGS)
 
 	PG_RETURN_BOOL(result);
 }
+
+/*
+ * Convenience routine to parse BlockNumber values, and emit a warning
+ * on parse errors.
+ *
+ * Returns 0 if the value is NULL or invalid.
+ */
+static BlockNumber
+str_to_blocknumber(const char *s)
+{
+	const BlockNumber default_value = 0;
+
+	BlockNumber result;
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	if (!s)
+		return default_value;
+
+	result = uint32in_subr(s, NULL, "BlockNumber", (Node *) &escontext);
+
+	if (escontext.error_occurred)
+	{
+		escontext.error_data->elevel = WARNING;
+		ThrowErrorData(escontext.error_data);
+		FreeErrorData(escontext.error_data);
+
+		return default_value;
+	}
+
+	return result;
+}
+
+/*
+ * Convenience routine to parse float values, and emit a warning on parse
+ * errors.
+ *
+ * Returns -1.0 if the value is NULL or invalid.
+ */
+static float
+str_to_float(const char *s)
+{
+	const float default_value = -1.0;
+
+	float		result;
+
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	if (!s)
+		return default_value;
+
+	result = float4in_internal((char *) s, NULL, "float", s, (Node *) &escontext);
+
+	if (escontext.error_occurred)
+	{
+		escontext.error_data->elevel = WARNING;
+		ThrowErrorData(escontext.error_data);
+		FreeErrorData(escontext.error_data);
+		return default_value;
+	}
+
+	return result;
+}
+
+/*
+ * Import relation statistics from regular string inputs.
+ */
+bool
+import_relation_statistics(Relation rel, const char *relpages,
+						   const char *reltuples, const char *relallvisible,
+						   const char *relallfrozen)
+{
+	LOCAL_FCINFO(newfcinfo, NUM_RELATION_STATS_ARGS);
+
+	InitFunctionCallInfoData(*newfcinfo, NULL, NUM_RELATION_STATS_ARGS, InvalidOid, NULL, NULL);
+
+	/*
+	 * Convert all string inputs to their required datatypes. NULL values are
+	 * left as the default.
+	 */
+	newfcinfo->args[RELSCHEMA_ARG].value = CStringGetTextDatum(get_namespace_name(RelationGetNamespace(rel)));
+	newfcinfo->args[RELSCHEMA_ARG].isnull = false;
+	newfcinfo->args[RELNAME_ARG].value = CStringGetTextDatum(RelationGetRelationName(rel));
+	newfcinfo->args[RELNAME_ARG].isnull = false;
+	newfcinfo->args[RELPAGES_ARG].value = UInt32GetDatum(str_to_blocknumber(relpages));
+	newfcinfo->args[RELPAGES_ARG].isnull = false;
+	newfcinfo->args[RELTUPLES_ARG].value = Float4GetDatum(str_to_float(reltuples));
+	newfcinfo->args[RELTUPLES_ARG].isnull = false;
+	newfcinfo->args[RELALLVISIBLE_ARG].value = UInt32GetDatum(str_to_blocknumber(relallvisible));
+	newfcinfo->args[RELALLVISIBLE_ARG].isnull = false;
+	newfcinfo->args[RELALLFROZEN_ARG].value = UInt32GetDatum(str_to_blocknumber(relallfrozen));
+	newfcinfo->args[RELALLFROZEN_ARG].isnull = false;
+
+	return relation_statistics_update(newfcinfo);
+}
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 6dbae583ecc..673d56117b8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -43,6 +43,7 @@
 #include "parser/parsetree.h"
 #include "postgres_fdw.h"
 #include "statistics/statistics.h"
+#include "statistics/relation_stats.h"
 #include "storage/latch.h"
 #include "utils/builtins.h"
 #include "utils/float.h"
@@ -367,33 +368,6 @@ enum AttStatsColumns
 	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"
@@ -714,7 +688,8 @@ 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 rel,
+									  const char *schemaname,
 									  const char *relname,
 									  int attrcnt,
 									  const RemoteAttributeMapping *remattrmap,
@@ -5661,7 +5636,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)
@@ -6128,7 +6103,8 @@ match_attrmap(PGresult *res,
  * 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,
@@ -6141,6 +6117,9 @@ import_fetched_statistics(const char *schemaname,
 	int			spirc;
 	bool		ok = false;
 
+	char	   *relpages = NULL;
+	char	   *reltuples = NULL;
+
 	/* Assign all the invariant parameters common to relation/attribute stats */
 	values[ATTIMPORT_SQL_VERSION] = Int32GetDatum(remstats->server_version_num);
 	nulls[ATTIMPORT_SQL_VERSION] = ' ';
@@ -6233,27 +6212,16 @@ import_fetched_statistics(const char *schemaname,
 	/*
 	 * 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.
 	 */
 	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);
+	if (!PQgetisnull(remstats->rel, 0, RELSTATS_RELPAGES))
+		relpages = PQgetvalue(remstats->rel, 0, RELSTATS_RELPAGES);
 
-	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 (!PQgetisnull(remstats->rel, 0, RELSTATS_RELTUPLES))
+		reltuples = PQgetvalue(remstats->rel, 0, RELSTATS_RELTUPLES);
 
-	if (!import_spi_query_ok())
+	if (!import_relation_statistics(relation, relpages, reltuples,
+									NULL, NULL))
 	{
 		ereport(WARNING,
 				errmsg("could not import statistics for foreign table \"%s.%s\" --- relation statistics import failed for this foreign table",

base-commit: 80bb0ebcc11fb9cb5904d490640408b3d7003a17
-- 
2.54.0



  [text/x-patch] v2-0002-Remove-SPI-in-favor-of-import_attribute_statistic.patch (22.9K, ../../CADkLM=dWJaw04u9FU=WkykOy7bu7B5eKmoDkfVz6JXh7mdemkw@mail.gmail.com/4-v2-0002-Remove-SPI-in-favor-of-import_attribute_statistic.patch)
  download | inline diff:
From c0be3905e22eafcfa09ed1f9644008897b210416 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email protected]>
Date: Thu, 18 Jun 2026 16:28:10 -0400
Subject: [PATCH v2 2/7] Remove SPI in favor of import_attribute_statistics.

This patch removes the SPI calls in postgres_fdw.c that were related to
attribute statistics. Instead, the functions import_attribute_statistics()
and clear_attribute_statistics() have been created for this purpose.

Like the preceding patch, the goal is to remove SPI from the workflow
and isolate all code redundancy in attribute_stats.c, where it can be
handled at a later time.

Some additional checks were added to the regression tests to ensure that
values are properly conveyed from a loopback-foreign table to the local
table.
---
 src/include/statistics/attribute_stats.h      |  36 +++
 src/backend/statistics/attribute_stats.c      | 192 +++++++++++++
 .../postgres_fdw/expected/postgres_fdw.out    |  41 +++
 contrib/postgres_fdw/postgres_fdw.c           | 258 +++---------------
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  32 +++
 5 files changed, 339 insertions(+), 220 deletions(-)
 create mode 100644 src/include/statistics/attribute_stats.h

diff --git a/src/include/statistics/attribute_stats.h b/src/include/statistics/attribute_stats.h
new file mode 100644
index 00000000000..4ebde0c7c3b
--- /dev/null
+++ b/src/include/statistics/attribute_stats.h
@@ -0,0 +1,36 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ATTRIBUTE_STATS_H
+
+#include "access/genam.h"
+
+
+bool delete_attribute_statistics(Relation rel, AttrNumber attnum, bool inherited);
+
+bool import_attribute_statistics(Relation rel, const char *attname,
+								 AttrNumber attnum, bool inherited,
+								 const char *null_frac, const char *avg_width,
+								 const char *n_distinct,
+								 const char *most_common_vals,
+								 const char *most_common_freqs,
+								 const char *histogram_bounds,
+								 const char *correlation,
+								 const char *most_common_elems,
+								 const char *most_common_elem_freqs,
+								 const char *elem_count_histogram,
+								 const char *range_length_histogram,
+								 const char *range_empty_frac,
+								 const char *range_bounds_histogram);
+
+#define ATTRIBUTE_STATS_H
+#endif
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 1cc4d657231..f67d62c0104 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -22,10 +22,12 @@
 #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"
 #include "utils/builtins.h"
+#include "utils/float.h"
 #include "utils/fmgroids.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
@@ -688,3 +690,193 @@ pg_restore_attribute_stats(PG_FUNCTION_ARGS)
 
 	PG_RETURN_BOOL(result);
 }
+
+/*
+ * Convenience routine for setting optional text arguments
+ */
+static void
+set_text_arg(NullableDatum *arg, const char *s)
+{
+	if (s)
+	{
+		arg->value = CStringGetTextDatum(s);
+		arg->isnull = false;
+	}
+	else
+	{
+		arg->value = (Datum) 0;
+		arg->isnull = true;
+	}
+}
+
+/*
+ * Convenience routine for setting optional float arguments
+ */
+static void
+set_float_arg(NullableDatum *arg, const char *s)
+{
+	arg->value = (Datum) 0;
+	arg->isnull = true;
+
+	if (s)
+	{
+		ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+		float4		val;
+
+		val = float4in_internal((char *) s, NULL, "float", s, (Node *) &escontext);
+
+		if (escontext.error_occurred)
+		{
+			escontext.error_data->elevel = WARNING;
+			ThrowErrorData(escontext.error_data);
+			FreeErrorData(escontext.error_data);
+		}
+		else
+		{
+			arg->value = Float4GetDatum(val);
+			arg->isnull = false;
+		}
+	}
+}
+
+/*
+ * Convenience routine for setting optional int32 arguments
+ */
+static void
+set_int32_arg(NullableDatum *arg, const char *s)
+{
+	arg->value = (Datum) 0;
+	arg->isnull = true;
+
+	if (s)
+	{
+		ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+		int32		val;
+
+		val = pg_strtoint32_safe(s, (Node *) &escontext);
+
+		if (escontext.error_occurred)
+		{
+			escontext.error_data->elevel = WARNING;
+			ThrowErrorData(escontext.error_data);
+			FreeErrorData(escontext.error_data);
+		}
+		else
+		{
+			arg->value = Int32GetDatum(val);
+			arg->isnull = false;
+		}
+	}
+}
+
+/*
+ * Convenience routine for setting optional float[] arguments
+ */
+static void
+set_floatarr_arg(NullableDatum *arg, const char *s)
+{
+	arg->value = (Datum) 0;
+	arg->isnull = true;
+
+	if (s)
+	{
+		ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+		FmgrInfo	flinfo;
+		Datum		val;
+
+		fmgr_info(F_ARRAY_IN, &flinfo);
+
+		if (!InputFunctionCallSafe(&flinfo, (char *) s, FLOAT4OID, -1,
+								   (Node *) &escontext, &val))
+		{
+			escontext.error_data->elevel = WARNING;
+			ThrowErrorData(escontext.error_data);
+			FreeErrorData(escontext.error_data);
+		}
+		else
+		{
+			arg->value = val;
+			arg->isnull = false;
+		}
+	}
+}
+
+/*
+ * Delete attribute statistics.
+ */
+bool
+delete_attribute_statistics(Relation rel, AttrNumber attnum, bool inherited)
+{
+	return delete_pg_statistic(RelationGetRelid(rel), attnum, inherited);
+}
+
+/*
+ * Import attribute statistics from regular string inputs for all statitical
+ * values.
+ *
+ * To convey a "null"/not-set value for attnum, use InvalidAttrNumber.
+ */
+bool
+import_attribute_statistics(Relation rel, const char *attname,
+							AttrNumber attnum, bool inherited,
+							const char *null_frac, const char *avg_width,
+							const char *n_distinct,
+							const char *most_common_vals,
+							const char *most_common_freqs,
+							const char *histogram_bounds,
+							const char *correlation,
+							const char *most_common_elems,
+							const char *most_common_elem_freqs,
+							const char *elem_count_histogram,
+							const char *range_length_histogram,
+							const char *range_empty_frac,
+							const char *range_bounds_histogram)
+{
+	LOCAL_FCINFO(newfcinfo, NUM_ATTRIBUTE_STATS_ARGS);
+
+	InitFunctionCallInfoData(*newfcinfo, NULL, NUM_ATTRIBUTE_STATS_ARGS, InvalidOid, NULL, NULL);
+
+	/*
+	 * Convert all string inputs to their required datatypes. NULL values are
+	 * left as the default.
+	 */
+	newfcinfo->args[ATTRELSCHEMA_ARG].value = CStringGetTextDatum(get_namespace_name(RelationGetNamespace(rel)));
+	newfcinfo->args[ATTRELSCHEMA_ARG].isnull = false;
+	newfcinfo->args[ATTRELNAME_ARG].value = CStringGetTextDatum(RelationGetRelationName(rel));
+	newfcinfo->args[ATTRELNAME_ARG].isnull = false;
+
+	set_text_arg(&newfcinfo->args[ATTNAME_ARG], attname);
+
+	if (attnum != InvalidAttrNumber)
+	{
+		newfcinfo->args[ATTNUM_ARG].value = Int16GetDatum(attnum);
+		newfcinfo->args[ATTNUM_ARG].isnull = false;
+	}
+	else
+	{
+		newfcinfo->args[ATTNUM_ARG].value = (Datum) 0;
+		newfcinfo->args[ATTNUM_ARG].isnull = true;
+	}
+
+	newfcinfo->args[INHERITED_ARG].value = BoolGetDatum(inherited);
+	newfcinfo->args[INHERITED_ARG].isnull = false;
+
+	set_float_arg(&newfcinfo->args[NULL_FRAC_ARG], null_frac);
+	set_int32_arg(&newfcinfo->args[AVG_WIDTH_ARG], avg_width);
+	set_float_arg(&newfcinfo->args[N_DISTINCT_ARG], n_distinct);
+	set_text_arg(&newfcinfo->args[MOST_COMMON_VALS_ARG], most_common_vals);
+	set_floatarr_arg(&newfcinfo->args[MOST_COMMON_FREQS_ARG], most_common_freqs);
+	set_text_arg(&newfcinfo->args[HISTOGRAM_BOUNDS_ARG], histogram_bounds);
+	set_float_arg(&newfcinfo->args[CORRELATION_ARG], correlation);
+	set_text_arg(&newfcinfo->args[MOST_COMMON_ELEMS_ARG], most_common_elems);
+	set_floatarr_arg(&newfcinfo->args[MOST_COMMON_ELEM_FREQS_ARG], most_common_elem_freqs);
+	set_floatarr_arg(&newfcinfo->args[ELEM_COUNT_HISTOGRAM_ARG], elem_count_histogram);
+	set_text_arg(&newfcinfo->args[RANGE_LENGTH_HISTOGRAM_ARG], range_length_histogram);
+	set_float_arg(&newfcinfo->args[RANGE_EMPTY_FRAC_ARG], range_empty_frac);
+	set_text_arg(&newfcinfo->args[RANGE_BOUNDS_HISTOGRAM_ARG], range_bounds_histogram);
+
+	return attribute_statistics_update(newfcinfo);
+}
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index e90289e4ab1..e931fa64e0d 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -12927,6 +12927,47 @@ 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)
+
+-- 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 673d56117b8..8aa589d5a56 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,7 @@
 #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"
@@ -368,109 +368,6 @@ enum AttStatsColumns
 	ATTSTATS_NUM_FIELDS,
 };
 
-/* 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,
-};
-
 /*
  * SQL functions
  */
@@ -688,15 +585,12 @@ static bool match_attrmap(PGresult *res,
 						  const char *remote_relname,
 						  int attrcnt,
 						  RemoteAttributeMapping *remattrmap);
-static bool import_fetched_statistics(Relation rel,
+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);
@@ -6099,6 +5993,19 @@ match_attrmap(PGresult *res,
 	return true;
 }
 
+
+/*
+ * Conenience routine to fetch
+ */
+static char *
+get_opt_value(PGresult *res, int row, int col)
+{
+	if (PQgetisnull(res, row, col))
+		return NULL;
+
+	return PQgetvalue(res, row, col);
+}
+
 /*
  * Import fetched statistics into the local statistics tables.
  */
@@ -6110,28 +6017,11 @@ import_fetched_statistics(Relation relation,
 						  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		ok = false;
 
 	char	   *relpages = NULL;
 	char	   *reltuples = NULL;
 
-	/* 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
@@ -6139,26 +6029,16 @@ import_fetched_statistics(Relation relation,
 	 */
 	if (remstats->att != NULL)
 	{
-		Assert(PQnfields(remstats->att) == ATTSTATS_NUM_FIELDS);
-		Assert(PQntuples(remstats->att) >= 1);
+		PGresult   *res = remstats->att;
 
-		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] = ' ';
+		Assert(PQnfields(res) == ATTSTATS_NUM_FIELDS);
+		Assert(PQntuples(res) >= 1);
 
 		for (int mapidx = 0; mapidx < attrcnt; mapidx++)
 		{
 			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);
@@ -6168,42 +6048,28 @@ import_fetched_statistics(Relation relation,
 			 */
 			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);
+			delete_attribute_statistics(relation, attnum, false);
 
-			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())
+			if (!import_attribute_statistics(relation, attname,
+											 InvalidAttrNumber,
+											 false,
+											 get_opt_value(res, row, ATTSTATS_NULL_FRAC),
+											 get_opt_value(res, row, ATTSTATS_AVG_WIDTH),
+											 get_opt_value(res, row, ATTSTATS_N_DISTINCT),
+											 get_opt_value(res, row, ATTSTATS_MOST_COMMON_VALS),
+											 get_opt_value(res, row, ATTSTATS_MOST_COMMON_FREQS),
+											 get_opt_value(res, row, ATTSTATS_HISTOGRAM_BOUNDS),
+											 get_opt_value(res, row, ATTSTATS_CORRELATION),
+											 get_opt_value(res, row, ATTSTATS_MOST_COMMON_ELEMS),
+											 get_opt_value(res, row, ATTSTATS_MOST_COMMON_ELEM_FREQS),
+											 get_opt_value(res, row, ATTSTATS_ELEM_COUNT_HISTOGRAM),
+											 get_opt_value(res, row, ATTSTATS_RANGE_LENGTH_HISTOGRAM),
+											 get_opt_value(res, row, ATTSTATS_RANGE_EMPTY_FRAC),
+											 get_opt_value(res, row, ATTSTATS_RANGE_BOUNDS_HISTOGRAM)))
 			{
 				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;
 			}
 		}
@@ -6232,57 +6098,9 @@ import_fetched_statistics(Relation relation,
 	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 dfc58beb0d2..627177123b3 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -4584,6 +4584,38 @@ 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;
+
+-- 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.54.0



  [text/x-patch] v2-0003-New-function-stats_acquire_relation_lock.patch (2.9K, ../../CADkLM=dWJaw04u9FU=WkykOy7bu7B5eKmoDkfVz6JXh7mdemkw@mail.gmail.com/5-v2-0003-New-function-stats_acquire_relation_lock.patch)
  download | inline diff:
From 3f857611b69c95f325aba5ed8049e376326db8fd Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email protected]>
Date: Mon, 22 Jun 2026 13:43:37 -0400
Subject: [PATCH v2 3/7] New function: stats_acquire_relation_lock().

Introduce a new funciton, stats_acquire_relation_lock() which will
perform the RangeVarGetRelidExtended() call used to verify the correct
reloid of the relation, verify that the caller has the correct
permissions for the operation, and acquiring the lock.

Additionally, it will check RecoveryInProgress() before acquiring a lock
because we can't acquire locks during recovery, let alone set
statistics.

This function will be used by all of the callers of
relation_statistics_update() and attribute_statistics_update().
---
 src/include/statistics/stat_utils.h |  2 ++
 src/backend/statistics/stat_utils.c | 26 ++++++++++++++++++++++++++
 2 files changed, 28 insertions(+)

diff --git a/src/include/statistics/stat_utils.h b/src/include/statistics/stat_utils.h
index 74da7790579..bcdfa616ce6 100644
--- a/src/include/statistics/stat_utils.h
+++ b/src/include/statistics/stat_utils.h
@@ -37,6 +37,8 @@ extern bool stats_check_arg_pair(FunctionCallInfo fcinfo,
 extern void RangeVarCallbackForStats(const RangeVar *relation,
 									 Oid relId, Oid oldRelId, void *arg);
 
+extern Oid stats_acquire_relation_lock(const char *nspname, const char *relname);
+
 extern bool stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 											 FunctionCallInfo positional_fcinfo,
 											 struct StatsArgInfo *arginfo);
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index a673e3c704b..75792ff966a 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -26,6 +26,7 @@
 #include "catalog/pg_statistic.h"
 #include "funcapi.h"
 #include "miscadmin.h"
+#include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
 #include "statistics/stat_utils.h"
 #include "storage/lmgr.h"
@@ -753,3 +754,28 @@ statatt_init_empty_tuple(Oid reloid, int16 attnum, bool inherited,
 		nulls[Anum_pg_statistic_stacoll1 + slotnum - 1] = false;
 	}
 }
+
+/*
+ * Convenience routine to encapsulate the RangeVarGetRelidExtended()
+ * that all external callers must make before calling
+ * relation_statistics_update() or attribute_statistics_update().
+ *
+ * Check for recovery in progress before acquiring any locks.
+ */
+Oid
+stats_acquire_relation_lock(const char *nspname, const char *relname)
+{
+	Oid		reloid;
+	Oid		locked_table = InvalidOid;
+
+	if (RecoveryInProgress())
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("recovery is in progress"),
+				 errhint("Statistics cannot be modified during recovery.")));
+
+	reloid = RangeVarGetRelidExtended(makeRangeVar((char *) nspname, (char *) relname, -1),
+									  ShareUpdateExclusiveLock, 0,
+									  RangeVarCallbackForStats, &locked_table);
+	return reloid;
+}
-- 
2.54.0



  [text/x-patch] v2-0005-Rename-import_relation_statistics-to-relation_sta.patch (11.7K, ../../CADkLM=dWJaw04u9FU=WkykOy7bu7B5eKmoDkfVz6JXh7mdemkw@mail.gmail.com/6-v2-0005-Rename-import_relation_statistics-to-relation_sta.patch)
  download | inline diff:
From 6cda28a063bdb854fbfb42dcdce2650730bdeea2 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email protected]>
Date: Mon, 22 Jun 2026 14:05:32 -0400
Subject: [PATCH v2 5/7] Rename import_relation_statistics() to
 relation_statistics_update().

Rename existing relation_statistics_update() to update_relstats() and
change the call signature to have the relation oid. All callers now
independently resovle the reloid via stats_acquire_relation_lock().
---
 src/include/statistics/relation_stats.h |   2 +-
 src/backend/statistics/relation_stats.c | 194 +++++++++++++-----------
 contrib/postgres_fdw/postgres_fdw.c     |   2 +-
 3 files changed, 105 insertions(+), 93 deletions(-)

diff --git a/src/include/statistics/relation_stats.h b/src/include/statistics/relation_stats.h
index f68a1a755a1..423ff8961f2 100644
--- a/src/include/statistics/relation_stats.h
+++ b/src/include/statistics/relation_stats.h
@@ -14,7 +14,7 @@
 
 #include "access/genam.h"
 
-bool import_relation_statistics(Relation rel, const char *relpages,
+bool relation_statistics_update(Relation rel, const char *relpages,
 								const char *reltuples, const char *relallvisible,
 								const char *relallfrozen);
 
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 9c308ee50e5..2e07df78d8b 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -32,8 +32,8 @@
 
 
 /*
- * Positional argument numbers, names, and types for
- * relation_statistics_update().
+ * Positional argument numbers, names, and types for positional_fcinfo
+ * used by update_relstats().
  */
 
 enum relation_stats_argnum
@@ -58,18 +58,15 @@ static struct StatsArgInfo relarginfo[] =
 	[NUM_RELATION_STATS_ARGS] = {0}
 };
 
-static bool relation_statistics_update(FunctionCallInfo fcinfo);
+static bool update_relstats(Oid reloid, FunctionCallInfo fcinfo);
 
 /*
  * Internal function for modifying statistics for a relation.
  */
 static bool
-relation_statistics_update(FunctionCallInfo fcinfo)
+update_relstats(Oid reloid, FunctionCallInfo fcinfo)
 {
 	bool		result = true;
-	char	   *nspname;
-	char	   *relname;
-	Oid			reloid;
 	Relation	crel;
 	BlockNumber relpages = 0;
 	bool		update_relpages = false;
@@ -85,23 +82,6 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 	Datum		values[4] = {0};
 	bool		nulls[4] = {0};
 	int			nreplaces = 0;
-	Oid			locked_table = InvalidOid;
-
-	stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG);
-	stats_check_required_arg(fcinfo, relarginfo, RELNAME_ARG);
-
-	nspname = TextDatumGetCString(PG_GETARG_DATUM(RELSCHEMA_ARG));
-	relname = TextDatumGetCString(PG_GETARG_DATUM(RELNAME_ARG));
-
-	if (RecoveryInProgress())
-		ereport(ERROR,
-				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
-				 errmsg("recovery is in progress"),
-				 errhint("Statistics cannot be modified during recovery.")));
-
-	reloid = RangeVarGetRelidExtended(makeRangeVar(nspname, relname, -1),
-									  ShareUpdateExclusiveLock, 0,
-									  RangeVarCallbackForStats, &locked_table);
 
 	if (!PG_ARGISNULL(RELPAGES_ARG))
 	{
@@ -203,24 +183,35 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 Datum
 pg_clear_relation_stats(PG_FUNCTION_ARGS)
 {
-	LOCAL_FCINFO(newfcinfo, 6);
+	LOCAL_FCINFO(positional_fcinfo, NUM_RELATION_STATS_ARGS);
+	Oid			reloid;
+	char	   *nspname;
+	char	   *relname;
 
-	InitFunctionCallInfoData(*newfcinfo, NULL, 6, InvalidOid, NULL, NULL);
+	InitFunctionCallInfoData(*positional_fcinfo, NULL, 6, InvalidOid, NULL, NULL);
 
-	newfcinfo->args[0].value = PG_GETARG_DATUM(0);
-	newfcinfo->args[0].isnull = PG_ARGISNULL(0);
-	newfcinfo->args[1].value = PG_GETARG_DATUM(1);
-	newfcinfo->args[1].isnull = PG_ARGISNULL(1);
-	newfcinfo->args[2].value = UInt32GetDatum(0);
-	newfcinfo->args[2].isnull = false;
-	newfcinfo->args[3].value = Float4GetDatum(-1.0);
-	newfcinfo->args[3].isnull = false;
-	newfcinfo->args[4].value = UInt32GetDatum(0);
-	newfcinfo->args[4].isnull = false;
-	newfcinfo->args[5].value = UInt32GetDatum(0);
-	newfcinfo->args[5].isnull = false;
+	positional_fcinfo->args[RELSCHEMA_ARG].value = PG_GETARG_DATUM(0);
+	positional_fcinfo->args[RELSCHEMA_ARG].isnull = PG_ARGISNULL(0);
+	positional_fcinfo->args[RELNAME_ARG].value = PG_GETARG_DATUM(1);
+	positional_fcinfo->args[RELNAME_ARG].isnull = PG_ARGISNULL(1);
+	positional_fcinfo->args[RELPAGES_ARG].value = UInt32GetDatum(0);
+	positional_fcinfo->args[RELPAGES_ARG].isnull = false;
+	positional_fcinfo->args[RELTUPLES_ARG].value = Float4GetDatum(-1.0);
+	positional_fcinfo->args[RELTUPLES_ARG].isnull = false;
+	positional_fcinfo->args[RELALLVISIBLE_ARG].value = UInt32GetDatum(0);
+	positional_fcinfo->args[RELALLVISIBLE_ARG].isnull = false;
+	positional_fcinfo->args[RELALLFROZEN_ARG].value = UInt32GetDatum(0);
+	positional_fcinfo->args[RELALLFROZEN_ARG].isnull = false;
 
-	relation_statistics_update(newfcinfo);
+	stats_check_required_arg(positional_fcinfo, relarginfo, RELSCHEMA_ARG);
+	stats_check_required_arg(positional_fcinfo, relarginfo, RELNAME_ARG);
+
+	nspname = TextDatumGetCString(positional_fcinfo->args[RELSCHEMA_ARG].value);
+	relname = TextDatumGetCString(positional_fcinfo->args[RELNAME_ARG].value);
+
+	reloid = stats_acquire_relation_lock(nspname, relname);
+
+	update_relstats(reloid, positional_fcinfo);
 	PG_RETURN_VOID();
 }
 
@@ -229,6 +220,9 @@ pg_restore_relation_stats(PG_FUNCTION_ARGS)
 {
 	LOCAL_FCINFO(positional_fcinfo, NUM_RELATION_STATS_ARGS);
 	bool		result = true;
+	Oid			reloid;
+	char	   *nspname;
+	char	   *relname;
 
 	InitFunctionCallInfoData(*positional_fcinfo, NULL,
 							 NUM_RELATION_STATS_ARGS,
@@ -238,7 +232,15 @@ pg_restore_relation_stats(PG_FUNCTION_ARGS)
 										  relarginfo))
 		result = false;
 
-	if (!relation_statistics_update(positional_fcinfo))
+	stats_check_required_arg(positional_fcinfo, relarginfo, RELSCHEMA_ARG);
+	stats_check_required_arg(positional_fcinfo, relarginfo, RELNAME_ARG);
+
+	nspname = TextDatumGetCString(positional_fcinfo->args[RELSCHEMA_ARG].value);
+	relname = TextDatumGetCString(positional_fcinfo->args[RELNAME_ARG].value);
+
+	reloid = stats_acquire_relation_lock(nspname, relname);
+
+	if (!update_relstats(reloid, positional_fcinfo))
 		result = false;
 
 	PG_RETURN_BOOL(result);
@@ -248,92 +250,102 @@ pg_restore_relation_stats(PG_FUNCTION_ARGS)
  * Convenience routine to parse BlockNumber values, and emit a warning
  * on parse errors.
  *
- * Returns 0 if the value is NULL or invalid.
+ * Returns Datum of 0 if the value is NULL or invalid.
  */
-static BlockNumber
+static Datum
 str_to_blocknumber(const char *s)
 {
-	const BlockNumber default_value = 0;
+	BlockNumber result = 0;
 
-	BlockNumber result;
-	ErrorSaveContext escontext = {T_ErrorSaveContext};
-
-	if (!s)
-		return default_value;
-
-	result = uint32in_subr(s, NULL, "BlockNumber", (Node *) &escontext);
-
-	if (escontext.error_occurred)
+	if (s)
 	{
-		escontext.error_data->elevel = WARNING;
-		ThrowErrorData(escontext.error_data);
-		FreeErrorData(escontext.error_data);
+		BlockNumber scratch_result;
+		ErrorSaveContext escontext = {T_ErrorSaveContext};
 
-		return default_value;
+		scratch_result = uint32in_subr(s, NULL, "BlockNumber", (Node *) &escontext);
+
+		if (escontext.error_occurred)
+		{
+			escontext.error_data->elevel = WARNING;
+			ThrowErrorData(escontext.error_data);
+			FreeErrorData(escontext.error_data);
+		}
+		else
+			result = scratch_result;
 	}
 
-	return result;
+	return UInt32GetDatum(result);
 }
 
 /*
  * Convenience routine to parse float values, and emit a warning on parse
  * errors.
  *
- * Returns -1.0 if the value is NULL or invalid.
+ * Returns Datum of -1.0 if the value is NULL or invalid.
  */
-static float
+static Datum
 str_to_float(const char *s)
 {
-	const float default_value = -1.0;
+	float result = -1.0;
 
-	float		result;
-
-	ErrorSaveContext escontext = {T_ErrorSaveContext};
-
-	if (!s)
-		return default_value;
-
-	result = float4in_internal((char *) s, NULL, "float", s, (Node *) &escontext);
-
-	if (escontext.error_occurred)
+	if (s)
 	{
-		escontext.error_data->elevel = WARNING;
-		ThrowErrorData(escontext.error_data);
-		FreeErrorData(escontext.error_data);
-		return default_value;
+		ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+		float	scratch_result;
+
+		scratch_result = float4in_internal((char *) s, NULL, "float", s, (Node *) &escontext);
+
+		if (escontext.error_occurred)
+		{
+			escontext.error_data->elevel = WARNING;
+			ThrowErrorData(escontext.error_data);
+			FreeErrorData(escontext.error_data);
+		}
+		else
+			result = scratch_result;
 	}
 
-	return result;
+	return Float4GetDatum(result);
 }
 
 /*
  * Import relation statistics from regular string inputs.
  */
 bool
-import_relation_statistics(Relation rel, const char *relpages,
+relation_statistics_update(Relation rel, const char *relpages,
 						   const char *reltuples, const char *relallvisible,
 						   const char *relallfrozen)
 {
-	LOCAL_FCINFO(newfcinfo, NUM_RELATION_STATS_ARGS);
+	LOCAL_FCINFO(positional_fcinfo, NUM_RELATION_STATS_ARGS);
+	Oid			reloid;
+	char	   *nspname;
+	char	   *relname;
 
-	InitFunctionCallInfoData(*newfcinfo, NULL, NUM_RELATION_STATS_ARGS, InvalidOid, NULL, NULL);
+	InitFunctionCallInfoData(*positional_fcinfo, NULL, NUM_RELATION_STATS_ARGS,
+							 InvalidOid, NULL, NULL);
+
+	nspname = get_namespace_name(RelationGetNamespace(rel));
+	relname = RelationGetRelationName(rel);
+
+	reloid = stats_acquire_relation_lock(nspname, relname);
 
 	/*
 	 * Convert all string inputs to their required datatypes. NULL values are
 	 * left as the default.
 	 */
-	newfcinfo->args[RELSCHEMA_ARG].value = CStringGetTextDatum(get_namespace_name(RelationGetNamespace(rel)));
-	newfcinfo->args[RELSCHEMA_ARG].isnull = false;
-	newfcinfo->args[RELNAME_ARG].value = CStringGetTextDatum(RelationGetRelationName(rel));
-	newfcinfo->args[RELNAME_ARG].isnull = false;
-	newfcinfo->args[RELPAGES_ARG].value = UInt32GetDatum(str_to_blocknumber(relpages));
-	newfcinfo->args[RELPAGES_ARG].isnull = false;
-	newfcinfo->args[RELTUPLES_ARG].value = Float4GetDatum(str_to_float(reltuples));
-	newfcinfo->args[RELTUPLES_ARG].isnull = false;
-	newfcinfo->args[RELALLVISIBLE_ARG].value = UInt32GetDatum(str_to_blocknumber(relallvisible));
-	newfcinfo->args[RELALLVISIBLE_ARG].isnull = false;
-	newfcinfo->args[RELALLFROZEN_ARG].value = UInt32GetDatum(str_to_blocknumber(relallfrozen));
-	newfcinfo->args[RELALLFROZEN_ARG].isnull = false;
+	positional_fcinfo->args[RELSCHEMA_ARG].value = CStringGetTextDatum(nspname);
+	positional_fcinfo->args[RELSCHEMA_ARG].isnull = false;
+	positional_fcinfo->args[RELNAME_ARG].value = CStringGetTextDatum(relname);
+	positional_fcinfo->args[RELNAME_ARG].isnull = false;
+	positional_fcinfo->args[RELPAGES_ARG].value = str_to_blocknumber(relpages);
+	positional_fcinfo->args[RELPAGES_ARG].isnull = false;
+	positional_fcinfo->args[RELTUPLES_ARG].value = str_to_float(reltuples);
+	positional_fcinfo->args[RELTUPLES_ARG].isnull = false;
+	positional_fcinfo->args[RELALLVISIBLE_ARG].value = str_to_blocknumber(relallvisible);
+	positional_fcinfo->args[RELALLVISIBLE_ARG].isnull = false;
+	positional_fcinfo->args[RELALLFROZEN_ARG].value = str_to_blocknumber(relallfrozen);
+	positional_fcinfo->args[RELALLFROZEN_ARG].isnull = false;
 
-	return relation_statistics_update(newfcinfo);
+	return update_relstats(reloid, positional_fcinfo);
 }
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 8aa589d5a56..1d203f5fcf5 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -6086,7 +6086,7 @@ import_fetched_statistics(Relation relation,
 	if (!PQgetisnull(remstats->rel, 0, RELSTATS_RELTUPLES))
 		reltuples = PQgetvalue(remstats->rel, 0, RELSTATS_RELTUPLES);
 
-	if (!import_relation_statistics(relation, relpages, reltuples,
+	if (!relation_statistics_update(relation, relpages, reltuples,
 									NULL, NULL))
 	{
 		ereport(WARNING,
-- 
2.54.0



  [text/x-patch] v2-0004-postgres_fdw-additional-regression-reverse-set-di.patch (2.2K, ../../CADkLM=dWJaw04u9FU=WkykOy7bu7B5eKmoDkfVz6JXh7mdemkw@mail.gmail.com/7-v2-0004-postgres_fdw-additional-regression-reverse-set-di.patch)
  download | inline diff:
From 3eec089f361d42b24c8e9c2c3bfc92f9006b8e15 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email protected]>
Date: Mon, 22 Jun 2026 13:48:44 -0400
Subject: [PATCH v2 4/7] postgres_fdw: additional regression reverse
 set-difference test

Add an addtional test to postgres_fdw.sql which selects the reverse
set-difference operation, allowing us to see the values in the row that
should have been set to certain values but somehow wasn't. The current
test would only show the values of the source table but not the value
found in the destination table.
---
 contrib/postgres_fdw/expected/postgres_fdw.out | 11 +++++++++++
 contrib/postgres_fdw/sql/postgres_fdw.sql      |  8 ++++++++
 2 files changed, 19 insertions(+)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index e931fa64e0d..b46f38a04fe 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -12950,6 +12950,17 @@ WHERE oid = 'public.dtest_ftable'::regclass;
 ----------+-----------
 (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,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 627177123b3..5a4f05c89e5 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -4601,6 +4601,14 @@ 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,
-- 
2.54.0



  [text/x-patch] v2-0006-Rename-attribute_statistics-functions.patch (4.6K, ../../CADkLM=dWJaw04u9FU=WkykOy7bu7B5eKmoDkfVz6JXh7mdemkw@mail.gmail.com/8-v2-0006-Rename-attribute_statistics-functions.patch)
  download | inline diff:
From c95cc2d44e98847695aba94ea04a44906ebf5d51 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email protected]>
Date: Mon, 22 Jun 2026 14:16:31 -0400
Subject: [PATCH v2 6/7] Rename attribute_statistics functions.

The static attribute_statistics_update() is now update_attstats(). The
function import_attribute_stats() is now attribute_statisticss_update()
and delete_attribute_statistics() is now attribute_statistics_delete().

Changes to function signatures will happen in a later patch.
---
 src/include/statistics/attribute_stats.h |  4 ++--
 src/backend/statistics/attribute_stats.c | 14 +++++++-------
 contrib/postgres_fdw/postgres_fdw.c      |  4 ++--
 3 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/src/include/statistics/attribute_stats.h b/src/include/statistics/attribute_stats.h
index 4ebde0c7c3b..8a4ee5112e4 100644
--- a/src/include/statistics/attribute_stats.h
+++ b/src/include/statistics/attribute_stats.h
@@ -15,9 +15,9 @@
 #include "access/genam.h"
 
 
-bool delete_attribute_statistics(Relation rel, AttrNumber attnum, bool inherited);
+bool attribute_statistics_delete(Relation rel, AttrNumber attnum, bool inherited);
 
-bool import_attribute_statistics(Relation rel, const char *attname,
+bool attribute_statistics_update(Relation rel, const char *attname,
 								 AttrNumber attnum, bool inherited,
 								 const char *null_frac, const char *avg_width,
 								 const char *n_distinct,
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index f67d62c0104..7dccce83c65 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -34,7 +34,7 @@
 
 /*
  * Positional argument numbers, names, and types for
- * attribute_statistics_update() and pg_restore_attribute_stats().
+ * update_attstats() and pg_restore_attribute_stats().
  */
 
 enum attribute_stats_argnum
@@ -106,7 +106,7 @@ static struct StatsArgInfo cleararginfo[] =
 	[C_NUM_ATTRIBUTE_STATS_ARGS] = {0}
 };
 
-static bool attribute_statistics_update(FunctionCallInfo fcinfo);
+static bool update_attstats(FunctionCallInfo fcinfo);
 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);
@@ -128,7 +128,7 @@ static bool delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit);
  * and other statistic kinds may still be updated.
  */
 static bool
-attribute_statistics_update(FunctionCallInfo fcinfo)
+update_attstats(FunctionCallInfo fcinfo)
 {
 	char	   *nspname;
 	char	   *relname;
@@ -685,7 +685,7 @@ pg_restore_attribute_stats(PG_FUNCTION_ARGS)
 										  attarginfo))
 		result = false;
 
-	if (!attribute_statistics_update(positional_fcinfo))
+	if (!update_attstats(positional_fcinfo))
 		result = false;
 
 	PG_RETURN_BOOL(result);
@@ -808,7 +808,7 @@ set_floatarr_arg(NullableDatum *arg, const char *s)
  * Delete attribute statistics.
  */
 bool
-delete_attribute_statistics(Relation rel, AttrNumber attnum, bool inherited)
+attribute_statistics_delete(Relation rel, AttrNumber attnum, bool inherited)
 {
 	return delete_pg_statistic(RelationGetRelid(rel), attnum, inherited);
 }
@@ -820,7 +820,7 @@ delete_attribute_statistics(Relation rel, AttrNumber attnum, bool inherited)
  * To convey a "null"/not-set value for attnum, use InvalidAttrNumber.
  */
 bool
-import_attribute_statistics(Relation rel, const char *attname,
+attribute_statistics_update(Relation rel, const char *attname,
 							AttrNumber attnum, bool inherited,
 							const char *null_frac, const char *avg_width,
 							const char *n_distinct,
@@ -878,5 +878,5 @@ import_attribute_statistics(Relation rel, const char *attname,
 	set_float_arg(&newfcinfo->args[RANGE_EMPTY_FRAC_ARG], range_empty_frac);
 	set_text_arg(&newfcinfo->args[RANGE_BOUNDS_HISTOGRAM_ARG], range_bounds_histogram);
 
-	return attribute_statistics_update(newfcinfo);
+	return update_attstats(newfcinfo);
 }
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 1d203f5fcf5..9680af902ca 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -6048,9 +6048,9 @@ import_fetched_statistics(Relation relation,
 			 */
 			CHECK_FOR_INTERRUPTS();
 
-			delete_attribute_statistics(relation, attnum, false);
+			attribute_statistics_delete(relation, attnum, false);
 
-			if (!import_attribute_statistics(relation, attname,
+			if (!attribute_statistics_update(relation, attname,
 											 InvalidAttrNumber,
 											 false,
 											 get_opt_value(res, row, ATTSTATS_NULL_FRAC),
-- 
2.54.0



  [text/x-patch] v2-0007-Make-callers-of-update_attstats-use-stats_acquire.patch (9.0K, ../../CADkLM=dWJaw04u9FU=WkykOy7bu7B5eKmoDkfVz6JXh7mdemkw@mail.gmail.com/9-v2-0007-Make-callers-of-update_attstats-use-stats_acquire.patch)
  download | inline diff:
From 79564c6211d7dd8f80062d9acbcea20969698803 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email protected]>
Date: Mon, 22 Jun 2026 14:48:12 -0400
Subject: [PATCH v2 7/7] Make callers of update_attstats() use
 stats_acquire_relation_lock().

This is the same change that was made in relation_stats.c, but with the
function renaming already done beforehand.
---
 src/backend/statistics/attribute_stats.c | 109 +++++++++++------------
 1 file changed, 51 insertions(+), 58 deletions(-)

diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 7dccce83c65..f903d2c0003 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -33,8 +33,7 @@
 #include "utils/syscache.h"
 
 /*
- * Positional argument numbers, names, and types for
- * update_attstats() and pg_restore_attribute_stats().
+ * Positional argument numbers, names, and types for update_attstats()
  */
 
 enum attribute_stats_argnum
@@ -106,7 +105,7 @@ static struct StatsArgInfo cleararginfo[] =
 	[C_NUM_ATTRIBUTE_STATS_ARGS] = {0}
 };
 
-static bool update_attstats(FunctionCallInfo fcinfo);
+static bool update_attstats(Oid reloid, FunctionCallInfo fcinfo);
 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);
@@ -128,15 +127,12 @@ static bool delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit);
  * and other statistic kinds may still be updated.
  */
 static bool
-update_attstats(FunctionCallInfo fcinfo)
+update_attstats(Oid reloid, FunctionCallInfo fcinfo)
 {
-	char	   *nspname;
 	char	   *relname;
-	Oid			reloid;
 	char	   *attname;
 	AttrNumber	attnum;
 	bool		inherited;
-	Oid			locked_table = InvalidOid;
 
 	Relation	starel;
 	HeapTuple	statup;
@@ -173,20 +169,8 @@ update_attstats(FunctionCallInfo fcinfo)
 	stats_check_required_arg(fcinfo, attarginfo, ATTRELSCHEMA_ARG);
 	stats_check_required_arg(fcinfo, attarginfo, ATTRELNAME_ARG);
 
-	nspname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELSCHEMA_ARG));
 	relname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELNAME_ARG));
 
-	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 (!PG_ARGISNULL(ATTNAME_ARG))
 	{
@@ -605,7 +589,6 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 	char	   *attname;
 	AttrNumber	attnum;
 	bool		inherited;
-	Oid			locked_table = InvalidOid;
 
 	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELSCHEMA_ARG);
 	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELNAME_ARG);
@@ -614,16 +597,7 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 
 	nspname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELSCHEMA_ARG));
 	relname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELNAME_ARG));
-
-	if (RecoveryInProgress())
-		ereport(ERROR,
-				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
-				 errmsg("recovery is in progress"),
-				 errhint("Statistics cannot be modified during recovery.")));
-
-	reloid = RangeVarGetRelidExtended(makeRangeVar(nspname, relname, -1),
-									  ShareUpdateExclusiveLock, 0,
-									  RangeVarCallbackForStats, &locked_table);
+	reloid = stats_acquire_relation_lock(nspname, relname);
 
 	attname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTNAME_ARG));
 	attnum = get_attnum(reloid, attname);
@@ -677,6 +651,9 @@ pg_restore_attribute_stats(PG_FUNCTION_ARGS)
 {
 	LOCAL_FCINFO(positional_fcinfo, NUM_ATTRIBUTE_STATS_ARGS);
 	bool		result = true;
+	Oid			reloid;
+	char	   *nspname;
+	char	   *relname;
 
 	InitFunctionCallInfoData(*positional_fcinfo, NULL, NUM_ATTRIBUTE_STATS_ARGS,
 							 InvalidOid, NULL, NULL);
@@ -685,7 +662,15 @@ pg_restore_attribute_stats(PG_FUNCTION_ARGS)
 										  attarginfo))
 		result = false;
 
-	if (!update_attstats(positional_fcinfo))
+	stats_check_required_arg(positional_fcinfo, attarginfo, ATTRELSCHEMA_ARG);
+	stats_check_required_arg(positional_fcinfo, attarginfo, ATTRELNAME_ARG);
+
+	nspname = TextDatumGetCString(positional_fcinfo->args[ATTRELSCHEMA_ARG].value);
+	relname = TextDatumGetCString(positional_fcinfo->args[ATTRELNAME_ARG].value);
+
+	reloid = stats_acquire_relation_lock(nspname, relname);
+
+	if (!update_attstats(reloid, positional_fcinfo))
 		result = false;
 
 	PG_RETURN_BOOL(result);
@@ -835,48 +820,56 @@ attribute_statistics_update(Relation rel, const char *attname,
 							const char *range_empty_frac,
 							const char *range_bounds_histogram)
 {
-	LOCAL_FCINFO(newfcinfo, NUM_ATTRIBUTE_STATS_ARGS);
+	LOCAL_FCINFO(positional_fcinfo, NUM_ATTRIBUTE_STATS_ARGS);
+	Oid			reloid;
+	char	   *nspname;
+	char	   *relname;
 
-	InitFunctionCallInfoData(*newfcinfo, NULL, NUM_ATTRIBUTE_STATS_ARGS, InvalidOid, NULL, NULL);
+	InitFunctionCallInfoData(*positional_fcinfo, NULL, NUM_ATTRIBUTE_STATS_ARGS, InvalidOid, NULL, NULL);
+
+	nspname = get_namespace_name(RelationGetNamespace(rel));
+	relname = RelationGetRelationName(rel);
+
+	reloid = stats_acquire_relation_lock(nspname, relname);
 
 	/*
 	 * Convert all string inputs to their required datatypes. NULL values are
 	 * left as the default.
 	 */
-	newfcinfo->args[ATTRELSCHEMA_ARG].value = CStringGetTextDatum(get_namespace_name(RelationGetNamespace(rel)));
-	newfcinfo->args[ATTRELSCHEMA_ARG].isnull = false;
-	newfcinfo->args[ATTRELNAME_ARG].value = CStringGetTextDatum(RelationGetRelationName(rel));
-	newfcinfo->args[ATTRELNAME_ARG].isnull = false;
+	positional_fcinfo->args[ATTRELSCHEMA_ARG].value = CStringGetTextDatum(nspname);
+	positional_fcinfo->args[ATTRELSCHEMA_ARG].isnull = false;
+	positional_fcinfo->args[ATTRELNAME_ARG].value = CStringGetTextDatum(relname);
+	positional_fcinfo->args[ATTRELNAME_ARG].isnull = false;
 
-	set_text_arg(&newfcinfo->args[ATTNAME_ARG], attname);
+	set_text_arg(&positional_fcinfo->args[ATTNAME_ARG], attname);
 
 	if (attnum != InvalidAttrNumber)
 	{
-		newfcinfo->args[ATTNUM_ARG].value = Int16GetDatum(attnum);
-		newfcinfo->args[ATTNUM_ARG].isnull = false;
+		positional_fcinfo->args[ATTNUM_ARG].value = Int16GetDatum(attnum);
+		positional_fcinfo->args[ATTNUM_ARG].isnull = false;
 	}
 	else
 	{
-		newfcinfo->args[ATTNUM_ARG].value = (Datum) 0;
-		newfcinfo->args[ATTNUM_ARG].isnull = true;
+		positional_fcinfo->args[ATTNUM_ARG].value = (Datum) 0;
+		positional_fcinfo->args[ATTNUM_ARG].isnull = true;
 	}
 
-	newfcinfo->args[INHERITED_ARG].value = BoolGetDatum(inherited);
-	newfcinfo->args[INHERITED_ARG].isnull = false;
+	positional_fcinfo->args[INHERITED_ARG].value = BoolGetDatum(inherited);
+	positional_fcinfo->args[INHERITED_ARG].isnull = false;
 
-	set_float_arg(&newfcinfo->args[NULL_FRAC_ARG], null_frac);
-	set_int32_arg(&newfcinfo->args[AVG_WIDTH_ARG], avg_width);
-	set_float_arg(&newfcinfo->args[N_DISTINCT_ARG], n_distinct);
-	set_text_arg(&newfcinfo->args[MOST_COMMON_VALS_ARG], most_common_vals);
-	set_floatarr_arg(&newfcinfo->args[MOST_COMMON_FREQS_ARG], most_common_freqs);
-	set_text_arg(&newfcinfo->args[HISTOGRAM_BOUNDS_ARG], histogram_bounds);
-	set_float_arg(&newfcinfo->args[CORRELATION_ARG], correlation);
-	set_text_arg(&newfcinfo->args[MOST_COMMON_ELEMS_ARG], most_common_elems);
-	set_floatarr_arg(&newfcinfo->args[MOST_COMMON_ELEM_FREQS_ARG], most_common_elem_freqs);
-	set_floatarr_arg(&newfcinfo->args[ELEM_COUNT_HISTOGRAM_ARG], elem_count_histogram);
-	set_text_arg(&newfcinfo->args[RANGE_LENGTH_HISTOGRAM_ARG], range_length_histogram);
-	set_float_arg(&newfcinfo->args[RANGE_EMPTY_FRAC_ARG], range_empty_frac);
-	set_text_arg(&newfcinfo->args[RANGE_BOUNDS_HISTOGRAM_ARG], range_bounds_histogram);
+	set_float_arg(&positional_fcinfo->args[NULL_FRAC_ARG], null_frac);
+	set_int32_arg(&positional_fcinfo->args[AVG_WIDTH_ARG], avg_width);
+	set_float_arg(&positional_fcinfo->args[N_DISTINCT_ARG], n_distinct);
+	set_text_arg(&positional_fcinfo->args[MOST_COMMON_VALS_ARG], most_common_vals);
+	set_floatarr_arg(&positional_fcinfo->args[MOST_COMMON_FREQS_ARG], most_common_freqs);
+	set_text_arg(&positional_fcinfo->args[HISTOGRAM_BOUNDS_ARG], histogram_bounds);
+	set_float_arg(&positional_fcinfo->args[CORRELATION_ARG], correlation);
+	set_text_arg(&positional_fcinfo->args[MOST_COMMON_ELEMS_ARG], most_common_elems);
+	set_floatarr_arg(&positional_fcinfo->args[MOST_COMMON_ELEM_FREQS_ARG], most_common_elem_freqs);
+	set_floatarr_arg(&positional_fcinfo->args[ELEM_COUNT_HISTOGRAM_ARG], elem_count_histogram);
+	set_text_arg(&positional_fcinfo->args[RANGE_LENGTH_HISTOGRAM_ARG], range_length_histogram);
+	set_float_arg(&positional_fcinfo->args[RANGE_EMPTY_FRAC_ARG], range_empty_frac);
+	set_text_arg(&positional_fcinfo->args[RANGE_BOUNDS_HISTOGRAM_ARG], range_bounds_histogram);
 
-	return update_attstats(newfcinfo);
+	return update_attstats(reloid, positional_fcinfo);
 }
-- 
2.54.0



^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-17 10:23             ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-17 16:00               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-18 10:42                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-18 21:23                   ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-19 13:45                     ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-20 04:11                       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-22 19:08                         ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
@ 2026-06-23 11:43                           ` jian he <[email protected]>
  2026-06-23 15:32                             ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  1 sibling, 1 reply; 33+ messages in thread

From: jian he @ 2026-06-23 11:43 UTC (permalink / raw)
  To: Corey Huinker <[email protected]>; +Cc: Robert Haas <[email protected]>; Etsuro Fujita <[email protected]>; pgsql-hackers

On Tue, Jun 23, 2026 at 3:08 AM Corey Huinker <[email protected]> wrote:
>
> To that end, here's a new and rebased patch set:
>
> 0001 - exactly the same as before

+/*
+ * Convenience routine to parse float values, and emit a warning on parse
+ * errors.
+ *
+ * Returns -1.0 if the value is NULL or invalid.
+ */
+static float
+str_to_float(const char *s)
+{
+ const float default_value = -1.0;
+
+ float result;
+
+ ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+ if (!s)
+ return default_value;
+
+ result = float4in_internal((char *) s, NULL, "float", s, (Node *) &escontext);
+
+ if (escontext.error_occurred)
+ {
+ escontext.error_data->elevel = WARNING;
+ ThrowErrorData(escontext.error_data);
+ FreeErrorData(escontext.error_data);
+ return default_value;
+ }
+
+ return result;
+}
+

Just a quick thought: the above can be replaced by InputFunctionCallSafe?



--
jian
https://www.enterprisedb.com/





^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-17 10:23             ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-17 16:00               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-18 10:42                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-18 21:23                   ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-19 13:45                     ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-20 04:11                       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-22 19:08                         ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-23 11:43                           ` Re: use of SPI by postgresImportForeignStatistics jian he <[email protected]>
@ 2026-06-23 15:32                             ` Corey Huinker <[email protected]>
  0 siblings, 0 replies; 33+ messages in thread

From: Corey Huinker @ 2026-06-23 15:32 UTC (permalink / raw)
  To: jian he <[email protected]>; +Cc: Robert Haas <[email protected]>; Etsuro Fujita <[email protected]>; pgsql-hackers

>
>
> Just a quick thought: the above can be replaced by InputFunctionCallSafe?
>

Potentially, yes. The functions were originally returning the actual data
types rather than the Datum serializations. Which way those functions go
depends a lot on the data structure they end up populating, which is
currently in flux.


^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-17 10:23             ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-17 16:00               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-18 10:42                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-18 21:23                   ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-19 13:45                     ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-20 04:11                       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-22 19:08                         ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
@ 2026-06-23 12:10                           ` Robert Haas <[email protected]>
  2026-06-23 15:53                             ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  1 sibling, 1 reply; 33+ messages in thread

From: Robert Haas @ 2026-06-23 12:10 UTC (permalink / raw)
  To: Corey Huinker <[email protected]>; +Cc: Etsuro Fujita <[email protected]>; pgsql-hackers

On Mon, Jun 22, 2026 at 3:08 PM Corey Huinker <[email protected]> wrote:
> The first bit of dissonance comes from the SQL-level functions having schemaname+relname parameters, and the reloid is resolved via RangeVarGetRelidExtended() which has a callback to check for correct permissions and setting the proper lock level. However, the C-caller would either have the reloid already, or an already open Relation, but no assurance that the caller has the correct permissions for that table or the correct lock level on the table. So either we make an equivalent to RangeVarGetRelidExtended() that takes an oid, or the C-caller has to derive a RangeVar, call the existing RangeVarGetRelidExtended() function, and verify the result reloid against the supplied parameter. I went with deriving the RangeVar and putting an Assert on the before/after reloids, but perhaps the smarter play is to make a function that checks for ShareUpdateExclusiveLock on the Relation, and then does the equivalent of RangeVarCallbackForStats().

I don't think this is really a problem. If the caller is specifying
the OID, they should have called RangeVarGetRelidExtended themselves.
Permissions-checking, locking, and opening the relation should all
happen simultaneously, and the logic shouldn't be duplicated later.

> A smaller bit of dissonance was with RecoveryInProgress(), which if I recall we're checking before RangeVarGetRelidExtended() to avoid trying to take a lock that will fail. That check might not be meaningful if the C call takes a relation, thus ensuring that some level of locking worked, thus we aren't in recovery.

I don't quite follow this part.

> Next is the existing validation functions stats_check_required_arg(), stats_check_arg_array(), and stats_check_arg_pairs() all work on values indexed by the positional functioncallinfo and the corresponding relstatsinfo/attstatsinfo structure, and this makes a lot of type checking and value checking compact and uniform.  If we want to keep this sort of uniformity, the resulting StatsData structure will end up looking a lot like the FunctionCallInfo that we already had.

Yeah, this is worth thinking about. You could consider putting an
array inside the struct and use #defines for the indexes. And instead
of having a separate Boolean for each index, you could use the same
index to reference the N'th bit of a single integer flag variable.

> Next is the fact that the end-destination for every value passed in is a Datum for a pg_statistic heaptuple. Most Datum values are checked only for their null-ness and if they're the correct type, so the value itself is usually just passed directly from fcinfo into the heap tuple values[] array. The float[] values are checked for number of elements and whether any elements are NULL, but that is done via array functions that take a Datum input. Only in a few cases do we actually look at the actual internal value of the Datum (reltuples, attname, attnum, the anyarrays), so there's little to gain there.

Right, so the question is whether it makes more sense to pass down C
strings or Datums.

> There's some additional hassle in the fact that pg_restore_attribute_stats() can take an attnum parameter OR an attname parameter, but not both. I was able to resolve that in a semi-elegant fashion, but the other issues have convinced me that we're probably better off continuing to use the FunctionCallInfo version of attribute_stats_update(), though perhaps with a different name, allowing us to use that name for the new API call instead of import_attribute_statistics().

On that particular point, I think I'm still unconvinced, but I also
haven't looked deeply into this just yet, so maybe I'm wrong.

-- 
Robert Haas
EDB: http://www.enterprisedb.com






^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-17 10:23             ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-17 16:00               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-18 10:42                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-18 21:23                   ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-19 13:45                     ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-20 04:11                       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-22 19:08                         ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-23 12:10                           ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
@ 2026-06-23 15:53                             ` Corey Huinker <[email protected]>
  2026-06-29 08:14                               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  0 siblings, 1 reply; 33+ messages in thread

From: Corey Huinker @ 2026-06-23 15:53 UTC (permalink / raw)
  To: Robert Haas <[email protected]>; +Cc: Etsuro Fujita <[email protected]>; pgsql-hackers

On Tue, Jun 23, 2026 at 8:11 AM Robert Haas <[email protected]> wrote:

> On Mon, Jun 22, 2026 at 3:08 PM Corey Huinker <[email protected]>
> wrote:
> > The first bit of dissonance comes from the SQL-level functions having
> schemaname+relname parameters, and the reloid is resolved via
> RangeVarGetRelidExtended() which has a callback to check for correct
> permissions and setting the proper lock level. However, the C-caller would
> either have the reloid already, or an already open Relation, but no
> assurance that the caller has the correct permissions for that table or the
> correct lock level on the table. So either we make an equivalent to
> RangeVarGetRelidExtended() that takes an oid, or the C-caller has to derive
> a RangeVar, call the existing RangeVarGetRelidExtended() function, and
> verify the result reloid against the supplied parameter. I went with
> deriving the RangeVar and putting an Assert on the before/after reloids,
> but perhaps the smarter play is to make a function that checks for
> ShareUpdateExclusiveLock on the Relation, and then does the equivalent of
> RangeVarCallbackForStats().
>
> I don't think this is really a problem. If the caller is specifying
> the OID, they should have called RangeVarGetRelidExtended themselves.
> Permissions-checking, locking, and opening the relation should all
> happen simultaneously, and the logic shouldn't be duplicated later.
>

It seems dangerous to me to provide an externally visible function that
assumes the caller has already self-verified that they have permission to
modify stats for an object.

It's possible that the existing analyze() code already has verified these
permissions, but at this moment I'm unsure.


> > A smaller bit of dissonance was with RecoveryInProgress(), which if I
> recall we're checking before RangeVarGetRelidExtended() to avoid trying to
> take a lock that will fail. That check might not be meaningful if the C
> call takes a relation, thus ensuring that some level of locking worked,
> thus we aren't in recovery.
>
> I don't quite follow this part.
>

I mean that I don't know if that check is required in situations where the
caller already has an open relation with the right lock, and if we can
avoid the call by having the API take a Relation rather than an Oid, then
that would be preferable.


>
> > Next is the existing validation functions stats_check_required_arg(),
> stats_check_arg_array(), and stats_check_arg_pairs() all work on values
> indexed by the positional functioncallinfo and the corresponding
> relstatsinfo/attstatsinfo structure, and this makes a lot of type checking
> and value checking compact and uniform.  If we want to keep this sort of
> uniformity, the resulting StatsData structure will end up looking a lot
> like the FunctionCallInfo that we already had.
>
> Yeah, this is worth thinking about. You could consider putting an
> array inside the struct and use #defines for the indexes. And instead
> of having a separate Boolean for each index, you could use the same
> index to reference the N'th bit of a single integer flag variable.


That's basically what I was trying, though I wasn't brave enough to use
bitflags in a v1 patch.

The #defines already exist now in the form of enums that act as indexes
into the FunctionCallInfo array. But if my data structure is an array of
Datums with an array of is-not-nulls, then I'm just not that far away from
the FunctionCallInfo structure we already have.


>
> > Next is the fact that the end-destination for every value passed in is a
> Datum for a pg_statistic heaptuple. Most Datum values are checked only for
> their null-ness and if they're the correct type, so the value itself is
> usually just passed directly from fcinfo into the heap tuple values[]
> array. The float[] values are checked for number of elements and whether
> any elements are NULL, but that is done via array functions that take a
> Datum input. Only in a few cases do we actually look at the actual internal
> value of the Datum (reltuples, attname, attnum, the anyarrays), so there's
> little to gain there.
>
> Right, so the question is whether it makes more sense to pass down C
> strings or Datums.
>

Requiring Datums simplifies things greatly inside the existing functions,
but pushes that complexity to the caller. My first proposal was to keep
complexity to an absolute minimum on the caller side, but your comments
make me think there's considerable tolerance for more complexity on the
caller side.


> > There's some additional hassle in the fact that
> pg_restore_attribute_stats() can take an attnum parameter OR an attname
> parameter, but not both. I was able to resolve that in a semi-elegant
> fashion, but the other issues have convinced me that we're probably better
> off continuing to use the FunctionCallInfo version of
> attribute_stats_update(), though perhaps with a different name, allowing us
> to use that name for the new API call instead of
> import_attribute_statistics().
>
> On that particular point, I think I'm still unconvinced, but I also
> haven't looked deeply into this just yet, so maybe I'm wrong.
>

The issue becomes moot if we require the caller to construct their own
isnull and values arrays as was suggested above.


^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-17 10:23             ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-17 16:00               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-18 10:42                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-18 21:23                   ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-19 13:45                     ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-20 04:11                       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-22 19:08                         ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-23 12:10                           ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-23 15:53                             ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
@ 2026-06-29 08:14                               ` Corey Huinker <[email protected]>
  2026-06-29 11:50                                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  0 siblings, 1 reply; 33+ messages in thread

From: Corey Huinker @ 2026-06-29 08:14 UTC (permalink / raw)
  To: Robert Haas <[email protected]>; +Cc: Etsuro Fujita <[email protected]>; pgsql-hackers

>
>
> Requiring Datums simplifies things greatly inside the existing functions,
> but pushes that complexity to the caller. My first proposal was to keep
> complexity to an absolute minimum on the caller side, but your comments
> make me think there's considerable tolerance for more complexity on the
> caller side.
>

I've had some time to look this over, and it's pretty clear that we don't
actually need the whole positional FunctionCallInfo, we just need the
fcinfo->args of it. We also need the ability to pass statistical values
along with the values that comprise the key of the
relation/attribute/object to be modified.

There are important differences in the parameters needed by the different
types of functions. The pg_restore_*() SQL function calls need to identify
the schema+relname of the relation being modified, and already have all of
the values as typed Datums, whereas the internal API calls already have an
identified and locked open Relation, but all of the statistical parameters
are just cstrings.

The solution I chose was to create common functions that take a relation
object and an array of just the statistical parameters. This requires the
pg_restore_* functions to resolve and lock the relation themselves, and
then carry forward just the subset of parameters that are statistics (right
types, but wrong number of arguments). Conversely, the internal API
functions need to map their array of cstring values to the correctly typed
Datums (wrong types, but right number of arguments in the right order).

Attached is v2. The patches are broken down into very small bites to aid
digestion and to confirm that tests pass after each comparatively minor
change.

0001-0003: standardize enum values to further indicate which enum the
values belong to. This will be valuable later when values from an array of
one length have to be mapped to values in an array of a different
length. One patch each for relation stats, attribute stats, extended stats.

0004: Refactors the stats_check_* functions that previously took an fcinfo,
but now just take a NullableDatum array.

0005-0007: Change the function signature of
(relation|attribute|extended)_statistics_update() to no longer take
FunctionCallInfo as a parameter and instead take a NullableDatum[].

0008: Changes stats_fill_fcinfo_from_args() which maps a keyword args
fcinfo to a positional fcinfo to map directly to a NullableDatum[], and
changes all callers to use the new function and signature.

0009: rename relation_statistics_update -> update_relstats and
relation_stats_argnum -> relation_args_argnum to make room for
public-facing objects in the following patch.
0010: Introduce new public facing relation_statistics_update, which takes
isnull/string arrays that will be filled out by the caller, currently
postgres_fdw, and calls update_relstats same as pg_restore_relation_stats().

0011: same as 0009, but for attribute stats
0012: same as 0010, but for attribute stats

0013: remove SPI calls from postgres_fdw, and use the newly public
relation_statistics_update() and attribute_statistics_update() functions
instead.


Attachments:

  [application/octet-stream] v2-0004-Stop-using-fcinfo-in-stats_check-functions.patch (10.4K, ../../CADkLM=c9BEuA9vPjE9Wco6+Lz-HSnnmBZrFhi=-yNi0K8tmZug@mail.gmail.com/3-v2-0004-Stop-using-fcinfo-in-stats_check-functions.patch)
  download | inline diff:
From 77c08d00530246928bf607e377ae609547b4917f Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email protected]>
Date: Sun, 28 Jun 2026 15:26:23 -0500
Subject: [PATCH v2 04/13] Stop using fcinfo in stats_check functions.

Change the stats_check utlity functions from taking a full
FunctionCallInfo and instead just use the NullableData array, which is
all the functions needed anyway.

Callers can now pass in fcinfo->args instead of fcinfo as an
intermediate step towards removing FunctionCallInfo entirely.
---
 src/backend/statistics/attribute_stats.c      | 26 +++++++++----------
 src/backend/statistics/extended_stats_funcs.c | 20 +++++++-------
 src/backend/statistics/relation_stats.c       |  4 +--
 src/backend/statistics/stat_utils.c           | 20 +++++++-------
 src/include/statistics/stat_utils.h           |  7 ++---
 5 files changed, 39 insertions(+), 38 deletions(-)

diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index a0ec667011e..fbb52131ed0 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -168,8 +168,8 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 	bool		result = true;
 
-	stats_check_required_arg(fcinfo, attarginfo, ATTARG_ATTRELSCHEMA);
-	stats_check_required_arg(fcinfo, attarginfo, ATTARG_ATTRELNAME);
+	stats_check_required_arg(fcinfo->args, attarginfo, ATTARG_ATTRELSCHEMA);
+	stats_check_required_arg(fcinfo->args, attarginfo, ATTARG_ATTRELNAME);
 
 	nspname = TextDatumGetCString(PG_GETARG_DATUM(ATTARG_ATTRELSCHEMA));
 	relname = TextDatumGetCString(PG_GETARG_DATUM(ATTARG_ATTRELNAME));
@@ -228,7 +228,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 				 errmsg("cannot modify statistics on system column \"%s\"",
 						attname)));
 
-	stats_check_required_arg(fcinfo, attarginfo, ATTARG_INHERITED);
+	stats_check_required_arg(fcinfo->args, attarginfo, ATTARG_INHERITED);
 	inherited = PG_GETARG_BOOL(ATTARG_INHERITED);
 
 	/*
@@ -236,31 +236,31 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	 * and set the corresponding argument to NULL in fcinfo.
 	 */
 
-	if (!stats_check_arg_array(fcinfo, attarginfo, ATTARG_MOST_COMMON_FREQS))
+	if (!stats_check_arg_array(fcinfo->args, attarginfo, ATTARG_MOST_COMMON_FREQS))
 	{
 		do_mcv = false;
 		result = false;
 	}
 
-	if (!stats_check_arg_array(fcinfo, attarginfo, ATTARG_MOST_COMMON_ELEM_FREQS))
+	if (!stats_check_arg_array(fcinfo->args, attarginfo, ATTARG_MOST_COMMON_ELEM_FREQS))
 	{
 		do_mcelem = false;
 		result = false;
 	}
-	if (!stats_check_arg_array(fcinfo, attarginfo, ATTARG_ELEM_COUNT_HISTOGRAM))
+	if (!stats_check_arg_array(fcinfo->args, attarginfo, ATTARG_ELEM_COUNT_HISTOGRAM))
 	{
 		do_dechist = false;
 		result = false;
 	}
 
-	if (!stats_check_arg_pair(fcinfo, attarginfo,
+	if (!stats_check_arg_pair(fcinfo->args, attarginfo,
 							  ATTARG_MOST_COMMON_VALS, ATTARG_MOST_COMMON_FREQS))
 	{
 		do_mcv = false;
 		result = false;
 	}
 
-	if (!stats_check_arg_pair(fcinfo, attarginfo,
+	if (!stats_check_arg_pair(fcinfo->args, attarginfo,
 							  ATTARG_MOST_COMMON_ELEMS,
 							  ATTARG_MOST_COMMON_ELEM_FREQS))
 	{
@@ -268,7 +268,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 		result = false;
 	}
 
-	if (!stats_check_arg_pair(fcinfo, attarginfo,
+	if (!stats_check_arg_pair(fcinfo->args, attarginfo,
 							  ATTARG_RANGE_LENGTH_HISTOGRAM,
 							  ATTARG_RANGE_EMPTY_FRAC))
 	{
@@ -605,10 +605,10 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 	bool		inherited;
 	Oid			locked_table = InvalidOid;
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTARG_ATTRELSCHEMA);
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTARG_ATTRELNAME);
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTARG_ATTNAME);
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTARG_INHERITED);
+	stats_check_required_arg(fcinfo->args, cleararginfo, C_ATTARG_ATTRELSCHEMA);
+	stats_check_required_arg(fcinfo->args, cleararginfo, C_ATTARG_ATTRELNAME);
+	stats_check_required_arg(fcinfo->args, cleararginfo, C_ATTARG_ATTNAME);
+	stats_check_required_arg(fcinfo->args, cleararginfo, C_ATTARG_INHERITED);
 
 	nspname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTARG_ATTRELSCHEMA));
 	relname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTARG_ATTRELNAME));
diff --git a/src/backend/statistics/extended_stats_funcs.c b/src/backend/statistics/extended_stats_funcs.c
index f96376d58d8..f9a1d287e22 100644
--- a/src/backend/statistics/extended_stats_funcs.c
+++ b/src/backend/statistics/extended_stats_funcs.c
@@ -373,17 +373,17 @@ extended_statistics_update(FunctionCallInfo fcinfo)
 	}
 
 	/* relation arguments */
-	stats_check_required_arg(fcinfo, extarginfo, EXTARG_RELSCHEMA);
+	stats_check_required_arg(fcinfo->args, extarginfo, EXTARG_RELSCHEMA);
 	relnspname = TextDatumGetCString(PG_GETARG_DATUM(EXTARG_RELSCHEMA));
-	stats_check_required_arg(fcinfo, extarginfo, EXTARG_RELNAME);
+	stats_check_required_arg(fcinfo->args, extarginfo, EXTARG_RELNAME);
 	relname = TextDatumGetCString(PG_GETARG_DATUM(EXTARG_RELNAME));
 
 	/* extended statistics arguments */
-	stats_check_required_arg(fcinfo, extarginfo, EXTARG_STATSCHEMA);
+	stats_check_required_arg(fcinfo->args, extarginfo, EXTARG_STATSCHEMA);
 	nspname = TextDatumGetCString(PG_GETARG_DATUM(EXTARG_STATSCHEMA));
-	stats_check_required_arg(fcinfo, extarginfo, EXTARG_STATNAME);
+	stats_check_required_arg(fcinfo->args, extarginfo, EXTARG_STATNAME);
 	stxname = TextDatumGetCString(PG_GETARG_DATUM(EXTARG_STATNAME));
-	stats_check_required_arg(fcinfo, extarginfo, EXTARG_INHERITED);
+	stats_check_required_arg(fcinfo->args, extarginfo, EXTARG_INHERITED);
 	inherited = PG_GETARG_BOOL(EXTARG_INHERITED);
 
 	/*
@@ -1769,17 +1769,17 @@ pg_clear_extended_stats(PG_FUNCTION_ARGS)
 	Oid			locked_table = InvalidOid;
 
 	/* relation arguments */
-	stats_check_required_arg(fcinfo, extarginfo, EXTARG_RELSCHEMA);
+	stats_check_required_arg(fcinfo->args, extarginfo, EXTARG_RELSCHEMA);
 	relnspname = TextDatumGetCString(PG_GETARG_DATUM(EXTARG_RELSCHEMA));
-	stats_check_required_arg(fcinfo, extarginfo, EXTARG_RELNAME);
+	stats_check_required_arg(fcinfo->args, extarginfo, EXTARG_RELNAME);
 	relname = TextDatumGetCString(PG_GETARG_DATUM(EXTARG_RELNAME));
 
 	/* extended statistics arguments */
-	stats_check_required_arg(fcinfo, extarginfo, EXTARG_STATSCHEMA);
+	stats_check_required_arg(fcinfo->args, extarginfo, EXTARG_STATSCHEMA);
 	nspname = TextDatumGetCString(PG_GETARG_DATUM(EXTARG_STATSCHEMA));
-	stats_check_required_arg(fcinfo, extarginfo, EXTARG_STATNAME);
+	stats_check_required_arg(fcinfo->args, extarginfo, EXTARG_STATNAME);
 	stxname = TextDatumGetCString(PG_GETARG_DATUM(EXTARG_STATNAME));
-	stats_check_required_arg(fcinfo, extarginfo, EXTARG_INHERITED);
+	stats_check_required_arg(fcinfo->args, extarginfo, EXTARG_INHERITED);
 	inherited = PG_GETARG_BOOL(EXTARG_INHERITED);
 
 	if (RecoveryInProgress())
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 2ff97c0359e..4cfee157be1 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -85,8 +85,8 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 	int			nreplaces = 0;
 	Oid			locked_table = InvalidOid;
 
-	stats_check_required_arg(fcinfo, relarginfo, RELARG_SCHEMA);
-	stats_check_required_arg(fcinfo, relarginfo, RELARG_RELNAME);
+	stats_check_required_arg(fcinfo->args, relarginfo, RELARG_SCHEMA);
+	stats_check_required_arg(fcinfo->args, relarginfo, RELARG_RELNAME);
 
 	nspname = TextDatumGetCString(PG_GETARG_DATUM(RELARG_SCHEMA));
 	relname = TextDatumGetCString(PG_GETARG_DATUM(RELARG_RELNAME));
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index a673e3c704b..24b3849fb8b 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -50,11 +50,11 @@ static Node *statatt_get_index_expr(Relation rel, int attnum);
  * Ensure that a given argument is not null.
  */
 void
-stats_check_required_arg(FunctionCallInfo fcinfo,
+stats_check_required_arg(const NullableDatum *args,
 						 struct StatsArgInfo *arginfo,
 						 int argnum)
 {
-	if (PG_ARGISNULL(argnum))
+	if (args[argnum].isnull)
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("argument \"%s\" must not be null",
@@ -69,16 +69,16 @@ stats_check_required_arg(FunctionCallInfo fcinfo,
  * true.
  */
 bool
-stats_check_arg_array(FunctionCallInfo fcinfo,
+stats_check_arg_array(const NullableDatum *args,
 					  struct StatsArgInfo *arginfo,
 					  int argnum)
 {
 	ArrayType  *arr;
 
-	if (PG_ARGISNULL(argnum))
+	if (args[argnum].isnull)
 		return true;
 
-	arr = DatumGetArrayTypeP(PG_GETARG_DATUM(argnum));
+	arr = DatumGetArrayTypeP(args[argnum].value);
 
 	if (ARR_NDIM(arr) != 1)
 	{
@@ -110,17 +110,17 @@ stats_check_arg_array(FunctionCallInfo fcinfo,
  * true.
  */
 bool
-stats_check_arg_pair(FunctionCallInfo fcinfo,
+stats_check_arg_pair(const NullableDatum *args,
 					 struct StatsArgInfo *arginfo,
 					 int argnum1, int argnum2)
 {
-	if (PG_ARGISNULL(argnum1) && PG_ARGISNULL(argnum2))
+	if (args[argnum1].isnull && args[argnum2].isnull)
 		return true;
 
-	if (PG_ARGISNULL(argnum1) || PG_ARGISNULL(argnum2))
+	if (args[argnum1].isnull || args[argnum2].isnull)
 	{
-		int			nullarg = PG_ARGISNULL(argnum1) ? argnum1 : argnum2;
-		int			otherarg = PG_ARGISNULL(argnum1) ? argnum2 : argnum1;
+		int			nullarg = args[argnum1].isnull ? argnum1 : argnum2;
+		int			otherarg = args[argnum1].isnull ? argnum2 : argnum1;
 
 		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
diff --git a/src/include/statistics/stat_utils.h b/src/include/statistics/stat_utils.h
index 74da7790579..414636c1cd2 100644
--- a/src/include/statistics/stat_utils.h
+++ b/src/include/statistics/stat_utils.h
@@ -15,6 +15,7 @@
 
 #include "access/attnum.h"
 #include "fmgr.h"
+#include "postgres.h"
 
 /* avoid including primnodes.h here */
 typedef struct RangeVar RangeVar;
@@ -25,12 +26,12 @@ struct StatsArgInfo
 	Oid			argtype;
 };
 
-extern void stats_check_required_arg(FunctionCallInfo fcinfo,
+extern void stats_check_required_arg(const NullableDatum *args,
 									 struct StatsArgInfo *arginfo,
 									 int argnum);
-extern bool stats_check_arg_array(FunctionCallInfo fcinfo,
+extern bool stats_check_arg_array(const NullableDatum *args,
 								  struct StatsArgInfo *arginfo, int argnum);
-extern bool stats_check_arg_pair(FunctionCallInfo fcinfo,
+extern bool stats_check_arg_pair(const NullableDatum *args,
 								 struct StatsArgInfo *arginfo,
 								 int argnum1, int argnum2);
 
-- 
2.50.1 (Apple Git-155)



  [application/octet-stream] v2-0002-Rename-attribute_stats_argnum-enumeriations.patch (16.1K, ../../CADkLM=c9BEuA9vPjE9Wco6+Lz-HSnnmBZrFhi=-yNi0K8tmZug@mail.gmail.com/4-v2-0002-Rename-attribute_stats_argnum-enumeriations.patch)
  download | inline diff:
From 89e624841f26b628c79a930981ee333c58558c8f Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email protected]>
Date: Sat, 27 Jun 2026 12:02:43 -0400
Subject: [PATCH v2 02/13] Rename attribute_stats_argnum enumeriations.

Rename all attribute_stats_argnum enumerated names from foo_ARG to
ATTARG_foo and C_foo_ARG to C_ATTARG_foo.  This change will help
distinguish these values from other enumerations which will be added in
future commits.
---
 src/backend/statistics/attribute_stats.c | 206 +++++++++++------------
 1 file changed, 103 insertions(+), 103 deletions(-)

diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 1cc4d657231..a0ec667011e 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -37,48 +37,48 @@
 
 enum attribute_stats_argnum
 {
-	ATTRELSCHEMA_ARG = 0,
-	ATTRELNAME_ARG,
-	ATTNAME_ARG,
-	ATTNUM_ARG,
-	INHERITED_ARG,
-	NULL_FRAC_ARG,
-	AVG_WIDTH_ARG,
-	N_DISTINCT_ARG,
-	MOST_COMMON_VALS_ARG,
-	MOST_COMMON_FREQS_ARG,
-	HISTOGRAM_BOUNDS_ARG,
-	CORRELATION_ARG,
-	MOST_COMMON_ELEMS_ARG,
-	MOST_COMMON_ELEM_FREQS_ARG,
-	ELEM_COUNT_HISTOGRAM_ARG,
-	RANGE_LENGTH_HISTOGRAM_ARG,
-	RANGE_EMPTY_FRAC_ARG,
-	RANGE_BOUNDS_HISTOGRAM_ARG,
-	NUM_ATTRIBUTE_STATS_ARGS
+	ATTARG_ATTRELSCHEMA = 0,
+	ATTARG_ATTRELNAME,
+	ATTARG_ATTNAME,
+	ATTARG_ATTNUM,
+	ATTARG_INHERITED,
+	ATTARG_NULL_FRAC,
+	ATTARG_AVG_WIDTH,
+	ATTARG_N_DISTINCT,
+	ATTARG_MOST_COMMON_VALS,
+	ATTARG_MOST_COMMON_FREQS,
+	ATTARG_HISTOGRAM_BOUNDS,
+	ATTARG_CORRELATION,
+	ATTARG_MOST_COMMON_ELEMS,
+	ATTARG_MOST_COMMON_ELEM_FREQS,
+	ATTARG_ELEM_COUNT_HISTOGRAM,
+	ATTARG_RANGE_LENGTH_HISTOGRAM,
+	ATTARG_RANGE_EMPTY_FRAC,
+	ATTARG_RANGE_BOUNDS_HISTOGRAM,
+	ATTARG_NUM_ATTARGS
 };
 
 static struct StatsArgInfo attarginfo[] =
 {
-	[ATTRELSCHEMA_ARG] = {"schemaname", TEXTOID},
-	[ATTRELNAME_ARG] = {"relname", TEXTOID},
-	[ATTNAME_ARG] = {"attname", TEXTOID},
-	[ATTNUM_ARG] = {"attnum", INT2OID},
-	[INHERITED_ARG] = {"inherited", BOOLOID},
-	[NULL_FRAC_ARG] = {"null_frac", FLOAT4OID},
-	[AVG_WIDTH_ARG] = {"avg_width", INT4OID},
-	[N_DISTINCT_ARG] = {"n_distinct", FLOAT4OID},
-	[MOST_COMMON_VALS_ARG] = {"most_common_vals", TEXTOID},
-	[MOST_COMMON_FREQS_ARG] = {"most_common_freqs", FLOAT4ARRAYOID},
-	[HISTOGRAM_BOUNDS_ARG] = {"histogram_bounds", TEXTOID},
-	[CORRELATION_ARG] = {"correlation", FLOAT4OID},
-	[MOST_COMMON_ELEMS_ARG] = {"most_common_elems", TEXTOID},
-	[MOST_COMMON_ELEM_FREQS_ARG] = {"most_common_elem_freqs", FLOAT4ARRAYOID},
-	[ELEM_COUNT_HISTOGRAM_ARG] = {"elem_count_histogram", FLOAT4ARRAYOID},
-	[RANGE_LENGTH_HISTOGRAM_ARG] = {"range_length_histogram", TEXTOID},
-	[RANGE_EMPTY_FRAC_ARG] = {"range_empty_frac", FLOAT4OID},
-	[RANGE_BOUNDS_HISTOGRAM_ARG] = {"range_bounds_histogram", TEXTOID},
-	[NUM_ATTRIBUTE_STATS_ARGS] = {0}
+	[ATTARG_ATTRELSCHEMA] = {"schemaname", TEXTOID},
+	[ATTARG_ATTRELNAME] = {"relname", TEXTOID},
+	[ATTARG_ATTNAME] = {"attname", TEXTOID},
+	[ATTARG_ATTNUM] = {"attnum", INT2OID},
+	[ATTARG_INHERITED] = {"inherited", BOOLOID},
+	[ATTARG_NULL_FRAC] = {"null_frac", FLOAT4OID},
+	[ATTARG_AVG_WIDTH] = {"avg_width", INT4OID},
+	[ATTARG_N_DISTINCT] = {"n_distinct", FLOAT4OID},
+	[ATTARG_MOST_COMMON_VALS] = {"most_common_vals", TEXTOID},
+	[ATTARG_MOST_COMMON_FREQS] = {"most_common_freqs", FLOAT4ARRAYOID},
+	[ATTARG_HISTOGRAM_BOUNDS] = {"histogram_bounds", TEXTOID},
+	[ATTARG_CORRELATION] = {"correlation", FLOAT4OID},
+	[ATTARG_MOST_COMMON_ELEMS] = {"most_common_elems", TEXTOID},
+	[ATTARG_MOST_COMMON_ELEM_FREQS] = {"most_common_elem_freqs", FLOAT4ARRAYOID},
+	[ATTARG_ELEM_COUNT_HISTOGRAM] = {"elem_count_histogram", FLOAT4ARRAYOID},
+	[ATTARG_RANGE_LENGTH_HISTOGRAM] = {"range_length_histogram", TEXTOID},
+	[ATTARG_RANGE_EMPTY_FRAC] = {"range_empty_frac", FLOAT4OID},
+	[ATTARG_RANGE_BOUNDS_HISTOGRAM] = {"range_bounds_histogram", TEXTOID},
+	[ATTARG_NUM_ATTARGS] = {0}
 };
 
 /*
@@ -88,20 +88,20 @@ static struct StatsArgInfo attarginfo[] =
 
 enum clear_attribute_stats_argnum
 {
-	C_ATTRELSCHEMA_ARG = 0,
-	C_ATTRELNAME_ARG,
-	C_ATTNAME_ARG,
-	C_INHERITED_ARG,
-	C_NUM_ATTRIBUTE_STATS_ARGS
+	C_ATTARG_ATTRELSCHEMA = 0,
+	C_ATTARG_ATTRELNAME,
+	C_ATTARG_ATTNAME,
+	C_ATTARG_INHERITED,
+	C_ATTARG_NUM_ATTARGS
 };
 
 static struct StatsArgInfo cleararginfo[] =
 {
-	[C_ATTRELSCHEMA_ARG] = {"relation", TEXTOID},
-	[C_ATTRELNAME_ARG] = {"relation", TEXTOID},
-	[C_ATTNAME_ARG] = {"attname", TEXTOID},
-	[C_INHERITED_ARG] = {"inherited", BOOLOID},
-	[C_NUM_ATTRIBUTE_STATS_ARGS] = {0}
+	[C_ATTARG_ATTRELSCHEMA] = {"relation", TEXTOID},
+	[C_ATTARG_ATTRELNAME] = {"relation", TEXTOID},
+	[C_ATTARG_ATTNAME] = {"attname", TEXTOID},
+	[C_ATTARG_INHERITED] = {"inherited", BOOLOID},
+	[C_ATTARG_NUM_ATTARGS] = {0}
 };
 
 static bool attribute_statistics_update(FunctionCallInfo fcinfo);
@@ -151,16 +151,16 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 	FmgrInfo	array_in_fn;
 
-	bool		do_mcv = !PG_ARGISNULL(MOST_COMMON_FREQS_ARG) &&
-		!PG_ARGISNULL(MOST_COMMON_VALS_ARG);
-	bool		do_histogram = !PG_ARGISNULL(HISTOGRAM_BOUNDS_ARG);
-	bool		do_correlation = !PG_ARGISNULL(CORRELATION_ARG);
-	bool		do_mcelem = !PG_ARGISNULL(MOST_COMMON_ELEMS_ARG) &&
-		!PG_ARGISNULL(MOST_COMMON_ELEM_FREQS_ARG);
-	bool		do_dechist = !PG_ARGISNULL(ELEM_COUNT_HISTOGRAM_ARG);
-	bool		do_bounds_histogram = !PG_ARGISNULL(RANGE_BOUNDS_HISTOGRAM_ARG);
-	bool		do_range_length_histogram = !PG_ARGISNULL(RANGE_LENGTH_HISTOGRAM_ARG) &&
-		!PG_ARGISNULL(RANGE_EMPTY_FRAC_ARG);
+	bool		do_mcv = !PG_ARGISNULL(ATTARG_MOST_COMMON_FREQS) &&
+		!PG_ARGISNULL(ATTARG_MOST_COMMON_VALS);
+	bool		do_histogram = !PG_ARGISNULL(ATTARG_HISTOGRAM_BOUNDS);
+	bool		do_correlation = !PG_ARGISNULL(ATTARG_CORRELATION);
+	bool		do_mcelem = !PG_ARGISNULL(ATTARG_MOST_COMMON_ELEMS) &&
+		!PG_ARGISNULL(ATTARG_MOST_COMMON_ELEM_FREQS);
+	bool		do_dechist = !PG_ARGISNULL(ATTARG_ELEM_COUNT_HISTOGRAM);
+	bool		do_bounds_histogram = !PG_ARGISNULL(ATTARG_RANGE_BOUNDS_HISTOGRAM);
+	bool		do_range_length_histogram = !PG_ARGISNULL(ATTARG_RANGE_LENGTH_HISTOGRAM) &&
+		!PG_ARGISNULL(ATTARG_RANGE_EMPTY_FRAC);
 
 	Datum		values[Natts_pg_statistic] = {0};
 	bool		nulls[Natts_pg_statistic] = {0};
@@ -168,11 +168,11 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 	bool		result = true;
 
-	stats_check_required_arg(fcinfo, attarginfo, ATTRELSCHEMA_ARG);
-	stats_check_required_arg(fcinfo, attarginfo, ATTRELNAME_ARG);
+	stats_check_required_arg(fcinfo, attarginfo, ATTARG_ATTRELSCHEMA);
+	stats_check_required_arg(fcinfo, attarginfo, ATTARG_ATTRELNAME);
 
-	nspname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELSCHEMA_ARG));
-	relname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELNAME_ARG));
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(ATTARG_ATTRELSCHEMA));
+	relname = TextDatumGetCString(PG_GETARG_DATUM(ATTARG_ATTRELNAME));
 
 	if (RecoveryInProgress())
 		ereport(ERROR,
@@ -186,13 +186,13 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 									  RangeVarCallbackForStats, &locked_table);
 
 	/* user can specify either attname or attnum, but not both */
-	if (!PG_ARGISNULL(ATTNAME_ARG))
+	if (!PG_ARGISNULL(ATTARG_ATTNAME))
 	{
-		if (!PG_ARGISNULL(ATTNUM_ARG))
+		if (!PG_ARGISNULL(ATTARG_ATTNUM))
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("cannot specify both \"%s\" and \"%s\"", "attname", "attnum")));
-		attname = TextDatumGetCString(PG_GETARG_DATUM(ATTNAME_ARG));
+		attname = TextDatumGetCString(PG_GETARG_DATUM(ATTARG_ATTNAME));
 		attnum = get_attnum(reloid, attname);
 		/* note that this test covers attisdropped cases too: */
 		if (attnum == InvalidAttrNumber)
@@ -201,9 +201,9 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 					 errmsg("column \"%s\" of relation \"%s\" does not exist",
 							attname, relname)));
 	}
-	else if (!PG_ARGISNULL(ATTNUM_ARG))
+	else if (!PG_ARGISNULL(ATTARG_ATTNUM))
 	{
-		attnum = PG_GETARG_INT16(ATTNUM_ARG);
+		attnum = PG_GETARG_INT16(ATTARG_ATTNUM);
 		attname = get_attname(reloid, attnum, true);
 		/* annoyingly, get_attname doesn't check attisdropped */
 		if (attname == NULL ||
@@ -228,49 +228,49 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 				 errmsg("cannot modify statistics on system column \"%s\"",
 						attname)));
 
-	stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG);
-	inherited = PG_GETARG_BOOL(INHERITED_ARG);
+	stats_check_required_arg(fcinfo, attarginfo, ATTARG_INHERITED);
+	inherited = PG_GETARG_BOOL(ATTARG_INHERITED);
 
 	/*
 	 * 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(fcinfo, attarginfo, MOST_COMMON_FREQS_ARG))
+	if (!stats_check_arg_array(fcinfo, attarginfo, ATTARG_MOST_COMMON_FREQS))
 	{
 		do_mcv = false;
 		result = false;
 	}
 
-	if (!stats_check_arg_array(fcinfo, attarginfo, MOST_COMMON_ELEM_FREQS_ARG))
+	if (!stats_check_arg_array(fcinfo, attarginfo, ATTARG_MOST_COMMON_ELEM_FREQS))
 	{
 		do_mcelem = false;
 		result = false;
 	}
-	if (!stats_check_arg_array(fcinfo, attarginfo, ELEM_COUNT_HISTOGRAM_ARG))
+	if (!stats_check_arg_array(fcinfo, attarginfo, ATTARG_ELEM_COUNT_HISTOGRAM))
 	{
 		do_dechist = false;
 		result = false;
 	}
 
 	if (!stats_check_arg_pair(fcinfo, attarginfo,
-							  MOST_COMMON_VALS_ARG, MOST_COMMON_FREQS_ARG))
+							  ATTARG_MOST_COMMON_VALS, ATTARG_MOST_COMMON_FREQS))
 	{
 		do_mcv = false;
 		result = false;
 	}
 
 	if (!stats_check_arg_pair(fcinfo, attarginfo,
-							  MOST_COMMON_ELEMS_ARG,
-							  MOST_COMMON_ELEM_FREQS_ARG))
+							  ATTARG_MOST_COMMON_ELEMS,
+							  ATTARG_MOST_COMMON_ELEM_FREQS))
 	{
 		do_mcelem = false;
 		result = false;
 	}
 
 	if (!stats_check_arg_pair(fcinfo, attarginfo,
-							  RANGE_LENGTH_HISTOGRAM_ARG,
-							  RANGE_EMPTY_FRAC_ARG))
+							  ATTARG_RANGE_LENGTH_HISTOGRAM,
+							  ATTARG_RANGE_EMPTY_FRAC))
 	{
 		do_range_length_histogram = false;
 		result = false;
@@ -344,19 +344,19 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 								 replaces);
 
 	/* if specified, set to argument values */
-	if (!PG_ARGISNULL(NULL_FRAC_ARG))
+	if (!PG_ARGISNULL(ATTARG_NULL_FRAC))
 	{
-		values[Anum_pg_statistic_stanullfrac - 1] = PG_GETARG_DATUM(NULL_FRAC_ARG);
+		values[Anum_pg_statistic_stanullfrac - 1] = PG_GETARG_DATUM(ATTARG_NULL_FRAC);
 		replaces[Anum_pg_statistic_stanullfrac - 1] = true;
 	}
-	if (!PG_ARGISNULL(AVG_WIDTH_ARG))
+	if (!PG_ARGISNULL(ATTARG_AVG_WIDTH))
 	{
-		values[Anum_pg_statistic_stawidth - 1] = PG_GETARG_DATUM(AVG_WIDTH_ARG);
+		values[Anum_pg_statistic_stawidth - 1] = PG_GETARG_DATUM(ATTARG_AVG_WIDTH);
 		replaces[Anum_pg_statistic_stawidth - 1] = true;
 	}
-	if (!PG_ARGISNULL(N_DISTINCT_ARG))
+	if (!PG_ARGISNULL(ATTARG_N_DISTINCT))
 	{
-		values[Anum_pg_statistic_stadistinct - 1] = PG_GETARG_DATUM(N_DISTINCT_ARG);
+		values[Anum_pg_statistic_stadistinct - 1] = PG_GETARG_DATUM(ATTARG_N_DISTINCT);
 		replaces[Anum_pg_statistic_stadistinct - 1] = true;
 	}
 
@@ -364,10 +364,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	if (do_mcv)
 	{
 		bool		converted;
-		Datum		stanumbers = PG_GETARG_DATUM(MOST_COMMON_FREQS_ARG);
+		Datum		stanumbers = PG_GETARG_DATUM(ATTARG_MOST_COMMON_FREQS);
 		Datum		stavalues = statatt_build_stavalues("most_common_vals",
 														&array_in_fn,
-														PG_GETARG_DATUM(MOST_COMMON_VALS_ARG),
+														PG_GETARG_DATUM(ATTARG_MOST_COMMON_VALS),
 														atttypid, atttypmod,
 														&converted);
 
@@ -407,7 +407,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 		stavalues = statatt_build_stavalues("histogram_bounds",
 											&array_in_fn,
-											PG_GETARG_DATUM(HISTOGRAM_BOUNDS_ARG),
+											PG_GETARG_DATUM(ATTARG_HISTOGRAM_BOUNDS),
 											atttypid, atttypmod,
 											&converted);
 
@@ -425,7 +425,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	/* STATISTIC_KIND_CORRELATION */
 	if (do_correlation)
 	{
-		Datum		elems[] = {PG_GETARG_DATUM(CORRELATION_ARG)};
+		Datum		elems[] = {PG_GETARG_DATUM(ATTARG_CORRELATION)};
 		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
 		Datum		stanumbers = PointerGetDatum(arry);
 
@@ -438,13 +438,13 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	/* STATISTIC_KIND_MCELEM */
 	if (do_mcelem)
 	{
-		Datum		stanumbers = PG_GETARG_DATUM(MOST_COMMON_ELEM_FREQS_ARG);
+		Datum		stanumbers = PG_GETARG_DATUM(ATTARG_MOST_COMMON_ELEM_FREQS);
 		bool		converted = false;
 		Datum		stavalues;
 
 		stavalues = statatt_build_stavalues("most_common_elems",
 											&array_in_fn,
-											PG_GETARG_DATUM(MOST_COMMON_ELEMS_ARG),
+											PG_GETARG_DATUM(ATTARG_MOST_COMMON_ELEMS),
 											elemtypid, atttypmod,
 											&converted);
 
@@ -462,7 +462,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	/* STATISTIC_KIND_DECHIST */
 	if (do_dechist)
 	{
-		Datum		stanumbers = PG_GETARG_DATUM(ELEM_COUNT_HISTOGRAM_ARG);
+		Datum		stanumbers = PG_GETARG_DATUM(ATTARG_ELEM_COUNT_HISTOGRAM);
 
 		statatt_set_slot(values, nulls, replaces,
 						 STATISTIC_KIND_DECHIST,
@@ -484,7 +484,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 		stavalues = statatt_build_stavalues("range_bounds_histogram",
 											&array_in_fn,
-											PG_GETARG_DATUM(RANGE_BOUNDS_HISTOGRAM_ARG),
+											PG_GETARG_DATUM(ATTARG_RANGE_BOUNDS_HISTOGRAM),
 											atttypid, atttypmod,
 											&converted);
 
@@ -503,7 +503,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	if (do_range_length_histogram)
 	{
 		/* The anyarray is always a float8[] for this stakind */
-		Datum		elems[] = {PG_GETARG_DATUM(RANGE_EMPTY_FRAC_ARG)};
+		Datum		elems[] = {PG_GETARG_DATUM(ATTARG_RANGE_EMPTY_FRAC)};
 		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
 		Datum		stanumbers = PointerGetDatum(arry);
 
@@ -512,7 +512,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 		stavalues = statatt_build_stavalues("range_length_histogram",
 											&array_in_fn,
-											PG_GETARG_DATUM(RANGE_LENGTH_HISTOGRAM_ARG),
+											PG_GETARG_DATUM(ATTARG_RANGE_LENGTH_HISTOGRAM),
 											FLOAT8OID, 0, &converted);
 
 		if (converted)
@@ -605,13 +605,13 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 	bool		inherited;
 	Oid			locked_table = InvalidOid;
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELSCHEMA_ARG);
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELNAME_ARG);
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG);
-	stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTARG_ATTRELSCHEMA);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTARG_ATTRELNAME);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTARG_ATTNAME);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTARG_INHERITED);
 
-	nspname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELSCHEMA_ARG));
-	relname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELNAME_ARG));
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTARG_ATTRELSCHEMA));
+	relname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTARG_ATTRELNAME));
 
 	if (RecoveryInProgress())
 		ereport(ERROR,
@@ -623,7 +623,7 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 									  ShareUpdateExclusiveLock, 0,
 									  RangeVarCallbackForStats, &locked_table);
 
-	attname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTNAME_ARG));
+	attname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTARG_ATTNAME));
 	attnum = get_attnum(reloid, attname);
 
 	if (attnum < 0)
@@ -638,7 +638,7 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 				 errmsg("column \"%s\" of relation \"%s\" does not exist",
 						attname, get_rel_name(reloid))));
 
-	inherited = PG_GETARG_BOOL(C_INHERITED_ARG);
+	inherited = PG_GETARG_BOOL(C_ATTARG_INHERITED);
 
 	delete_pg_statistic(reloid, attnum, inherited);
 	PG_RETURN_VOID();
@@ -673,10 +673,10 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 Datum
 pg_restore_attribute_stats(PG_FUNCTION_ARGS)
 {
-	LOCAL_FCINFO(positional_fcinfo, NUM_ATTRIBUTE_STATS_ARGS);
+	LOCAL_FCINFO(positional_fcinfo, ATTARG_NUM_ATTARGS);
 	bool		result = true;
 
-	InitFunctionCallInfoData(*positional_fcinfo, NULL, NUM_ATTRIBUTE_STATS_ARGS,
+	InitFunctionCallInfoData(*positional_fcinfo, NULL, ATTARG_NUM_ATTARGS,
 							 InvalidOid, NULL, NULL);
 
 	if (!stats_fill_fcinfo_from_arg_pairs(fcinfo, positional_fcinfo,
-- 
2.50.1 (Apple Git-155)



  [application/octet-stream] v2-0001-Rename-relation_stats_argnum-enumeriations.patch (4.2K, ../../CADkLM=c9BEuA9vPjE9Wco6+Lz-HSnnmBZrFhi=-yNi0K8tmZug@mail.gmail.com/5-v2-0001-Rename-relation_stats_argnum-enumeriations.patch)
  download | inline diff:
From f468c342d813f76ddc4a778cf3d6a5a7d26c184f Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email protected]>
Date: Sat, 27 Jun 2026 11:39:04 -0400
Subject: [PATCH v2 01/13] Rename relation_stats_argnum enumeriations.

Rename all relation_stats_argnum enumerated names from foo_ARG to
RELARG_foo. This change will help distinguish these values from other
enumerations which will be added in future commits.
---
 src/backend/statistics/relation_stats.c | 56 ++++++++++++-------------
 1 file changed, 28 insertions(+), 28 deletions(-)

diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index d6631e9a9a4..2ff97c0359e 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -36,24 +36,24 @@
 
 enum relation_stats_argnum
 {
-	RELSCHEMA_ARG = 0,
-	RELNAME_ARG,
-	RELPAGES_ARG,
-	RELTUPLES_ARG,
-	RELALLVISIBLE_ARG,
-	RELALLFROZEN_ARG,
-	NUM_RELATION_STATS_ARGS
+	RELARG_SCHEMA = 0,
+	RELARG_RELNAME,
+	RELARG_RELPAGES,
+	RELARG_RELTUPLES,
+	RELARG_RELALLVISIBLE,
+	RELARG_RELALLFROZEN,
+	RELARG_NUM_RELARGS
 };
 
 static struct StatsArgInfo relarginfo[] =
 {
-	[RELSCHEMA_ARG] = {"schemaname", TEXTOID},
-	[RELNAME_ARG] = {"relname", TEXTOID},
-	[RELPAGES_ARG] = {"relpages", INT4OID},
-	[RELTUPLES_ARG] = {"reltuples", FLOAT4OID},
-	[RELALLVISIBLE_ARG] = {"relallvisible", INT4OID},
-	[RELALLFROZEN_ARG] = {"relallfrozen", INT4OID},
-	[NUM_RELATION_STATS_ARGS] = {0}
+	[RELARG_SCHEMA] = {"schemaname", TEXTOID},
+	[RELARG_RELNAME] = {"relname", TEXTOID},
+	[RELARG_RELPAGES] = {"relpages", INT4OID},
+	[RELARG_RELTUPLES] = {"reltuples", FLOAT4OID},
+	[RELARG_RELALLVISIBLE] = {"relallvisible", INT4OID},
+	[RELARG_RELALLFROZEN] = {"relallfrozen", INT4OID},
+	[RELARG_NUM_RELARGS] = {0}
 };
 
 static bool relation_statistics_update(FunctionCallInfo fcinfo);
@@ -85,11 +85,11 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 	int			nreplaces = 0;
 	Oid			locked_table = InvalidOid;
 
-	stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG);
-	stats_check_required_arg(fcinfo, relarginfo, RELNAME_ARG);
+	stats_check_required_arg(fcinfo, relarginfo, RELARG_SCHEMA);
+	stats_check_required_arg(fcinfo, relarginfo, RELARG_RELNAME);
 
-	nspname = TextDatumGetCString(PG_GETARG_DATUM(RELSCHEMA_ARG));
-	relname = TextDatumGetCString(PG_GETARG_DATUM(RELNAME_ARG));
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(RELARG_SCHEMA));
+	relname = TextDatumGetCString(PG_GETARG_DATUM(RELARG_RELNAME));
 
 	if (RecoveryInProgress())
 		ereport(ERROR,
@@ -101,15 +101,15 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 									  ShareUpdateExclusiveLock, 0,
 									  RangeVarCallbackForStats, &locked_table);
 
-	if (!PG_ARGISNULL(RELPAGES_ARG))
+	if (!PG_ARGISNULL(RELARG_RELPAGES))
 	{
-		relpages = PG_GETARG_UINT32(RELPAGES_ARG);
+		relpages = PG_GETARG_UINT32(RELARG_RELPAGES);
 		update_relpages = true;
 	}
 
-	if (!PG_ARGISNULL(RELTUPLES_ARG))
+	if (!PG_ARGISNULL(RELARG_RELTUPLES))
 	{
-		reltuples = PG_GETARG_FLOAT4(RELTUPLES_ARG);
+		reltuples = PG_GETARG_FLOAT4(RELARG_RELTUPLES);
 		if (reltuples < -1.0)
 		{
 			ereport(WARNING,
@@ -121,15 +121,15 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 			update_reltuples = true;
 	}
 
-	if (!PG_ARGISNULL(RELALLVISIBLE_ARG))
+	if (!PG_ARGISNULL(RELARG_RELALLVISIBLE))
 	{
-		relallvisible = PG_GETARG_UINT32(RELALLVISIBLE_ARG);
+		relallvisible = PG_GETARG_UINT32(RELARG_RELALLVISIBLE);
 		update_relallvisible = true;
 	}
 
-	if (!PG_ARGISNULL(RELALLFROZEN_ARG))
+	if (!PG_ARGISNULL(RELARG_RELALLFROZEN))
 	{
-		relallfrozen = PG_GETARG_UINT32(RELALLFROZEN_ARG);
+		relallfrozen = PG_GETARG_UINT32(RELARG_RELALLFROZEN);
 		update_relallfrozen = true;
 	}
 
@@ -225,11 +225,11 @@ pg_clear_relation_stats(PG_FUNCTION_ARGS)
 Datum
 pg_restore_relation_stats(PG_FUNCTION_ARGS)
 {
-	LOCAL_FCINFO(positional_fcinfo, NUM_RELATION_STATS_ARGS);
+	LOCAL_FCINFO(positional_fcinfo, RELARG_NUM_RELARGS);
 	bool		result = true;
 
 	InitFunctionCallInfoData(*positional_fcinfo, NULL,
-							 NUM_RELATION_STATS_ARGS,
+							 RELARG_NUM_RELARGS,
 							 InvalidOid, NULL, NULL);
 
 	if (!stats_fill_fcinfo_from_arg_pairs(fcinfo, positional_fcinfo,

base-commit: b7e4e3e7fa73458ecca5cd10f341743fd12a4faa
-- 
2.50.1 (Apple Git-155)



  [application/octet-stream] v2-0003-Rename-extended-stats-enumerations.patch (28.1K, ../../CADkLM=c9BEuA9vPjE9Wco6+Lz-HSnnmBZrFhi=-yNi0K8tmZug@mail.gmail.com/6-v2-0003-Rename-extended-stats-enumerations.patch)
  download | inline diff:
From 99f1a23626991910b2f3baa113813542b7fd33f1 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email protected]>
Date: Sat, 27 Jun 2026 12:13:28 -0400
Subject: [PATCH v2 03/13] Rename extended stats enumerations.

Rename extended_stats_argnum and extended_stats_exprs_element
enumerations.

Rename all extended_stats_argnum and extended_stats_exprs_element
enumerated names from foo_ARG to EXTARG_foo, and foo_ELEM to
EXPRELEM_foo.  This change will help distinguish these values from other
enumerations which will be added in future commits.
---
 src/backend/statistics/extended_stats_funcs.c | 308 +++++++++---------
 1 file changed, 154 insertions(+), 154 deletions(-)

diff --git a/src/backend/statistics/extended_stats_funcs.c b/src/backend/statistics/extended_stats_funcs.c
index 2cb3942056f..f96376d58d8 100644
--- a/src/backend/statistics/extended_stats_funcs.c
+++ b/src/backend/statistics/extended_stats_funcs.c
@@ -45,18 +45,18 @@
  */
 enum extended_stats_argnum
 {
-	RELSCHEMA_ARG = 0,
-	RELNAME_ARG,
-	STATSCHEMA_ARG,
-	STATNAME_ARG,
-	INHERITED_ARG,
-	NDISTINCT_ARG,
-	DEPENDENCIES_ARG,
-	MOST_COMMON_VALS_ARG,
-	MOST_COMMON_FREQS_ARG,
-	MOST_COMMON_BASE_FREQS_ARG,
-	EXPRESSIONS_ARG,
-	NUM_EXTENDED_STATS_ARGS,
+	EXTARG_RELSCHEMA = 0,
+	EXTARG_RELNAME,
+	EXTARG_STATSCHEMA,
+	EXTARG_STATNAME,
+	EXTARG_INHERITED,
+	EXTARG_NDISTINCT,
+	EXTARG_DEPENDENCIES,
+	EXTARG_MOST_COMMON_VALS,
+	EXTARG_MOST_COMMON_FREQS,
+	EXTARG_MOST_COMMON_BASE_FREQS,
+	EXTARG_EXPRESSIONS,
+	EXTARG_NUM_EXTARGS,
 };
 
 /*
@@ -65,18 +65,18 @@ enum extended_stats_argnum
  */
 static struct StatsArgInfo extarginfo[] =
 {
-	[RELSCHEMA_ARG] = {"schemaname", TEXTOID},
-	[RELNAME_ARG] = {"relname", TEXTOID},
-	[STATSCHEMA_ARG] = {"statistics_schemaname", TEXTOID},
-	[STATNAME_ARG] = {"statistics_name", TEXTOID},
-	[INHERITED_ARG] = {"inherited", BOOLOID},
-	[NDISTINCT_ARG] = {"n_distinct", PG_NDISTINCTOID},
-	[DEPENDENCIES_ARG] = {"dependencies", PG_DEPENDENCIESOID},
-	[MOST_COMMON_VALS_ARG] = {"most_common_vals", TEXTARRAYOID},
-	[MOST_COMMON_FREQS_ARG] = {"most_common_freqs", FLOAT8ARRAYOID},
-	[MOST_COMMON_BASE_FREQS_ARG] = {"most_common_base_freqs", FLOAT8ARRAYOID},
-	[EXPRESSIONS_ARG] = {"exprs", JSONBOID},
-	[NUM_EXTENDED_STATS_ARGS] = {0},
+	[EXTARG_RELSCHEMA] = {"schemaname", TEXTOID},
+	[EXTARG_RELNAME] = {"relname", TEXTOID},
+	[EXTARG_STATSCHEMA] = {"statistics_schemaname", TEXTOID},
+	[EXTARG_STATNAME] = {"statistics_name", TEXTOID},
+	[EXTARG_INHERITED] = {"inherited", BOOLOID},
+	[EXTARG_NDISTINCT] = {"n_distinct", PG_NDISTINCTOID},
+	[EXTARG_DEPENDENCIES] = {"dependencies", PG_DEPENDENCIESOID},
+	[EXTARG_MOST_COMMON_VALS] = {"most_common_vals", TEXTARRAYOID},
+	[EXTARG_MOST_COMMON_FREQS] = {"most_common_freqs", FLOAT8ARRAYOID},
+	[EXTARG_MOST_COMMON_BASE_FREQS] = {"most_common_base_freqs", FLOAT8ARRAYOID},
+	[EXTARG_EXPRESSIONS] = {"exprs", JSONBOID},
+	[EXTARG_NUM_EXTARGS] = {0},
 };
 
 /*
@@ -85,26 +85,26 @@ static struct StatsArgInfo extarginfo[] =
  */
 enum extended_stats_exprs_element
 {
-	NULL_FRAC_ELEM = 0,
-	AVG_WIDTH_ELEM,
-	N_DISTINCT_ELEM,
-	MOST_COMMON_VALS_ELEM,
-	MOST_COMMON_FREQS_ELEM,
-	HISTOGRAM_BOUNDS_ELEM,
-	CORRELATION_ELEM,
-	MOST_COMMON_ELEMS_ELEM,
-	MOST_COMMON_ELEM_FREQS_ELEM,
-	ELEM_COUNT_HISTOGRAM_ELEM,
-	RANGE_LENGTH_HISTOGRAM_ELEM,
-	RANGE_EMPTY_FRAC_ELEM,
-	RANGE_BOUNDS_HISTOGRAM_ELEM,
-	NUM_ATTRIBUTE_STATS_ELEMS
+	EXPRELEM_NULL_FRAC = 0,
+	EXPRELEM_AVG_WIDTH,
+	EXPRELEM_N_DISTINCT,
+	EXPRELEM_MOST_COMMON_VALS,
+	EXPRELEM_MOST_COMMON_FREQS,
+	EXPRELEM_HISTOGRAM_BOUNDS,
+	EXPRELEM_CORRELATION,
+	EXPRELEM_MOST_COMMON_ELEMS,
+	EXPRELEM_MOST_COMMON_ELEM_FREQS,
+	EXPRELEM_ELEM_COUNT_HISTOGRAM,
+	EXPRELEM_RANGE_LENGTH_HISTOGRAM,
+	EXPRELEM_RANGE_EMPTY_FRAC,
+	EXPRELEM_RANGE_BOUNDS_HISTOGRAM,
+	EXPRELEM_NUM_EXPRELEMS
 };
 
 /*
  * The argument names of the repeating arguments for stxdexpr.
  */
-static const char *extexprargname[NUM_ATTRIBUTE_STATS_ELEMS] =
+static const char *extexprargname[EXPRELEM_NUM_EXPRELEMS] =
 {
 	"null_frac",
 	"avg_width",
@@ -356,12 +356,12 @@ extended_statistics_update(FunctionCallInfo fcinfo)
 	 * Therefore, none of the three array values is meaningful unless the
 	 * other two are also present and in sync in terms of array length.
 	 */
-	has.mcv = (!PG_ARGISNULL(MOST_COMMON_VALS_ARG) &&
-			   !PG_ARGISNULL(MOST_COMMON_FREQS_ARG) &&
-			   !PG_ARGISNULL(MOST_COMMON_BASE_FREQS_ARG));
-	has.ndistinct = !PG_ARGISNULL(NDISTINCT_ARG);
-	has.dependencies = !PG_ARGISNULL(DEPENDENCIES_ARG);
-	has.expressions = !PG_ARGISNULL(EXPRESSIONS_ARG);
+	has.mcv = (!PG_ARGISNULL(EXTARG_MOST_COMMON_VALS) &&
+			   !PG_ARGISNULL(EXTARG_MOST_COMMON_FREQS) &&
+			   !PG_ARGISNULL(EXTARG_MOST_COMMON_BASE_FREQS));
+	has.ndistinct = !PG_ARGISNULL(EXTARG_NDISTINCT);
+	has.dependencies = !PG_ARGISNULL(EXTARG_DEPENDENCIES);
+	has.expressions = !PG_ARGISNULL(EXTARG_EXPRESSIONS);
 
 	if (RecoveryInProgress())
 	{
@@ -373,18 +373,18 @@ extended_statistics_update(FunctionCallInfo fcinfo)
 	}
 
 	/* relation arguments */
-	stats_check_required_arg(fcinfo, extarginfo, RELSCHEMA_ARG);
-	relnspname = TextDatumGetCString(PG_GETARG_DATUM(RELSCHEMA_ARG));
-	stats_check_required_arg(fcinfo, extarginfo, RELNAME_ARG);
-	relname = TextDatumGetCString(PG_GETARG_DATUM(RELNAME_ARG));
+	stats_check_required_arg(fcinfo, extarginfo, EXTARG_RELSCHEMA);
+	relnspname = TextDatumGetCString(PG_GETARG_DATUM(EXTARG_RELSCHEMA));
+	stats_check_required_arg(fcinfo, extarginfo, EXTARG_RELNAME);
+	relname = TextDatumGetCString(PG_GETARG_DATUM(EXTARG_RELNAME));
 
 	/* extended statistics arguments */
-	stats_check_required_arg(fcinfo, extarginfo, STATSCHEMA_ARG);
-	nspname = TextDatumGetCString(PG_GETARG_DATUM(STATSCHEMA_ARG));
-	stats_check_required_arg(fcinfo, extarginfo, STATNAME_ARG);
-	stxname = TextDatumGetCString(PG_GETARG_DATUM(STATNAME_ARG));
-	stats_check_required_arg(fcinfo, extarginfo, INHERITED_ARG);
-	inherited = PG_GETARG_BOOL(INHERITED_ARG);
+	stats_check_required_arg(fcinfo, extarginfo, EXTARG_STATSCHEMA);
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(EXTARG_STATSCHEMA));
+	stats_check_required_arg(fcinfo, extarginfo, EXTARG_STATNAME);
+	stxname = TextDatumGetCString(PG_GETARG_DATUM(EXTARG_STATNAME));
+	stats_check_required_arg(fcinfo, extarginfo, EXTARG_INHERITED);
+	inherited = PG_GETARG_BOOL(EXTARG_INHERITED);
 
 	/*
 	 * First open the relation where we expect to find the statistics.  This
@@ -482,7 +482,7 @@ extended_statistics_update(FunctionCallInfo fcinfo)
 		ereport(WARNING,
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				errmsg("cannot specify parameter \"%s\"",
-					   extarginfo[NDISTINCT_ARG].argname),
+					   extarginfo[EXTARG_NDISTINCT].argname),
 				errhint("Extended statistics object \"%s.%s\" does not support statistics of this type.",
 						nspname, stxname));
 
@@ -499,7 +499,7 @@ extended_statistics_update(FunctionCallInfo fcinfo)
 		ereport(WARNING,
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				errmsg("cannot specify parameter \"%s\"",
-					   extarginfo[DEPENDENCIES_ARG].argname),
+					   extarginfo[EXTARG_DEPENDENCIES].argname),
 				errhint("Extended statistics object \"%s.%s\" does not support statistics of this type.",
 						nspname, stxname));
 		has.dependencies = false;
@@ -514,16 +514,16 @@ extended_statistics_update(FunctionCallInfo fcinfo)
 	 */
 	if (!enabled.mcv)
 	{
-		if (!PG_ARGISNULL(MOST_COMMON_VALS_ARG) ||
-			!PG_ARGISNULL(MOST_COMMON_FREQS_ARG) ||
-			!PG_ARGISNULL(MOST_COMMON_BASE_FREQS_ARG))
+		if (!PG_ARGISNULL(EXTARG_MOST_COMMON_VALS) ||
+			!PG_ARGISNULL(EXTARG_MOST_COMMON_FREQS) ||
+			!PG_ARGISNULL(EXTARG_MOST_COMMON_BASE_FREQS))
 		{
 			ereport(WARNING,
 					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					errmsg("cannot specify parameters \"%s\", \"%s\", or \"%s\"",
-						   extarginfo[MOST_COMMON_VALS_ARG].argname,
-						   extarginfo[MOST_COMMON_FREQS_ARG].argname,
-						   extarginfo[MOST_COMMON_BASE_FREQS_ARG].argname),
+						   extarginfo[EXTARG_MOST_COMMON_VALS].argname,
+						   extarginfo[EXTARG_MOST_COMMON_FREQS].argname,
+						   extarginfo[EXTARG_MOST_COMMON_BASE_FREQS].argname),
 					errhint("Extended statistics object \"%s.%s\" does not support statistics of this type.",
 							nspname, stxname));
 
@@ -538,16 +538,16 @@ extended_statistics_update(FunctionCallInfo fcinfo)
 		 * statistics object expects something, something is wrong.  This
 		 * issues a WARNING if a partial input has been provided.
 		 */
-		if (!PG_ARGISNULL(MOST_COMMON_VALS_ARG) ||
-			!PG_ARGISNULL(MOST_COMMON_FREQS_ARG) ||
-			!PG_ARGISNULL(MOST_COMMON_BASE_FREQS_ARG))
+		if (!PG_ARGISNULL(EXTARG_MOST_COMMON_VALS) ||
+			!PG_ARGISNULL(EXTARG_MOST_COMMON_FREQS) ||
+			!PG_ARGISNULL(EXTARG_MOST_COMMON_BASE_FREQS))
 		{
 			ereport(WARNING,
 					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					errmsg("could not use \"%s\", \"%s\", and \"%s\": missing one or more parameters",
-						   extarginfo[MOST_COMMON_VALS_ARG].argname,
-						   extarginfo[MOST_COMMON_FREQS_ARG].argname,
-						   extarginfo[MOST_COMMON_BASE_FREQS_ARG].argname));
+						   extarginfo[EXTARG_MOST_COMMON_VALS].argname,
+						   extarginfo[EXTARG_MOST_COMMON_FREQS].argname,
+						   extarginfo[EXTARG_MOST_COMMON_BASE_FREQS].argname));
 			success = false;
 		}
 	}
@@ -561,7 +561,7 @@ extended_statistics_update(FunctionCallInfo fcinfo)
 		ereport(WARNING,
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				errmsg("cannot specify parameter \"%s\"",
-					   extarginfo[EXPRESSIONS_ARG].argname),
+					   extarginfo[EXTARG_EXPRESSIONS].argname),
 				errhint("Extended statistics object \"%s.%s\" does not support statistics of this type.",
 						nspname, stxname));
 
@@ -655,7 +655,7 @@ extended_statistics_update(FunctionCallInfo fcinfo)
 
 	if (has.ndistinct)
 	{
-		Datum		ndistinct_datum = PG_GETARG_DATUM(NDISTINCT_ARG);
+		Datum		ndistinct_datum = PG_GETARG_DATUM(EXTARG_NDISTINCT);
 		bytea	   *data = DatumGetByteaPP(ndistinct_datum);
 		MVNDistinct *ndistinct = statext_ndistinct_deserialize(data);
 
@@ -674,7 +674,7 @@ extended_statistics_update(FunctionCallInfo fcinfo)
 
 	if (has.dependencies)
 	{
-		Datum		dependencies_datum = PG_GETARG_DATUM(DEPENDENCIES_ARG);
+		Datum		dependencies_datum = PG_GETARG_DATUM(EXTARG_DEPENDENCIES);
 		bytea	   *data = DatumGetByteaPP(dependencies_datum);
 		MVDependencies *dependencies = statext_dependencies_deserialize(data);
 
@@ -696,9 +696,9 @@ extended_statistics_update(FunctionCallInfo fcinfo)
 		Datum		datum;
 		bool		val_ok = false;
 
-		datum = import_mcv(PG_GETARG_ARRAYTYPE_P(MOST_COMMON_VALS_ARG),
-						   PG_GETARG_ARRAYTYPE_P(MOST_COMMON_FREQS_ARG),
-						   PG_GETARG_ARRAYTYPE_P(MOST_COMMON_BASE_FREQS_ARG),
+		datum = import_mcv(PG_GETARG_ARRAYTYPE_P(EXTARG_MOST_COMMON_VALS),
+						   PG_GETARG_ARRAYTYPE_P(EXTARG_MOST_COMMON_FREQS),
+						   PG_GETARG_ARRAYTYPE_P(EXTARG_MOST_COMMON_BASE_FREQS),
 						   atttypids, atttypmods, atttypcolls, numattrs,
 						   &val_ok);
 
@@ -733,7 +733,7 @@ extended_statistics_update(FunctionCallInfo fcinfo)
 								   &atttypids[numattnums],
 								   &atttypmods[numattnums],
 								   &atttypcolls[numattnums],
-								   PG_GETARG_JSONB_P(EXPRESSIONS_ARG),
+								   PG_GETARG_JSONB_P(EXTARG_EXPRESSIONS),
 								   &ok);
 
 		table_close(pgsd, RowExclusiveLock);
@@ -797,7 +797,7 @@ check_mcvlist_array(const ArrayType *arr, int argindex, int required_ndims,
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				errmsg("could not parse array \"%s\": incorrect number of elements (same as \"%s\" required)",
 					   extarginfo[argindex].argname,
-					   extarginfo[MOST_COMMON_VALS_ARG].argname));
+					   extarginfo[EXTARG_MOST_COMMON_VALS].argname));
 		return false;
 	}
 
@@ -831,7 +831,7 @@ import_mcv(const ArrayType *mcv_arr, const ArrayType *freqs_arr,
 		ereport(WARNING,
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				errmsg("could not parse array \"%s\": incorrect number of dimensions (%d required)",
-					   extarginfo[MOST_COMMON_VALS_ARG].argname, 2));
+					   extarginfo[EXTARG_MOST_COMMON_VALS].argname, 2));
 		goto mcv_error;
 	}
 
@@ -840,7 +840,7 @@ import_mcv(const ArrayType *mcv_arr, const ArrayType *freqs_arr,
 		ereport(WARNING,
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				errmsg("could not parse array \"%s\": found %d attributes but expected %d",
-					   extarginfo[MOST_COMMON_VALS_ARG].argname,
+					   extarginfo[EXTARG_MOST_COMMON_VALS].argname,
 					   ARR_DIMS(mcv_arr)[1], numattrs));
 		goto mcv_error;
 	}
@@ -861,13 +861,13 @@ import_mcv(const ArrayType *mcv_arr, const ArrayType *freqs_arr,
 		ereport(WARNING,
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				errmsg("could not parse array \"%s\": number of items (%d) exceeds maximum (%d)",
-					   extarginfo[MOST_COMMON_VALS_ARG].argname,
+					   extarginfo[EXTARG_MOST_COMMON_VALS].argname,
 					   nitems, STATS_MCVLIST_MAX_ITEMS));
 		goto mcv_error;
 	}
 
-	if (!check_mcvlist_array(freqs_arr, MOST_COMMON_FREQS_ARG, 1, nitems) ||
-		!check_mcvlist_array(base_freqs_arr, MOST_COMMON_BASE_FREQS_ARG, 1, nitems))
+	if (!check_mcvlist_array(freqs_arr, EXTARG_MOST_COMMON_FREQS, 1, nitems) ||
+		!check_mcvlist_array(base_freqs_arr, EXTARG_MOST_COMMON_BASE_FREQS, 1, nitems))
 	{
 		/* inconsistent input arrays found */
 		goto mcv_error;
@@ -899,7 +899,7 @@ static bool
 key_in_expr_argnames(JsonbValue *key)
 {
 	Assert(key->type == jbvString);
-	for (int i = 0; i < NUM_ATTRIBUTE_STATS_ELEMS; i++)
+	for (int i = 0; i < EXPRELEM_NUM_EXPRELEMS; i++)
 	{
 		if (strlen(extexprargname[i]) == key->val.string.len &&
 			strncmp(extexprargname[i], key->val.string.val, key->val.string.len) == 0)
@@ -1125,7 +1125,7 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 					Oid typid, int32 typmod, Oid typcoll,
 					bool *pg_statistic_ok)
 {
-	const char *argname = extarginfo[EXPRESSIONS_ARG].argname;
+	const char *argname = extarginfo[EXTARG_EXPRESSIONS].argname;
 	TypeCacheEntry *typcache;
 	Datum		values[Natts_pg_statistic];
 	bool		nulls[Natts_pg_statistic];
@@ -1134,8 +1134,8 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 	Datum		pgstdat = (Datum) 0;
 	Oid			elemtypid = InvalidOid;
 	Oid			elemeqopr = InvalidOid;
-	bool		found[NUM_ATTRIBUTE_STATS_ELEMS] = {0};
-	JsonbValue	val[NUM_ATTRIBUTE_STATS_ELEMS] = {0};
+	bool		found[EXPRELEM_NUM_EXPRELEMS] = {0};
+	JsonbValue	val[EXPRELEM_NUM_EXPRELEMS] = {0};
 
 	/* Assume the worst by default. */
 	*pg_statistic_ok = false;
@@ -1154,7 +1154,7 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 	 * neither a string nor a NULL, there is not much we can do, so just give
 	 * on the entire tuple for this expression.
 	 */
-	for (int i = 0; i < NUM_ATTRIBUTE_STATS_ELEMS; i++)
+	for (int i = 0; i < EXPRELEM_NUM_EXPRELEMS; i++)
 	{
 		const char *s = extexprargname[i];
 		int			len = strlen(s);
@@ -1190,26 +1190,26 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 	 * we have ruled out disagreeing pairs, we can use either found flag as a
 	 * proxy for the other.
 	 */
-	if (found[MOST_COMMON_VALS_ELEM] != found[MOST_COMMON_FREQS_ELEM])
+	if (found[EXPRELEM_MOST_COMMON_VALS] != found[EXPRELEM_MOST_COMMON_FREQS])
 	{
 		ereport(WARNING,
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				errmsg("could not parse \"%s\": invalid element in expression %d",
 					   argname, exprnum),
 				errhint("\"%s\" and \"%s\" must be both either strings or nulls.",
-						extexprargname[MOST_COMMON_VALS_ELEM],
-						extexprargname[MOST_COMMON_FREQS_ELEM]));
+						extexprargname[EXPRELEM_MOST_COMMON_VALS],
+						extexprargname[EXPRELEM_MOST_COMMON_FREQS]));
 		goto pg_statistic_error;
 	}
-	if (found[MOST_COMMON_ELEMS_ELEM] != found[MOST_COMMON_ELEM_FREQS_ELEM])
+	if (found[EXPRELEM_MOST_COMMON_ELEMS] != found[EXPRELEM_MOST_COMMON_ELEM_FREQS])
 	{
 		ereport(WARNING,
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				errmsg("could not parse \"%s\": invalid element in expression %d",
 					   argname, exprnum),
 				errhint("\"%s\" and \"%s\" must be both either strings or nulls.",
-						extexprargname[MOST_COMMON_ELEMS_ELEM],
-						extexprargname[MOST_COMMON_ELEM_FREQS_ELEM]));
+						extexprargname[EXPRELEM_MOST_COMMON_ELEMS],
+						extexprargname[EXPRELEM_MOST_COMMON_ELEM_FREQS]));
 		goto pg_statistic_error;
 	}
 
@@ -1217,17 +1217,17 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 	 * Range types may expect three values to be set.  All three of them must
 	 * either be found or not be found.  Any disagreement is a warning.
 	 */
-	if (found[RANGE_LENGTH_HISTOGRAM_ELEM] != found[RANGE_EMPTY_FRAC_ELEM] ||
-		found[RANGE_LENGTH_HISTOGRAM_ELEM] != found[RANGE_BOUNDS_HISTOGRAM_ELEM])
+	if (found[EXPRELEM_RANGE_LENGTH_HISTOGRAM] != found[EXPRELEM_RANGE_EMPTY_FRAC] ||
+		found[EXPRELEM_RANGE_LENGTH_HISTOGRAM] != found[EXPRELEM_RANGE_BOUNDS_HISTOGRAM])
 	{
 		ereport(WARNING,
 				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				errmsg("could not parse \"%s\": invalid element in expression %d",
 					   argname, exprnum),
 				errhint("\"%s\", \"%s\", and \"%s\" must be all either strings or all nulls.",
-						extexprargname[RANGE_LENGTH_HISTOGRAM_ELEM],
-						extexprargname[RANGE_EMPTY_FRAC_ELEM],
-						extexprargname[RANGE_BOUNDS_HISTOGRAM_ELEM]));
+						extexprargname[EXPRELEM_RANGE_LENGTH_HISTOGRAM],
+						extexprargname[EXPRELEM_RANGE_EMPTY_FRAC],
+						extexprargname[EXPRELEM_RANGE_BOUNDS_HISTOGRAM]));
 		goto pg_statistic_error;
 	}
 
@@ -1249,7 +1249,7 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 	 * type MCELEM or DECHIST, otherwise the values are unnecessary and not
 	 * meaningful.
 	 */
-	if (found[MOST_COMMON_ELEMS_ELEM] || found[ELEM_COUNT_HISTOGRAM_ELEM])
+	if (found[EXPRELEM_MOST_COMMON_ELEMS] || found[EXPRELEM_ELEM_COUNT_HISTOGRAM])
 	{
 		if (!statatt_get_elem_type(typid, typcache->typtype,
 								   &elemtypid, &elemeqopr))
@@ -1266,9 +1266,9 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 	 * These three fields can only be set if dealing with a range or
 	 * multi-range type.
 	 */
-	if (found[RANGE_LENGTH_HISTOGRAM_ELEM] ||
-		found[RANGE_EMPTY_FRAC_ELEM] ||
-		found[RANGE_BOUNDS_HISTOGRAM_ELEM])
+	if (found[EXPRELEM_RANGE_LENGTH_HISTOGRAM] ||
+		found[EXPRELEM_RANGE_EMPTY_FRAC] ||
+		found[EXPRELEM_RANGE_BOUNDS_HISTOGRAM])
 	{
 		if (typcache->typtype != TYPTYPE_RANGE &&
 			typcache->typtype != TYPTYPE_MULTIRANGE)
@@ -1278,44 +1278,44 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 					errmsg("could not parse \"%s\": invalid data in expression %d",
 						   argname, exprnum),
 					errhint("\"%s\", \"%s\", and \"%s\" can only be set for a range type.",
-							extexprargname[RANGE_LENGTH_HISTOGRAM_ELEM],
-							extexprargname[RANGE_EMPTY_FRAC_ELEM],
-							extexprargname[RANGE_BOUNDS_HISTOGRAM_ELEM]));
+							extexprargname[EXPRELEM_RANGE_LENGTH_HISTOGRAM],
+							extexprargname[EXPRELEM_RANGE_EMPTY_FRAC],
+							extexprargname[EXPRELEM_RANGE_BOUNDS_HISTOGRAM]));
 			goto pg_statistic_error;
 		}
 	}
 
 	/* null_frac */
-	if (found[NULL_FRAC_ELEM])
+	if (found[EXPRELEM_NULL_FRAC])
 	{
 		Datum		datum;
 
-		if (jbv_to_infunc_datum(&val[NULL_FRAC_ELEM], float4in, exprnum,
-								extexprargname[NULL_FRAC_ELEM], &datum))
+		if (jbv_to_infunc_datum(&val[EXPRELEM_NULL_FRAC], float4in, exprnum,
+								extexprargname[EXPRELEM_NULL_FRAC], &datum))
 			values[Anum_pg_statistic_stanullfrac - 1] = datum;
 		else
 			goto pg_statistic_error;
 	}
 
 	/* avg_width */
-	if (found[AVG_WIDTH_ELEM])
+	if (found[EXPRELEM_AVG_WIDTH])
 	{
 		Datum		datum;
 
-		if (jbv_to_infunc_datum(&val[AVG_WIDTH_ELEM], int4in, exprnum,
-								extexprargname[AVG_WIDTH_ELEM], &datum))
+		if (jbv_to_infunc_datum(&val[EXPRELEM_AVG_WIDTH], int4in, exprnum,
+								extexprargname[EXPRELEM_AVG_WIDTH], &datum))
 			values[Anum_pg_statistic_stawidth - 1] = datum;
 		else
 			goto pg_statistic_error;
 	}
 
 	/* n_distinct */
-	if (found[N_DISTINCT_ELEM])
+	if (found[EXPRELEM_N_DISTINCT])
 	{
 		Datum		datum;
 
-		if (jbv_to_infunc_datum(&val[N_DISTINCT_ELEM], float4in, exprnum,
-								extexprargname[N_DISTINCT_ELEM], &datum))
+		if (jbv_to_infunc_datum(&val[EXPRELEM_N_DISTINCT], float4in, exprnum,
+								extexprargname[EXPRELEM_N_DISTINCT], &datum))
 			values[Anum_pg_statistic_stadistinct - 1] = datum;
 		else
 			goto pg_statistic_error;
@@ -1334,7 +1334,7 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 	 * completely.
 	 */
 
-	if (found[MOST_COMMON_VALS_ELEM])
+	if (found[EXPRELEM_MOST_COMMON_VALS])
 	{
 		Datum		stavalues;
 		Datum		stanumbers;
@@ -1342,16 +1342,16 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 		bool		num_ok = false;
 		char	   *s;
 
-		s = jbv_string_get_cstr(&val[MOST_COMMON_VALS_ELEM]);
+		s = jbv_string_get_cstr(&val[EXPRELEM_MOST_COMMON_VALS]);
 		stavalues = array_in_safe(array_in_fn, s, typid, typmod, exprnum,
-								  extexprargname[MOST_COMMON_VALS_ELEM],
+								  extexprargname[EXPRELEM_MOST_COMMON_VALS],
 								  &val_ok);
 
 		pfree(s);
 
-		s = jbv_string_get_cstr(&val[MOST_COMMON_FREQS_ELEM]);
+		s = jbv_string_get_cstr(&val[EXPRELEM_MOST_COMMON_FREQS]);
 		stanumbers = array_in_safe(array_in_fn, s, FLOAT4OID, -1, exprnum,
-								   extexprargname[MOST_COMMON_FREQS_ELEM],
+								   extexprargname[EXPRELEM_MOST_COMMON_FREQS],
 								   &num_ok);
 		pfree(s);
 
@@ -1383,14 +1383,14 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 	}
 
 	/* STATISTIC_KIND_HISTOGRAM */
-	if (found[HISTOGRAM_BOUNDS_ELEM])
+	if (found[EXPRELEM_HISTOGRAM_BOUNDS])
 	{
 		Datum		stavalues;
 		bool		val_ok = false;
-		char	   *s = jbv_string_get_cstr(&val[HISTOGRAM_BOUNDS_ELEM]);
+		char	   *s = jbv_string_get_cstr(&val[EXPRELEM_HISTOGRAM_BOUNDS]);
 
 		stavalues = array_in_safe(array_in_fn, s, typid, typmod, exprnum,
-								  extexprargname[HISTOGRAM_BOUNDS_ELEM],
+								  extexprargname[EXPRELEM_HISTOGRAM_BOUNDS],
 								  &val_ok);
 		pfree(s);
 
@@ -1404,12 +1404,12 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 	}
 
 	/* STATISTIC_KIND_CORRELATION */
-	if (found[CORRELATION_ELEM])
+	if (found[EXPRELEM_CORRELATION])
 	{
 		Datum		corr[] = {(Datum) 0};
 
-		if (jbv_to_infunc_datum(&val[CORRELATION_ELEM], float4in, exprnum,
-								extexprargname[CORRELATION_ELEM], &corr[0]))
+		if (jbv_to_infunc_datum(&val[EXPRELEM_CORRELATION], float4in, exprnum,
+								extexprargname[EXPRELEM_CORRELATION], &corr[0]))
 		{
 			ArrayType  *arry = construct_array_builtin(corr, 1, FLOAT4OID);
 			Datum		stanumbers = PointerGetDatum(arry);
@@ -1424,7 +1424,7 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 	}
 
 	/* STATISTIC_KIND_MCELEM */
-	if (found[MOST_COMMON_ELEMS_ELEM])
+	if (found[EXPRELEM_MOST_COMMON_ELEMS])
 	{
 		Datum		stavalues;
 		Datum		stanumbers;
@@ -1432,16 +1432,16 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 		bool		num_ok = false;
 		char	   *s;
 
-		s = jbv_string_get_cstr(&val[MOST_COMMON_ELEMS_ELEM]);
+		s = jbv_string_get_cstr(&val[EXPRELEM_MOST_COMMON_ELEMS]);
 		stavalues = array_in_safe(array_in_fn, s, elemtypid, typmod, exprnum,
-								  extexprargname[MOST_COMMON_ELEMS_ELEM],
+								  extexprargname[EXPRELEM_MOST_COMMON_ELEMS],
 								  &val_ok);
 		pfree(s);
 
 
-		s = jbv_string_get_cstr(&val[MOST_COMMON_ELEM_FREQS_ELEM]);
+		s = jbv_string_get_cstr(&val[EXPRELEM_MOST_COMMON_ELEM_FREQS]);
 		stanumbers = array_in_safe(array_in_fn, s, FLOAT4OID, -1, exprnum,
-								   extexprargname[MOST_COMMON_ELEM_FREQS_ELEM],
+								   extexprargname[EXPRELEM_MOST_COMMON_ELEM_FREQS],
 								   &num_ok);
 		pfree(s);
 
@@ -1456,15 +1456,15 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 	}
 
 	/* STATISTIC_KIND_DECHIST */
-	if (found[ELEM_COUNT_HISTOGRAM_ELEM])
+	if (found[EXPRELEM_ELEM_COUNT_HISTOGRAM])
 	{
 		Datum		stanumbers;
 		bool		num_ok = false;
 		char	   *s;
 
-		s = jbv_string_get_cstr(&val[ELEM_COUNT_HISTOGRAM_ELEM]);
+		s = jbv_string_get_cstr(&val[EXPRELEM_ELEM_COUNT_HISTOGRAM]);
 		stanumbers = array_in_safe(array_in_fn, s, FLOAT4OID, -1, exprnum,
-								   extexprargname[ELEM_COUNT_HISTOGRAM_ELEM],
+								   extexprargname[EXPRELEM_ELEM_COUNT_HISTOGRAM],
 								   &num_ok);
 		pfree(s);
 
@@ -1482,7 +1482,7 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 	 * though it is numerically greater, and all other stakinds appear in
 	 * numerical order.
 	 */
-	if (found[RANGE_BOUNDS_HISTOGRAM_ELEM])
+	if (found[EXPRELEM_RANGE_BOUNDS_HISTOGRAM])
 	{
 		Datum		stavalues;
 		bool		val_ok = false;
@@ -1496,10 +1496,10 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 		if (type_is_multirange(typid))
 			rtypid = get_multirange_range(typid);
 
-		s = jbv_string_get_cstr(&val[RANGE_BOUNDS_HISTOGRAM_ELEM]);
+		s = jbv_string_get_cstr(&val[EXPRELEM_RANGE_BOUNDS_HISTOGRAM]);
 
 		stavalues = array_in_safe(array_in_fn, s, rtypid, typmod, exprnum,
-								  extexprargname[RANGE_BOUNDS_HISTOGRAM_ELEM],
+								  extexprargname[EXPRELEM_RANGE_BOUNDS_HISTOGRAM],
 								  &val_ok);
 
 		if (val_ok)
@@ -1512,7 +1512,7 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 	}
 
 	/* STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM */
-	if (found[RANGE_LENGTH_HISTOGRAM_ELEM])
+	if (found[EXPRELEM_RANGE_LENGTH_HISTOGRAM])
 	{
 		Datum		empty_frac[] = {(Datum) 0};
 		Datum		stavalues;
@@ -1520,8 +1520,8 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 		bool		val_ok = false;
 		char	   *s;
 
-		if (jbv_to_infunc_datum(&val[RANGE_EMPTY_FRAC_ELEM], float4in, exprnum,
-								extexprargname[RANGE_EMPTY_FRAC_ELEM], &empty_frac[0]))
+		if (jbv_to_infunc_datum(&val[EXPRELEM_RANGE_EMPTY_FRAC], float4in, exprnum,
+								extexprargname[EXPRELEM_RANGE_EMPTY_FRAC], &empty_frac[0]))
 		{
 			ArrayType  *arry = construct_array_builtin(empty_frac, 1, FLOAT4OID);
 
@@ -1530,9 +1530,9 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont,
 		else
 			goto pg_statistic_error;
 
-		s = jbv_string_get_cstr(&val[RANGE_LENGTH_HISTOGRAM_ELEM]);
+		s = jbv_string_get_cstr(&val[EXPRELEM_RANGE_LENGTH_HISTOGRAM]);
 		stavalues = array_in_safe(array_in_fn, s, FLOAT8OID, -1, exprnum,
-								  extexprargname[RANGE_LENGTH_HISTOGRAM_ELEM],
+								  extexprargname[EXPRELEM_RANGE_LENGTH_HISTOGRAM],
 								  &val_ok);
 
 		if (val_ok)
@@ -1575,7 +1575,7 @@ import_expressions(Relation pgsd, int numexprs,
 				   Oid *atttypcolls, Jsonb *exprs_jsonb,
 				   bool *exprs_is_perfect)
 {
-	const char *argname = extarginfo[EXPRESSIONS_ARG].argname;
+	const char *argname = extarginfo[EXTARG_EXPRESSIONS].argname;
 	Oid			pgstypoid = get_rel_type_id(StatisticRelationId);
 	ArrayBuildState *astate = NULL;
 	Datum		result = (Datum) 0;
@@ -1735,10 +1735,10 @@ delete_pg_statistic_ext_data(Oid stxoid, bool inherited)
 Datum
 pg_restore_extended_stats(PG_FUNCTION_ARGS)
 {
-	LOCAL_FCINFO(positional_fcinfo, NUM_EXTENDED_STATS_ARGS);
+	LOCAL_FCINFO(positional_fcinfo, EXTARG_NUM_EXTARGS);
 	bool		result = true;
 
-	InitFunctionCallInfoData(*positional_fcinfo, NULL, NUM_EXTENDED_STATS_ARGS,
+	InitFunctionCallInfoData(*positional_fcinfo, NULL, EXTARG_NUM_EXTARGS,
 							 InvalidOid, NULL, NULL);
 
 	if (!stats_fill_fcinfo_from_arg_pairs(fcinfo, positional_fcinfo, extarginfo))
@@ -1769,18 +1769,18 @@ pg_clear_extended_stats(PG_FUNCTION_ARGS)
 	Oid			locked_table = InvalidOid;
 
 	/* relation arguments */
-	stats_check_required_arg(fcinfo, extarginfo, RELSCHEMA_ARG);
-	relnspname = TextDatumGetCString(PG_GETARG_DATUM(RELSCHEMA_ARG));
-	stats_check_required_arg(fcinfo, extarginfo, RELNAME_ARG);
-	relname = TextDatumGetCString(PG_GETARG_DATUM(RELNAME_ARG));
+	stats_check_required_arg(fcinfo, extarginfo, EXTARG_RELSCHEMA);
+	relnspname = TextDatumGetCString(PG_GETARG_DATUM(EXTARG_RELSCHEMA));
+	stats_check_required_arg(fcinfo, extarginfo, EXTARG_RELNAME);
+	relname = TextDatumGetCString(PG_GETARG_DATUM(EXTARG_RELNAME));
 
 	/* extended statistics arguments */
-	stats_check_required_arg(fcinfo, extarginfo, STATSCHEMA_ARG);
-	nspname = TextDatumGetCString(PG_GETARG_DATUM(STATSCHEMA_ARG));
-	stats_check_required_arg(fcinfo, extarginfo, STATNAME_ARG);
-	stxname = TextDatumGetCString(PG_GETARG_DATUM(STATNAME_ARG));
-	stats_check_required_arg(fcinfo, extarginfo, INHERITED_ARG);
-	inherited = PG_GETARG_BOOL(INHERITED_ARG);
+	stats_check_required_arg(fcinfo, extarginfo, EXTARG_STATSCHEMA);
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(EXTARG_STATSCHEMA));
+	stats_check_required_arg(fcinfo, extarginfo, EXTARG_STATNAME);
+	stxname = TextDatumGetCString(PG_GETARG_DATUM(EXTARG_STATNAME));
+	stats_check_required_arg(fcinfo, extarginfo, EXTARG_INHERITED);
+	inherited = PG_GETARG_BOOL(EXTARG_INHERITED);
 
 	if (RecoveryInProgress())
 	{
-- 
2.50.1 (Apple Git-155)



  [application/octet-stream] v2-0005-Make-relation_statistics_update-stop-using-fcinfo.patch (3.6K, ../../CADkLM=c9BEuA9vPjE9Wco6+Lz-HSnnmBZrFhi=-yNi0K8tmZug@mail.gmail.com/7-v2-0005-Make-relation_statistics_update-stop-using-fcinfo.patch)
  download | inline diff:
From e933298dfd1ebd7a25346c9d342e093582d1fbd7 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email protected]>
Date: Sun, 28 Jun 2026 16:09:49 -0500
Subject: [PATCH v2 05/13] Make relation_statistics_update stop using fcinfo.

Change the function signature of relation_statistics_update to use a
NullableDatum array instead of a full FunctionCallInfo.
---
 src/backend/statistics/relation_stats.c | 32 ++++++++++++-------------
 1 file changed, 16 insertions(+), 16 deletions(-)

diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 4cfee157be1..1ee23612ae1 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -56,13 +56,13 @@ static struct StatsArgInfo relarginfo[] =
 	[RELARG_NUM_RELARGS] = {0}
 };
 
-static bool relation_statistics_update(FunctionCallInfo fcinfo);
+static bool relation_statistics_update(const NullableDatum *args);
 
 /*
  * Internal function for modifying statistics for a relation.
  */
 static bool
-relation_statistics_update(FunctionCallInfo fcinfo)
+relation_statistics_update(const NullableDatum *args)
 {
 	bool		result = true;
 	char	   *nspname;
@@ -85,11 +85,11 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 	int			nreplaces = 0;
 	Oid			locked_table = InvalidOid;
 
-	stats_check_required_arg(fcinfo->args, relarginfo, RELARG_SCHEMA);
-	stats_check_required_arg(fcinfo->args, relarginfo, RELARG_RELNAME);
+	stats_check_required_arg(args, relarginfo, RELARG_SCHEMA);
+	stats_check_required_arg(args, relarginfo, RELARG_RELNAME);
 
-	nspname = TextDatumGetCString(PG_GETARG_DATUM(RELARG_SCHEMA));
-	relname = TextDatumGetCString(PG_GETARG_DATUM(RELARG_RELNAME));
+	nspname = TextDatumGetCString(args[RELARG_SCHEMA].value);
+	relname = TextDatumGetCString(args[RELARG_RELNAME].value);
 
 	if (RecoveryInProgress())
 		ereport(ERROR,
@@ -101,15 +101,15 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 									  ShareUpdateExclusiveLock, 0,
 									  RangeVarCallbackForStats, &locked_table);
 
-	if (!PG_ARGISNULL(RELARG_RELPAGES))
+	if (!args[RELARG_RELPAGES].isnull)
 	{
-		relpages = PG_GETARG_UINT32(RELARG_RELPAGES);
+		relpages = DatumGetUInt32(args[RELARG_RELPAGES].value);
 		update_relpages = true;
 	}
 
-	if (!PG_ARGISNULL(RELARG_RELTUPLES))
+	if (!args[RELARG_RELTUPLES].isnull)
 	{
-		reltuples = PG_GETARG_FLOAT4(RELARG_RELTUPLES);
+		reltuples = DatumGetFloat4(args[RELARG_RELTUPLES].value);
 		if (reltuples < -1.0)
 		{
 			ereport(WARNING,
@@ -121,15 +121,15 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 			update_reltuples = true;
 	}
 
-	if (!PG_ARGISNULL(RELARG_RELALLVISIBLE))
+	if (!args[RELARG_RELALLVISIBLE].isnull)
 	{
-		relallvisible = PG_GETARG_UINT32(RELARG_RELALLVISIBLE);
+		relallvisible = DatumGetUInt32(args[RELARG_RELALLVISIBLE].value);
 		update_relallvisible = true;
 	}
 
-	if (!PG_ARGISNULL(RELARG_RELALLFROZEN))
+	if (!args[RELARG_RELALLFROZEN].isnull)
 	{
-		relallfrozen = PG_GETARG_UINT32(RELARG_RELALLFROZEN);
+		relallfrozen = DatumGetUInt32(args[RELARG_RELALLFROZEN].value);
 		update_relallfrozen = true;
 	}
 
@@ -218,7 +218,7 @@ pg_clear_relation_stats(PG_FUNCTION_ARGS)
 	newfcinfo->args[5].value = UInt32GetDatum(0);
 	newfcinfo->args[5].isnull = false;
 
-	relation_statistics_update(newfcinfo);
+	relation_statistics_update(newfcinfo->args);
 	PG_RETURN_VOID();
 }
 
@@ -236,7 +236,7 @@ pg_restore_relation_stats(PG_FUNCTION_ARGS)
 										  relarginfo))
 		result = false;
 
-	if (!relation_statistics_update(positional_fcinfo))
+	if (!relation_statistics_update(positional_fcinfo->args))
 		result = false;
 
 	PG_RETURN_BOOL(result);
-- 
2.50.1 (Apple Git-155)



  [application/octet-stream] v2-0006-Make-attribute_statistics_update-stop-using-fcinf.patch (11.0K, ../../CADkLM=c9BEuA9vPjE9Wco6+Lz-HSnnmBZrFhi=-yNi0K8tmZug@mail.gmail.com/8-v2-0006-Make-attribute_statistics_update-stop-using-fcinf.patch)
  download | inline diff:
From f8fd160b831c765f1364398e93c722f1edaeddb1 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email protected]>
Date: Sun, 28 Jun 2026 16:38:56 -0500
Subject: [PATCH v2 06/13] Make attribute_statistics_update stop using fcinfo.

Change the function signature of attribute_statistics_update to use a
NullableDatum array instead of a full FunctionCallInfo.
---
 src/backend/statistics/attribute_stats.c | 92 ++++++++++++------------
 1 file changed, 46 insertions(+), 46 deletions(-)

diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index fbb52131ed0..a847d053ee4 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -104,7 +104,7 @@ static struct StatsArgInfo cleararginfo[] =
 	[C_ATTARG_NUM_ATTARGS] = {0}
 };
 
-static bool attribute_statistics_update(FunctionCallInfo fcinfo);
+static bool attribute_statistics_update(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,7 +126,7 @@ static bool delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit);
  * and other statistic kinds may still be updated.
  */
 static bool
-attribute_statistics_update(FunctionCallInfo fcinfo)
+attribute_statistics_update(const NullableDatum *args)
 {
 	char	   *nspname;
 	char	   *relname;
@@ -151,16 +151,16 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 	FmgrInfo	array_in_fn;
 
-	bool		do_mcv = !PG_ARGISNULL(ATTARG_MOST_COMMON_FREQS) &&
-		!PG_ARGISNULL(ATTARG_MOST_COMMON_VALS);
-	bool		do_histogram = !PG_ARGISNULL(ATTARG_HISTOGRAM_BOUNDS);
-	bool		do_correlation = !PG_ARGISNULL(ATTARG_CORRELATION);
-	bool		do_mcelem = !PG_ARGISNULL(ATTARG_MOST_COMMON_ELEMS) &&
-		!PG_ARGISNULL(ATTARG_MOST_COMMON_ELEM_FREQS);
-	bool		do_dechist = !PG_ARGISNULL(ATTARG_ELEM_COUNT_HISTOGRAM);
-	bool		do_bounds_histogram = !PG_ARGISNULL(ATTARG_RANGE_BOUNDS_HISTOGRAM);
-	bool		do_range_length_histogram = !PG_ARGISNULL(ATTARG_RANGE_LENGTH_HISTOGRAM) &&
-		!PG_ARGISNULL(ATTARG_RANGE_EMPTY_FRAC);
+	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;
 
 	Datum		values[Natts_pg_statistic] = {0};
 	bool		nulls[Natts_pg_statistic] = {0};
@@ -168,11 +168,11 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 	bool		result = true;
 
-	stats_check_required_arg(fcinfo->args, attarginfo, ATTARG_ATTRELSCHEMA);
-	stats_check_required_arg(fcinfo->args, attarginfo, ATTARG_ATTRELNAME);
+	stats_check_required_arg(args, attarginfo, ATTARG_ATTRELSCHEMA);
+	stats_check_required_arg(args, attarginfo, ATTARG_ATTRELNAME);
 
-	nspname = TextDatumGetCString(PG_GETARG_DATUM(ATTARG_ATTRELSCHEMA));
-	relname = TextDatumGetCString(PG_GETARG_DATUM(ATTARG_ATTRELNAME));
+	nspname = TextDatumGetCString(args[ATTARG_ATTRELSCHEMA].value);
+	relname = TextDatumGetCString(args[ATTARG_ATTRELNAME].value);
 
 	if (RecoveryInProgress())
 		ereport(ERROR,
@@ -186,13 +186,13 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 									  RangeVarCallbackForStats, &locked_table);
 
 	/* user can specify either attname or attnum, but not both */
-	if (!PG_ARGISNULL(ATTARG_ATTNAME))
+	if (!args[ATTARG_ATTNAME].isnull)
 	{
-		if (!PG_ARGISNULL(ATTARG_ATTNUM))
+		if (!args[ATTARG_ATTNUM].isnull)
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("cannot specify both \"%s\" and \"%s\"", "attname", "attnum")));
-		attname = TextDatumGetCString(PG_GETARG_DATUM(ATTARG_ATTNAME));
+		attname = TextDatumGetCString(args[ATTARG_ATTNAME].value);
 		attnum = get_attnum(reloid, attname);
 		/* note that this test covers attisdropped cases too: */
 		if (attnum == InvalidAttrNumber)
@@ -201,9 +201,9 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 					 errmsg("column \"%s\" of relation \"%s\" does not exist",
 							attname, relname)));
 	}
-	else if (!PG_ARGISNULL(ATTARG_ATTNUM))
+	else if (!args[ATTARG_ATTNUM].isnull)
 	{
-		attnum = PG_GETARG_INT16(ATTARG_ATTNUM);
+		attnum = DatumGetInt16(args[ATTARG_ATTNUM].value);
 		attname = get_attname(reloid, attnum, true);
 		/* annoyingly, get_attname doesn't check attisdropped */
 		if (attname == NULL ||
@@ -228,39 +228,39 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 				 errmsg("cannot modify statistics on system column \"%s\"",
 						attname)));
 
-	stats_check_required_arg(fcinfo->args, attarginfo, ATTARG_INHERITED);
-	inherited = PG_GETARG_BOOL(ATTARG_INHERITED);
+	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(fcinfo->args, attarginfo, ATTARG_MOST_COMMON_FREQS))
+	if (!stats_check_arg_array(args, attarginfo, ATTARG_MOST_COMMON_FREQS))
 	{
 		do_mcv = false;
 		result = false;
 	}
 
-	if (!stats_check_arg_array(fcinfo->args, attarginfo, ATTARG_MOST_COMMON_ELEM_FREQS))
+	if (!stats_check_arg_array(args, attarginfo, ATTARG_MOST_COMMON_ELEM_FREQS))
 	{
 		do_mcelem = false;
 		result = false;
 	}
-	if (!stats_check_arg_array(fcinfo->args, attarginfo, ATTARG_ELEM_COUNT_HISTOGRAM))
+	if (!stats_check_arg_array(args, attarginfo, ATTARG_ELEM_COUNT_HISTOGRAM))
 	{
 		do_dechist = false;
 		result = false;
 	}
 
-	if (!stats_check_arg_pair(fcinfo->args, attarginfo,
+	if (!stats_check_arg_pair(args, attarginfo,
 							  ATTARG_MOST_COMMON_VALS, ATTARG_MOST_COMMON_FREQS))
 	{
 		do_mcv = false;
 		result = false;
 	}
 
-	if (!stats_check_arg_pair(fcinfo->args, attarginfo,
+	if (!stats_check_arg_pair(args, attarginfo,
 							  ATTARG_MOST_COMMON_ELEMS,
 							  ATTARG_MOST_COMMON_ELEM_FREQS))
 	{
@@ -268,7 +268,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 		result = false;
 	}
 
-	if (!stats_check_arg_pair(fcinfo->args, attarginfo,
+	if (!stats_check_arg_pair(args, attarginfo,
 							  ATTARG_RANGE_LENGTH_HISTOGRAM,
 							  ATTARG_RANGE_EMPTY_FRAC))
 	{
@@ -344,19 +344,19 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 								 replaces);
 
 	/* if specified, set to argument values */
-	if (!PG_ARGISNULL(ATTARG_NULL_FRAC))
+	if (!args[ATTARG_NULL_FRAC].isnull)
 	{
-		values[Anum_pg_statistic_stanullfrac - 1] = PG_GETARG_DATUM(ATTARG_NULL_FRAC);
+		values[Anum_pg_statistic_stanullfrac - 1] = args[ATTARG_NULL_FRAC].value;
 		replaces[Anum_pg_statistic_stanullfrac - 1] = true;
 	}
-	if (!PG_ARGISNULL(ATTARG_AVG_WIDTH))
+	if (!args[ATTARG_AVG_WIDTH].isnull)
 	{
-		values[Anum_pg_statistic_stawidth - 1] = PG_GETARG_DATUM(ATTARG_AVG_WIDTH);
+		values[Anum_pg_statistic_stawidth - 1] = args[ATTARG_AVG_WIDTH].value;
 		replaces[Anum_pg_statistic_stawidth - 1] = true;
 	}
-	if (!PG_ARGISNULL(ATTARG_N_DISTINCT))
+	if (!args[ATTARG_N_DISTINCT].isnull)
 	{
-		values[Anum_pg_statistic_stadistinct - 1] = PG_GETARG_DATUM(ATTARG_N_DISTINCT);
+		values[Anum_pg_statistic_stadistinct - 1] = args[ATTARG_N_DISTINCT].value;
 		replaces[Anum_pg_statistic_stadistinct - 1] = true;
 	}
 
@@ -364,10 +364,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	if (do_mcv)
 	{
 		bool		converted;
-		Datum		stanumbers = PG_GETARG_DATUM(ATTARG_MOST_COMMON_FREQS);
+		Datum		stanumbers = args[ATTARG_MOST_COMMON_FREQS].value;
 		Datum		stavalues = statatt_build_stavalues("most_common_vals",
 														&array_in_fn,
-														PG_GETARG_DATUM(ATTARG_MOST_COMMON_VALS),
+														args[ATTARG_MOST_COMMON_VALS].value,
 														atttypid, atttypmod,
 														&converted);
 
@@ -407,7 +407,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 		stavalues = statatt_build_stavalues("histogram_bounds",
 											&array_in_fn,
-											PG_GETARG_DATUM(ATTARG_HISTOGRAM_BOUNDS),
+											args[ATTARG_HISTOGRAM_BOUNDS].value,
 											atttypid, atttypmod,
 											&converted);
 
@@ -425,7 +425,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	/* STATISTIC_KIND_CORRELATION */
 	if (do_correlation)
 	{
-		Datum		elems[] = {PG_GETARG_DATUM(ATTARG_CORRELATION)};
+		Datum		elems[] = {args[ATTARG_CORRELATION].value};
 		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
 		Datum		stanumbers = PointerGetDatum(arry);
 
@@ -438,13 +438,13 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	/* STATISTIC_KIND_MCELEM */
 	if (do_mcelem)
 	{
-		Datum		stanumbers = PG_GETARG_DATUM(ATTARG_MOST_COMMON_ELEM_FREQS);
+		Datum		stanumbers = args[ATTARG_MOST_COMMON_ELEM_FREQS].value;
 		bool		converted = false;
 		Datum		stavalues;
 
 		stavalues = statatt_build_stavalues("most_common_elems",
 											&array_in_fn,
-											PG_GETARG_DATUM(ATTARG_MOST_COMMON_ELEMS),
+											args[ATTARG_MOST_COMMON_ELEMS].value,
 											elemtypid, atttypmod,
 											&converted);
 
@@ -462,7 +462,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	/* STATISTIC_KIND_DECHIST */
 	if (do_dechist)
 	{
-		Datum		stanumbers = PG_GETARG_DATUM(ATTARG_ELEM_COUNT_HISTOGRAM);
+		Datum		stanumbers = args[ATTARG_ELEM_COUNT_HISTOGRAM].value;
 
 		statatt_set_slot(values, nulls, replaces,
 						 STATISTIC_KIND_DECHIST,
@@ -484,7 +484,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 		stavalues = statatt_build_stavalues("range_bounds_histogram",
 											&array_in_fn,
-											PG_GETARG_DATUM(ATTARG_RANGE_BOUNDS_HISTOGRAM),
+											args[ATTARG_RANGE_BOUNDS_HISTOGRAM].value,
 											atttypid, atttypmod,
 											&converted);
 
@@ -503,7 +503,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	if (do_range_length_histogram)
 	{
 		/* The anyarray is always a float8[] for this stakind */
-		Datum		elems[] = {PG_GETARG_DATUM(ATTARG_RANGE_EMPTY_FRAC)};
+		Datum		elems[] = {args[ATTARG_RANGE_EMPTY_FRAC].value};
 		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
 		Datum		stanumbers = PointerGetDatum(arry);
 
@@ -512,7 +512,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 		stavalues = statatt_build_stavalues("range_length_histogram",
 											&array_in_fn,
-											PG_GETARG_DATUM(ATTARG_RANGE_LENGTH_HISTOGRAM),
+											args[ATTARG_RANGE_LENGTH_HISTOGRAM].value,
 											FLOAT8OID, 0, &converted);
 
 		if (converted)
@@ -683,7 +683,7 @@ pg_restore_attribute_stats(PG_FUNCTION_ARGS)
 										  attarginfo))
 		result = false;
 
-	if (!attribute_statistics_update(positional_fcinfo))
+	if (!attribute_statistics_update(positional_fcinfo->args))
 		result = false;
 
 	PG_RETURN_BOOL(result);
-- 
2.50.1 (Apple Git-155)



  [application/octet-stream] v2-0008-Change-stats_fill_fcinfo_from_arg_pairs-to-Nullab.patch (6.5K, ../../CADkLM=c9BEuA9vPjE9Wco6+Lz-HSnnmBZrFhi=-yNi0K8tmZug@mail.gmail.com/9-v2-0008-Change-stats_fill_fcinfo_from_arg_pairs-to-Nullab.patch)
  download | inline diff:
From 3e507a2ff2675732126a669e370328d5a0d1296e Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email protected]>
Date: Sun, 28 Jun 2026 17:21:51 -0500
Subject: [PATCH v2 08/13] Change stats_fill_fcinfo_from_arg_pairs to
 NullableDatum array.

Change stats_fill_fcinfo_from_arg_pairs to NullableDatum array, and in
doing so rename to stats_fill_args_from_arg_pairs. Change all callers to
instead initialize an NullableDatum array of the proper size instead of
a full FunctionCallInfoData structure.
---
 src/backend/statistics/attribute_stats.c      | 11 ++++-----
 src/backend/statistics/extended_stats_funcs.c |  9 +++----
 src/backend/statistics/relation_stats.c       | 12 ++++------
 src/backend/statistics/stat_utils.c           | 24 +++++++++----------
 src/include/statistics/stat_utils.h           |  6 ++---
 5 files changed, 26 insertions(+), 36 deletions(-)

diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index a847d053ee4..260e5bb1d48 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -673,17 +673,14 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 Datum
 pg_restore_attribute_stats(PG_FUNCTION_ARGS)
 {
-	LOCAL_FCINFO(positional_fcinfo, ATTARG_NUM_ATTARGS);
+	NullableDatum positional_args[ATTARG_NUM_ATTARGS];
 	bool		result = true;
 
-	InitFunctionCallInfoData(*positional_fcinfo, NULL, ATTARG_NUM_ATTARGS,
-							 InvalidOid, NULL, NULL);
-
-	if (!stats_fill_fcinfo_from_arg_pairs(fcinfo, positional_fcinfo,
-										  attarginfo))
+	if (!stats_fill_args_from_arg_pairs(fcinfo, positional_args,
+										attarginfo))
 		result = false;
 
-	if (!attribute_statistics_update(positional_fcinfo->args))
+	if (!attribute_statistics_update(positional_args))
 		result = false;
 
 	PG_RETURN_BOOL(result);
diff --git a/src/backend/statistics/extended_stats_funcs.c b/src/backend/statistics/extended_stats_funcs.c
index 5e46a7e9678..c807c10dbfe 100644
--- a/src/backend/statistics/extended_stats_funcs.c
+++ b/src/backend/statistics/extended_stats_funcs.c
@@ -1735,16 +1735,13 @@ delete_pg_statistic_ext_data(Oid stxoid, bool inherited)
 Datum
 pg_restore_extended_stats(PG_FUNCTION_ARGS)
 {
-	LOCAL_FCINFO(positional_fcinfo, EXTARG_NUM_EXTARGS);
+	NullableDatum positional_args[EXTARG_NUM_EXTARGS];
 	bool		result = true;
 
-	InitFunctionCallInfoData(*positional_fcinfo, NULL, EXTARG_NUM_EXTARGS,
-							 InvalidOid, NULL, NULL);
-
-	if (!stats_fill_fcinfo_from_arg_pairs(fcinfo, positional_fcinfo, extarginfo))
+	if (!stats_fill_args_from_arg_pairs(fcinfo, positional_args, extarginfo))
 		result = false;
 
-	if (!extended_statistics_update(positional_fcinfo->args))
+	if (!extended_statistics_update(positional_args))
 		result = false;
 
 	PG_RETURN_BOOL(result);
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 1ee23612ae1..213f924a44e 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -225,18 +225,14 @@ pg_clear_relation_stats(PG_FUNCTION_ARGS)
 Datum
 pg_restore_relation_stats(PG_FUNCTION_ARGS)
 {
-	LOCAL_FCINFO(positional_fcinfo, RELARG_NUM_RELARGS);
+	NullableDatum positional_args[RELARG_NUM_RELARGS];
 	bool		result = true;
 
-	InitFunctionCallInfoData(*positional_fcinfo, NULL,
-							 RELARG_NUM_RELARGS,
-							 InvalidOid, NULL, NULL);
-
-	if (!stats_fill_fcinfo_from_arg_pairs(fcinfo, positional_fcinfo,
-										  relarginfo))
+	if (!stats_fill_args_from_arg_pairs(fcinfo, positional_args,
+										relarginfo))
 		result = false;
 
-	if (!relation_statistics_update(positional_fcinfo->args))
+	if (!relation_statistics_update(positional_args))
 		result = false;
 
 	PG_RETURN_BOOL(result);
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index 24b3849fb8b..22f88d2830f 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -339,17 +339,17 @@ statatt_get_index_expr(Relation rel, int attnum)
 
 /*
  * Translate variadic argument pairs from 'pairs_fcinfo' into a
- * 'positional_fcinfo' appropriate for calling relation_statistics_update() or
- * attribute_statistics_update() with positional arguments.
+ * NullableDatum[] appropriate for calling the internal statistics update
+ * functions for relation stats, attribute stats, or extended stats.
  *
- * Caller should have already initialized positional_fcinfo with a size
- * appropriate for calling the intended positional function, and arginfo
- * should also match the intended positional function.
+ * Caller should have already initialized args with a size appropriate for
+ * calling the intended function, and arginfo should also match the intended
+ * function.
  */
 bool
-stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
-								 FunctionCallInfo positional_fcinfo,
-								 struct StatsArgInfo *arginfo)
+stats_fill_args_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
+							   NullableDatum *positional_args,
+							   struct StatsArgInfo *arginfo)
 {
 	Datum	   *args;
 	bool	   *argnulls;
@@ -360,8 +360,8 @@ stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 	/* clear positional args */
 	for (int i = 0; arginfo[i].argname != NULL; i++)
 	{
-		positional_fcinfo->args[i].value = (Datum) 0;
-		positional_fcinfo->args[i].isnull = true;
+		positional_args[i].value = (Datum) 0;
+		positional_args[i].isnull = true;
 	}
 
 	nargs = extract_variadic_args(pairs_fcinfo, 0, true,
@@ -416,8 +416,8 @@ stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 			continue;
 		}
 
-		positional_fcinfo->args[argnum].value = args[i + 1];
-		positional_fcinfo->args[argnum].isnull = false;
+		positional_args[argnum].value = args[i + 1];
+		positional_args[argnum].isnull = false;
 	}
 
 	return result;
diff --git a/src/include/statistics/stat_utils.h b/src/include/statistics/stat_utils.h
index 414636c1cd2..811db3a79ed 100644
--- a/src/include/statistics/stat_utils.h
+++ b/src/include/statistics/stat_utils.h
@@ -38,9 +38,9 @@ extern bool stats_check_arg_pair(const NullableDatum *args,
 extern void RangeVarCallbackForStats(const RangeVar *relation,
 									 Oid relId, Oid oldRelId, void *arg);
 
-extern bool stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
-											 FunctionCallInfo positional_fcinfo,
-											 struct StatsArgInfo *arginfo);
+extern bool stats_fill_args_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
+										   NullableDatum *positional_args,
+										   struct StatsArgInfo *arginfo);
 
 extern void statatt_get_type(Oid reloid, AttrNumber attnum,
 							 Oid *atttypid, int32 *atttypmod,
-- 
2.50.1 (Apple Git-155)



  [application/octet-stream] v2-0007-Make-extended_statistics_update-stop-using-fcinfo.patch (6.7K, ../../CADkLM=c9BEuA9vPjE9Wco6+Lz-HSnnmBZrFhi=-yNi0K8tmZug@mail.gmail.com/10-v2-0007-Make-extended_statistics_update-stop-using-fcinfo.patch)
  download | inline diff:
From 2e0ac34fb30bc01040caeca9323713679b9a6995 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email protected]>
Date: Sun, 28 Jun 2026 16:53:19 -0500
Subject: [PATCH v2 07/13] Make extended_statistics_update stop using fcinfo.

Change the function signature of extended_statistics_update to use a
NullableDatum array instead of a full FunctionCallInfo.
---
 src/backend/statistics/extended_stats_funcs.c | 62 +++++++++----------
 1 file changed, 31 insertions(+), 31 deletions(-)

diff --git a/src/backend/statistics/extended_stats_funcs.c b/src/backend/statistics/extended_stats_funcs.c
index f9a1d287e22..5e46a7e9678 100644
--- a/src/backend/statistics/extended_stats_funcs.c
+++ b/src/backend/statistics/extended_stats_funcs.c
@@ -121,7 +121,7 @@ static const char *extexprargname[EXPRELEM_NUM_EXPRELEMS] =
 	"range_bounds_histogram"
 };
 
-static bool extended_statistics_update(FunctionCallInfo fcinfo);
+static bool extended_statistics_update(const NullableDatum *args);
 
 static HeapTuple get_pg_statistic_ext(Relation pg_stext, Oid nspoid,
 									  const char *stxname);
@@ -311,7 +311,7 @@ upsert_pg_statistic_ext_data(const Datum *values, const bool *nulls,
  * be updated.
  */
 static bool
-extended_statistics_update(FunctionCallInfo fcinfo)
+extended_statistics_update(const NullableDatum *args)
 {
 	char	   *relnspname;
 	char	   *relname;
@@ -356,12 +356,12 @@ extended_statistics_update(FunctionCallInfo fcinfo)
 	 * Therefore, none of the three array values is meaningful unless the
 	 * other two are also present and in sync in terms of array length.
 	 */
-	has.mcv = (!PG_ARGISNULL(EXTARG_MOST_COMMON_VALS) &&
-			   !PG_ARGISNULL(EXTARG_MOST_COMMON_FREQS) &&
-			   !PG_ARGISNULL(EXTARG_MOST_COMMON_BASE_FREQS));
-	has.ndistinct = !PG_ARGISNULL(EXTARG_NDISTINCT);
-	has.dependencies = !PG_ARGISNULL(EXTARG_DEPENDENCIES);
-	has.expressions = !PG_ARGISNULL(EXTARG_EXPRESSIONS);
+	has.mcv = (!args[EXTARG_MOST_COMMON_VALS].isnull &&
+			   !args[EXTARG_MOST_COMMON_FREQS].isnull &&
+			   !args[EXTARG_MOST_COMMON_BASE_FREQS].isnull);
+	has.ndistinct = !args[EXTARG_NDISTINCT].isnull;
+	has.dependencies = !args[EXTARG_DEPENDENCIES].isnull;
+	has.expressions = !args[EXTARG_EXPRESSIONS].isnull;
 
 	if (RecoveryInProgress())
 	{
@@ -373,18 +373,18 @@ extended_statistics_update(FunctionCallInfo fcinfo)
 	}
 
 	/* relation arguments */
-	stats_check_required_arg(fcinfo->args, extarginfo, EXTARG_RELSCHEMA);
-	relnspname = TextDatumGetCString(PG_GETARG_DATUM(EXTARG_RELSCHEMA));
-	stats_check_required_arg(fcinfo->args, extarginfo, EXTARG_RELNAME);
-	relname = TextDatumGetCString(PG_GETARG_DATUM(EXTARG_RELNAME));
+	stats_check_required_arg(args, extarginfo, EXTARG_RELSCHEMA);
+	relnspname = TextDatumGetCString(args[EXTARG_RELSCHEMA].value);
+	stats_check_required_arg(args, extarginfo, EXTARG_RELNAME);
+	relname = TextDatumGetCString(args[EXTARG_RELNAME].value);
 
 	/* extended statistics arguments */
-	stats_check_required_arg(fcinfo->args, extarginfo, EXTARG_STATSCHEMA);
-	nspname = TextDatumGetCString(PG_GETARG_DATUM(EXTARG_STATSCHEMA));
-	stats_check_required_arg(fcinfo->args, extarginfo, EXTARG_STATNAME);
-	stxname = TextDatumGetCString(PG_GETARG_DATUM(EXTARG_STATNAME));
-	stats_check_required_arg(fcinfo->args, extarginfo, EXTARG_INHERITED);
-	inherited = PG_GETARG_BOOL(EXTARG_INHERITED);
+	stats_check_required_arg(args, extarginfo, EXTARG_STATSCHEMA);
+	nspname = TextDatumGetCString(args[EXTARG_STATSCHEMA].value);
+	stats_check_required_arg(args, extarginfo, EXTARG_STATNAME);
+	stxname = TextDatumGetCString(args[EXTARG_STATNAME].value);
+	stats_check_required_arg(args, extarginfo, EXTARG_INHERITED);
+	inherited = DatumGetBool(args[EXTARG_INHERITED].value);
 
 	/*
 	 * First open the relation where we expect to find the statistics.  This
@@ -514,9 +514,9 @@ extended_statistics_update(FunctionCallInfo fcinfo)
 	 */
 	if (!enabled.mcv)
 	{
-		if (!PG_ARGISNULL(EXTARG_MOST_COMMON_VALS) ||
-			!PG_ARGISNULL(EXTARG_MOST_COMMON_FREQS) ||
-			!PG_ARGISNULL(EXTARG_MOST_COMMON_BASE_FREQS))
+		if (!args[EXTARG_MOST_COMMON_VALS].isnull ||
+			!args[EXTARG_MOST_COMMON_FREQS].isnull ||
+			!args[EXTARG_MOST_COMMON_BASE_FREQS].isnull)
 		{
 			ereport(WARNING,
 					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -538,9 +538,9 @@ extended_statistics_update(FunctionCallInfo fcinfo)
 		 * statistics object expects something, something is wrong.  This
 		 * issues a WARNING if a partial input has been provided.
 		 */
-		if (!PG_ARGISNULL(EXTARG_MOST_COMMON_VALS) ||
-			!PG_ARGISNULL(EXTARG_MOST_COMMON_FREQS) ||
-			!PG_ARGISNULL(EXTARG_MOST_COMMON_BASE_FREQS))
+		if (!args[EXTARG_MOST_COMMON_VALS].isnull ||
+			!args[EXTARG_MOST_COMMON_FREQS].isnull ||
+			!args[EXTARG_MOST_COMMON_BASE_FREQS].isnull)
 		{
 			ereport(WARNING,
 					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -655,7 +655,7 @@ extended_statistics_update(FunctionCallInfo fcinfo)
 
 	if (has.ndistinct)
 	{
-		Datum		ndistinct_datum = PG_GETARG_DATUM(EXTARG_NDISTINCT);
+		Datum		ndistinct_datum = args[EXTARG_NDISTINCT].value;
 		bytea	   *data = DatumGetByteaPP(ndistinct_datum);
 		MVNDistinct *ndistinct = statext_ndistinct_deserialize(data);
 
@@ -674,7 +674,7 @@ extended_statistics_update(FunctionCallInfo fcinfo)
 
 	if (has.dependencies)
 	{
-		Datum		dependencies_datum = PG_GETARG_DATUM(EXTARG_DEPENDENCIES);
+		Datum		dependencies_datum = args[EXTARG_DEPENDENCIES].value;
 		bytea	   *data = DatumGetByteaPP(dependencies_datum);
 		MVDependencies *dependencies = statext_dependencies_deserialize(data);
 
@@ -696,9 +696,9 @@ extended_statistics_update(FunctionCallInfo fcinfo)
 		Datum		datum;
 		bool		val_ok = false;
 
-		datum = import_mcv(PG_GETARG_ARRAYTYPE_P(EXTARG_MOST_COMMON_VALS),
-						   PG_GETARG_ARRAYTYPE_P(EXTARG_MOST_COMMON_FREQS),
-						   PG_GETARG_ARRAYTYPE_P(EXTARG_MOST_COMMON_BASE_FREQS),
+		datum = import_mcv(DatumGetArrayTypeP(args[EXTARG_MOST_COMMON_VALS].value),
+						   DatumGetArrayTypeP(args[EXTARG_MOST_COMMON_FREQS].value),
+						   DatumGetArrayTypeP(args[EXTARG_MOST_COMMON_BASE_FREQS].value),
 						   atttypids, atttypmods, atttypcolls, numattrs,
 						   &val_ok);
 
@@ -733,7 +733,7 @@ extended_statistics_update(FunctionCallInfo fcinfo)
 								   &atttypids[numattnums],
 								   &atttypmods[numattnums],
 								   &atttypcolls[numattnums],
-								   PG_GETARG_JSONB_P(EXTARG_EXPRESSIONS),
+								   DatumGetJsonbP(args[EXTARG_EXPRESSIONS].value),
 								   &ok);
 
 		table_close(pgsd, RowExclusiveLock);
@@ -1744,7 +1744,7 @@ pg_restore_extended_stats(PG_FUNCTION_ARGS)
 	if (!stats_fill_fcinfo_from_arg_pairs(fcinfo, positional_fcinfo, extarginfo))
 		result = false;
 
-	if (!extended_statistics_update(positional_fcinfo))
+	if (!extended_statistics_update(positional_fcinfo->args))
 		result = false;
 
 	PG_RETURN_BOOL(result);
-- 
2.50.1 (Apple Git-155)



  [application/octet-stream] v2-0010-Add-relation_statistics_update-refactor-update_re.patch (12.5K, ../../CADkLM=c9BEuA9vPjE9Wco6+Lz-HSnnmBZrFhi=-yNi0K8tmZug@mail.gmail.com/11-v2-0010-Add-relation_statistics_update-refactor-update_re.patch)
  download | inline diff:
From 34cc6ac8697c368b23583c18e9172747218b8b02 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email protected]>
Date: Sun, 28 Jun 2026 21:51:56 -0500
Subject: [PATCH v2 10/13] Add relation_statistics_update, refactor
 update_relstats.

Introduce a new enum relation_stats_argnum, which is a subset of
relation_args_argnum but contains only the statistical values.

Modify update_relstats to take a Relation argument, and index the
NullableDatum array by the new enum relation_stats_argnum. All
processing and validation of non-statistical values like schema and
relname, as well as checking for recovery mode and acquiring the lock on
the relation are now handled in the calling functions
pg_restore_relation_stats and pg_clear_relation_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 relation_statistics_update which takes a Relation
argument and arrays of isnull and cstring arguments, which will be
translated into the Datum values required by update_relstats().

The end result is that all three user-facing functions are able to call
the same update_relstats().
---
 src/backend/statistics/relation_stats.c | 244 +++++++++++++++++++-----
 src/include/statistics/relation_stats.h |  30 +++
 2 files changed, 223 insertions(+), 51 deletions(-)
 create mode 100644 src/include/statistics/relation_stats.h

diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index d4459981ebc..21baa0abb55 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -21,6 +21,7 @@
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
 #include "nodes/makefuncs.h"
+#include "statistics/relation_stats.h"
 #include "statistics/stat_utils.h"
 #include "utils/builtins.h"
 #include "utils/fmgroids.h"
@@ -56,18 +57,15 @@ static struct StatsArgInfo relarginfo[] =
 	[RELARG_NUM_RELARGS] = {0}
 };
 
-static bool update_relstats(const NullableDatum *args);
+static bool update_relstats(Relation rel, const NullableDatum *args);
 
 /*
  * Internal function for modifying statistics for a relation.
  */
 static bool
-update_relstats(const NullableDatum *args)
+update_relstats(Relation rel, const NullableDatum *args)
 {
 	bool		result = true;
-	char	   *nspname;
-	char	   *relname;
-	Oid			reloid;
 	Relation	crel;
 	BlockNumber relpages = 0;
 	bool		update_relpages = false;
@@ -83,33 +81,16 @@ update_relstats(const NullableDatum *args)
 	Datum		values[4] = {0};
 	bool		nulls[4] = {0};
 	int			nreplaces = 0;
-	Oid			locked_table = InvalidOid;
-
-	stats_check_required_arg(args, relarginfo, RELARG_SCHEMA);
-	stats_check_required_arg(args, relarginfo, RELARG_RELNAME);
-
-	nspname = TextDatumGetCString(args[RELARG_SCHEMA].value);
-	relname = TextDatumGetCString(args[RELARG_RELNAME].value);
-
-	if (RecoveryInProgress())
-		ereport(ERROR,
-				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
-				 errmsg("recovery is in progress"),
-				 errhint("Statistics cannot be modified during recovery.")));
-
-	reloid = RangeVarGetRelidExtended(makeRangeVar(nspname, relname, -1),
-									  ShareUpdateExclusiveLock, 0,
-									  RangeVarCallbackForStats, &locked_table);
 
-	if (!args[RELARG_RELPAGES].isnull)
+	if (!args[RELSTAT_RELPAGES].isnull)
 	{
-		relpages = DatumGetUInt32(args[RELARG_RELPAGES].value);
+		relpages = DatumGetUInt32(args[RELSTAT_RELPAGES].value);
 		update_relpages = true;
 	}
 
-	if (!args[RELARG_RELTUPLES].isnull)
+	if (!args[RELSTAT_RELTUPLES].isnull)
 	{
-		reltuples = DatumGetFloat4(args[RELARG_RELTUPLES].value);
+		reltuples = DatumGetFloat4(args[RELSTAT_RELTUPLES].value);
 		if (reltuples < -1.0)
 		{
 			ereport(WARNING,
@@ -121,15 +102,15 @@ update_relstats(const NullableDatum *args)
 			update_reltuples = true;
 	}
 
-	if (!args[RELARG_RELALLVISIBLE].isnull)
+	if (!args[RELSTAT_RELALLVISIBLE].isnull)
 	{
-		relallvisible = DatumGetUInt32(args[RELARG_RELALLVISIBLE].value);
+		relallvisible = DatumGetUInt32(args[RELSTAT_RELALLVISIBLE].value);
 		update_relallvisible = true;
 	}
 
-	if (!args[RELARG_RELALLFROZEN].isnull)
+	if (!args[RELSTAT_RELALLFROZEN].isnull)
 	{
-		relallfrozen = DatumGetUInt32(args[RELARG_RELALLFROZEN].value);
+		relallfrozen = DatumGetUInt32(args[RELSTAT_RELALLFROZEN].value);
 		update_relallfrozen = true;
 	}
 
@@ -139,9 +120,9 @@ update_relstats(const NullableDatum *args)
 	 */
 	crel = table_open(RelationRelationId, RowExclusiveLock);
 
-	ctup = SearchSysCache1(RELOID, ObjectIdGetDatum(reloid));
+	ctup = SearchSysCache1(RELOID, ObjectIdGetDatum(RelationGetRelid(rel)));
 	if (!HeapTupleIsValid(ctup))
-		elog(ERROR, "pg_class entry for relid %u not found", reloid);
+		elog(ERROR, "pg_class entry for relid %u not found", RelationGetRelid(rel));
 
 	pgcform = (Form_pg_class) GETSTRUCT(ctup);
 
@@ -201,24 +182,55 @@ update_relstats(const NullableDatum *args)
 Datum
 pg_clear_relation_stats(PG_FUNCTION_ARGS)
 {
-	LOCAL_FCINFO(newfcinfo, 6);
-
-	InitFunctionCallInfoData(*newfcinfo, NULL, 6, InvalidOid, NULL, NULL);
-
-	newfcinfo->args[0].value = PG_GETARG_DATUM(0);
-	newfcinfo->args[0].isnull = PG_ARGISNULL(0);
-	newfcinfo->args[1].value = PG_GETARG_DATUM(1);
-	newfcinfo->args[1].isnull = PG_ARGISNULL(1);
-	newfcinfo->args[2].value = UInt32GetDatum(0);
-	newfcinfo->args[2].isnull = false;
-	newfcinfo->args[3].value = Float4GetDatum(-1.0);
-	newfcinfo->args[3].isnull = false;
-	newfcinfo->args[4].value = UInt32GetDatum(0);
-	newfcinfo->args[4].isnull = false;
-	newfcinfo->args[5].value = UInt32GetDatum(0);
-	newfcinfo->args[5].isnull = false;
-
-	update_relstats(newfcinfo->args);
+	NullableDatum positional_args[RELARG_NUM_RELARGS];
+	NullableDatum stats[RELSTAT_NUM_RELSTATS];
+	char	   *nspname;
+	char	   *relname;
+	Oid			reloid;
+	Oid			locked_table = InvalidOid;
+	Relation	rel;
+
+	/*
+	 * Fill out just enough of positional_args to do the same required checks
+	 * as pg_restore_relation_stats
+	 */
+	positional_args[RELARG_SCHEMA].isnull = PG_ARGISNULL(0);
+	positional_args[RELARG_SCHEMA].value = PG_GETARG_DATUM(0);
+	positional_args[RELARG_RELNAME].isnull = PG_ARGISNULL(1);
+	positional_args[RELARG_RELNAME].value = PG_GETARG_DATUM(1);
+
+	stats_check_required_arg(positional_args, relarginfo, RELARG_SCHEMA);
+	stats_check_required_arg(positional_args, relarginfo, RELARG_RELNAME);
+
+	nspname = TextDatumGetCString(positional_args[RELARG_SCHEMA].value);
+	relname = TextDatumGetCString(positional_args[RELARG_RELNAME].value);
+
+	if (RecoveryInProgress())
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("recovery is in progress"),
+				 errhint("Statistics cannot be modified during recovery.")));
+
+	reloid = RangeVarGetRelidExtended(makeRangeVar(nspname, relname, -1),
+									  ShareUpdateExclusiveLock, 0,
+									  RangeVarCallbackForStats, &locked_table);
+
+	stats[RELSTAT_RELPAGES].isnull = false;
+	stats[RELSTAT_RELPAGES].value = UInt32GetDatum(0);
+
+	stats[RELSTAT_RELTUPLES].isnull = false;
+	stats[RELSTAT_RELTUPLES].value = Float4GetDatum(-1.0);
+
+	stats[RELSTAT_RELALLVISIBLE].isnull = false;
+	stats[RELSTAT_RELALLVISIBLE].value = UInt32GetDatum(0);
+
+	stats[RELSTAT_RELALLFROZEN].isnull = false;
+	stats[RELSTAT_RELALLFROZEN].value = UInt32GetDatum(0);
+
+	rel = relation_open(reloid, NoLock);
+	update_relstats(rel, stats);
+	relation_close(rel, NoLock);
+
 	PG_RETURN_VOID();
 }
 
@@ -226,14 +238,144 @@ Datum
 pg_restore_relation_stats(PG_FUNCTION_ARGS)
 {
 	NullableDatum positional_args[RELARG_NUM_RELARGS];
+	NullableDatum stats[RELSTAT_NUM_RELSTATS];
+
+	char	   *nspname;
+	char	   *relname;
+	Oid			reloid;
+	Oid			locked_table = InvalidOid;
+	Relation	rel;
 	bool		result = true;
 
 	if (!stats_fill_args_from_arg_pairs(fcinfo, positional_args,
 										relarginfo))
 		result = false;
 
-	if (!update_relstats(positional_args))
+	stats_check_required_arg(positional_args, relarginfo, RELARG_SCHEMA);
+	stats_check_required_arg(positional_args, relarginfo, RELARG_RELNAME);
+
+	nspname = TextDatumGetCString(positional_args[RELARG_SCHEMA].value);
+	relname = TextDatumGetCString(positional_args[RELARG_RELNAME].value);
+
+	if (RecoveryInProgress())
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("recovery is in progress"),
+				 errhint("Statistics cannot be modified during recovery.")));
+
+	reloid = RangeVarGetRelidExtended(makeRangeVar(nspname, relname, -1),
+									  ShareUpdateExclusiveLock, 0,
+									  RangeVarCallbackForStats, &locked_table);
+
+	/* Map RELARGs to RELSTATs */
+	stats[RELSTAT_RELPAGES].isnull = positional_args[RELARG_RELPAGES].isnull;
+	stats[RELSTAT_RELPAGES].value = positional_args[RELARG_RELPAGES].value;
+
+	stats[RELSTAT_RELTUPLES].isnull = positional_args[RELARG_RELTUPLES].isnull;
+	stats[RELSTAT_RELTUPLES].value = positional_args[RELARG_RELTUPLES].value;
+
+	stats[RELSTAT_RELALLVISIBLE].isnull = positional_args[RELARG_RELALLVISIBLE].isnull;
+	stats[RELSTAT_RELALLVISIBLE].value = positional_args[RELARG_RELALLVISIBLE].value;
+
+	stats[RELSTAT_RELALLFROZEN].isnull = positional_args[RELARG_RELALLFROZEN].isnull;
+	stats[RELSTAT_RELALLFROZEN].value = positional_args[RELARG_RELALLFROZEN].value;
+
+	rel = relation_open(reloid, NoLock);
+
+	if (!update_relstats(rel, stats))
 		result = false;
 
+	relation_close(rel, NoLock);
+
 	PG_RETURN_BOOL(result);
 }
+
+/*
+ * Convenience routine to parse BlockNumber values, and emit a warning
+ * on parse errors.
+ */
+static void
+str_to_blocknumber(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];
+		BlockNumber result;
+		ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+		if (s == NULL)
+			elog(ERROR, "value is null but flag is non-null");
+
+		result = uint32in_subr(s, NULL, "BlockNumber", (Node *) &escontext);
+
+		if (escontext.error_occurred)
+		{
+			escontext.error_data->elevel = WARNING;
+			ThrowErrorData(escontext.error_data);
+			FreeErrorData(escontext.error_data);
+			return;
+		}
+		else
+		{
+			stats[statnum].isnull = false;
+			stats[statnum].value = UInt32GetDatum(result);
+		}
+	}
+}
+
+/*
+ * 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;
+		}
+	}
+}
+
+/*
+ * Update statistics for an already opened Relation with a lock level of at least
+ * ShareUpdateExclusiveLock.
+ */
+bool
+relation_statistics_update(Relation rel, const bool *isnull, const char **values)
+{
+	NullableDatum stats[RELSTAT_NUM_RELSTATS];
+
+	str_to_blocknumber(stats, RELSTAT_RELPAGES, isnull, values);
+	str_to_float(stats, RELSTAT_RELTUPLES, isnull, values);
+	str_to_blocknumber(stats, RELSTAT_RELALLVISIBLE, isnull, values);
+	str_to_blocknumber(stats, RELSTAT_RELALLFROZEN, isnull, values);
+
+	return update_relstats(rel, stats);
+}
diff --git a/src/include/statistics/relation_stats.h b/src/include/statistics/relation_stats.h
new file mode 100644
index 00000000000..56ce7ef3e66
--- /dev/null
+++ b/src/include/statistics/relation_stats.h
@@ -0,0 +1,30 @@
+/*-------------------------------------------------------------------------
+ *
+ * relation_stats.h
+ *    Functions for the internal manipulation of relation statistics.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/statistics/relation_stats.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef RELATION_STATS_H
+
+#include "access/genam.h"
+
+enum relation_stats_argnum
+{
+	RELSTAT_RELPAGES,
+	RELSTAT_RELTUPLES,
+	RELSTAT_RELALLVISIBLE,
+	RELSTAT_RELALLFROZEN,
+	RELSTAT_NUM_RELSTATS
+};
+
+extern bool relation_statistics_update(Relation rel, const bool *isnull,
+									   const char **values);
+
+#define RELATION_STATS_H
+#endif
-- 
2.50.1 (Apple Git-155)



  [application/octet-stream] v2-0009-Rename-relation_statistics_update-and-relation_st.patch (2.1K, ../../CADkLM=c9BEuA9vPjE9Wco6+Lz-HSnnmBZrFhi=-yNi0K8tmZug@mail.gmail.com/12-v2-0009-Rename-relation_statistics_update-and-relation_st.patch)
  download | inline diff:
From 165fce8411908ca3e61ecb9a072a901ee6297db8 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email protected]>
Date: Sun, 28 Jun 2026 21:39:15 -0500
Subject: [PATCH v2 09/13] Rename relation_statistics_update and
 relation_stats_argnum.

Rename relation_statistics_update to update_relstats. This allows us to
create a public function of that same name.

Rename relation_stats_argnum to relation_args_argnum, because it covers
all parameters of pg_restore_relation_stats, not just the statistical
ones. This makes way for a publicly visible enum that has only
statistical ones.
---
 src/backend/statistics/relation_stats.c | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 213f924a44e..d4459981ebc 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -31,10 +31,10 @@
 
 /*
  * Positional argument numbers, names, and types for
- * relation_statistics_update().
+ * update_relstats().
  */
 
-enum relation_stats_argnum
+enum relation_args_argnum
 {
 	RELARG_SCHEMA = 0,
 	RELARG_RELNAME,
@@ -56,13 +56,13 @@ static struct StatsArgInfo relarginfo[] =
 	[RELARG_NUM_RELARGS] = {0}
 };
 
-static bool relation_statistics_update(const NullableDatum *args);
+static bool update_relstats(const NullableDatum *args);
 
 /*
  * Internal function for modifying statistics for a relation.
  */
 static bool
-relation_statistics_update(const NullableDatum *args)
+update_relstats(const NullableDatum *args)
 {
 	bool		result = true;
 	char	   *nspname;
@@ -218,7 +218,7 @@ pg_clear_relation_stats(PG_FUNCTION_ARGS)
 	newfcinfo->args[5].value = UInt32GetDatum(0);
 	newfcinfo->args[5].isnull = false;
 
-	relation_statistics_update(newfcinfo->args);
+	update_relstats(newfcinfo->args);
 	PG_RETURN_VOID();
 }
 
@@ -232,7 +232,7 @@ pg_restore_relation_stats(PG_FUNCTION_ARGS)
 										relarginfo))
 		result = false;
 
-	if (!relation_statistics_update(positional_args))
+	if (!update_relstats(positional_args))
 		result = false;
 
 	PG_RETURN_BOOL(result);
-- 
2.50.1 (Apple Git-155)



  [application/octet-stream] v2-0012-Add-attribute_statistics_update-refactor-update_a.patch (23.3K, ../../CADkLM=c9BEuA9vPjE9Wco6+Lz-HSnnmBZrFhi=-yNi0K8tmZug@mail.gmail.com/13-v2-0012-Add-attribute_statistics_update-refactor-update_a.patch)
  download | inline diff:
From 4b832c5c733f6a919547287c9faf15ffa45d23c4 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email protected]>
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)



  [application/octet-stream] v2-0013-Replace-statistics-import-SPI-calls-with-new-impo.patch (22.9K, ../../CADkLM=c9BEuA9vPjE9Wco6+Lz-HSnnmBZrFhi=-yNi0K8tmZug@mail.gmail.com/14-v2-0013-Replace-statistics-import-SPI-calls-with-new-impo.patch)
  download | inline diff:
From 188b5f7d80cfa5af8affd82f6579591d3f6d43dc Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email protected]>
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)



  [application/octet-stream] v2-0011-Rename-attribute_statistics_update.patch (2.6K, ../../CADkLM=c9BEuA9vPjE9Wco6+Lz-HSnnmBZrFhi=-yNi0K8tmZug@mail.gmail.com/15-v2-0011-Rename-attribute_statistics_update.patch)
  download | inline diff:
From dee30704ded4a30a34d8e3e7069b51693f1b9f75 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email protected]>
Date: Sun, 28 Jun 2026 22:10:06 -0500
Subject: [PATCH v2 11/13] Rename attribute_statistics_update.

Rename attribute_statistics_update and attribute_stats_argnum.  This
allows us to create a public function of the same name but a slightly
different purpose.

Rename attribute_stats_argnum to attribute_args_argnum and
clear_attribute_stats_argnum to clear_attribute_args_argnum because they
cover all parameters for pg_restore_attribute_stats and
pg_clear_attribute_stats respectively, not just the statistical ones.

These changes make way for publicly visible enums of the same names that
contain only the statistical columns.
---
 src/backend/statistics/attribute_stats.c | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 260e5bb1d48..24c3204e837 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -32,10 +32,10 @@
 
 /*
  * Positional argument numbers, names, and types for
- * attribute_statistics_update() and pg_restore_attribute_stats().
+ * update_attstats() and pg_restore_attribute_stats().
  */
 
-enum attribute_stats_argnum
+enum attribute_args_argnum
 {
 	ATTARG_ATTRELSCHEMA = 0,
 	ATTARG_ATTRELNAME,
@@ -86,7 +86,7 @@ static struct StatsArgInfo attarginfo[] =
  * pg_clear_attribute_stats().
  */
 
-enum clear_attribute_stats_argnum
+enum clear_attribute_args_argnum
 {
 	C_ATTARG_ATTRELSCHEMA = 0,
 	C_ATTARG_ATTRELNAME,
@@ -104,7 +104,7 @@ static struct StatsArgInfo cleararginfo[] =
 	[C_ATTARG_NUM_ATTARGS] = {0}
 };
 
-static bool attribute_statistics_update(const NullableDatum *args);
+static bool update_attstats(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,7 +126,7 @@ static bool delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit);
  * and other statistic kinds may still be updated.
  */
 static bool
-attribute_statistics_update(const NullableDatum *args)
+update_attstats(const NullableDatum *args)
 {
 	char	   *nspname;
 	char	   *relname;
@@ -680,7 +680,7 @@ pg_restore_attribute_stats(PG_FUNCTION_ARGS)
 										attarginfo))
 		result = false;
 
-	if (!attribute_statistics_update(positional_args))
+	if (!update_attstats(positional_args))
 		result = false;
 
 	PG_RETURN_BOOL(result);
-- 
2.50.1 (Apple Git-155)



^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-17 10:23             ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-17 16:00               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-18 10:42                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-18 21:23                   ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-19 13:45                     ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-20 04:11                       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-22 19:08                         ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-23 12:10                           ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-23 15:53                             ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-29 08:14                               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
@ 2026-06-29 11:50                                 ` Etsuro Fujita <[email protected]>
  2026-06-29 18:17                                   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  0 siblings, 1 reply; 33+ messages in thread

From: Etsuro Fujita @ 2026-06-29 11:50 UTC (permalink / raw)
  To: Corey Huinker <[email protected]>; +Cc: Robert Haas <[email protected]>; pgsql-hackers

On Mon, Jun 29, 2026 at 5:14 PM Corey Huinker <[email protected]> wrote:

>> Requiring Datums simplifies things greatly inside the existing functions, but pushes that complexity to the caller. My first proposal was to keep complexity to an absolute minimum on the caller side, but your comments make me think there's considerable tolerance for more complexity on the caller side.
>
>
> I've had some time to look this over, and it's pretty clear that we don't actually need the whole positional FunctionCallInfo, we just need the fcinfo->args of it. We also need the ability to pass statistical values along with the values that comprise the key of the relation/attribute/object to be modified.
>
> There are important differences in the parameters needed by the different types of functions. The pg_restore_*() SQL function calls need to identify the schema+relname of the relation being modified, and already have all of the values as typed Datums, whereas the internal API calls already have an identified and locked open Relation, but all of the statistical parameters are just cstrings.
>
> The solution I chose was to create common functions that take a relation object and an array of just the statistical parameters. This requires the pg_restore_* functions to resolve and lock the relation themselves, and then carry forward just the subset of parameters that are statistics (right types, but wrong number of arguments). Conversely, the internal API functions need to map their array of cstring values to the correctly typed Datums (wrong types, but right number of arguments in the right order).
>
> Attached is v2. The patches are broken down into very small bites to aid digestion and to confirm that tests pass after each comparatively minor change.

Thanks for doing that work!

I like this refactoring, but while it's rather mechanical, it's pretty
large, so I think it's too late to do the refactoring at this time
just before the beta 2 release.  So I'd vote for going with your
v1-0001 and v1-0002 and doing the refactoring in v20.  As mentioned by
Robert, I don't think it's good to call LOCAL_FCINFO() in
import_relation_statistics() and import_attribute_statistics() to call
the guts of those functions either, but that is *consistent* with the
existing way pg_restore_relation_stats() and
pg_restore_attribute_stats() do that, so that is actually not that
bad.  Also, as you mentioned above, it's inefficient for the new API
functions to lock an already-locked relation, and validate an
already-validated attname/attnum, but I think it would be negligible.

Best regards,
Etsuro Fujita






^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-17 10:23             ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-17 16:00               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-18 10:42                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-18 21:23                   ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-19 13:45                     ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-20 04:11                       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-22 19:08                         ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-23 12:10                           ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-23 15:53                             ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-29 08:14                               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-29 11:50                                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
@ 2026-06-29 18:17                                   ` Robert Haas <[email protected]>
  2026-06-30 01:10                                     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  0 siblings, 1 reply; 33+ messages in thread

From: Robert Haas @ 2026-06-29 18:17 UTC (permalink / raw)
  To: Etsuro Fujita <[email protected]>; +Cc: Corey Huinker <[email protected]>; pgsql-hackers

On Mon, Jun 29, 2026 at 7:50 AM Etsuro Fujita <[email protected]> wrote:
> I like this refactoring, but while it's rather mechanical, it's pretty
> large, so I think it's too late to do the refactoring at this time
> just before the beta 2 release.  So I'd vote for going with your
> v1-0001 and v1-0002 and doing the refactoring in v20.  As mentioned by
> Robert, I don't think it's good to call LOCAL_FCINFO() in
> import_relation_statistics() and import_attribute_statistics() to call
> the guts of those functions either, but that is *consistent* with the
> existing way pg_restore_relation_stats() and
> pg_restore_attribute_stats() do that, so that is actually not that
> bad.  Also, as you mentioned above, it's inefficient for the new API
> functions to lock an already-locked relation, and validate an
> already-validated attname/attnum, but I think it would be negligible.

Fujita-san, is it then your plan to get those two patches committed?

-- 
Robert Haas
EDB: http://www.enterprisedb.com






^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-17 10:23             ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-17 16:00               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-18 10:42                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-18 21:23                   ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-19 13:45                     ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-20 04:11                       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-22 19:08                         ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-23 12:10                           ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-23 15:53                             ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-29 08:14                               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-29 11:50                                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-29 18:17                                   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
@ 2026-06-30 01:10                                     ` Corey Huinker <[email protected]>
  2026-06-30 12:29                                       ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  0 siblings, 1 reply; 33+ messages in thread

From: Corey Huinker @ 2026-06-30 01:10 UTC (permalink / raw)
  To: Robert Haas <[email protected]>; +Cc: Etsuro Fujita <[email protected]>; pgsql-hackers

On Mon, Jun 29, 2026 at 2:17 PM Robert Haas <[email protected]> wrote:

> On Mon, Jun 29, 2026 at 7:50 AM Etsuro Fujita <[email protected]>
> wrote:
> > I like this refactoring, but while it's rather mechanical, it's pretty
> > large, so I think it's too late to do the refactoring at this time
> > just before the beta 2 release.  So I'd vote for going with your
> > v1-0001 and v1-0002 and doing the refactoring in v20.  As mentioned by
> > Robert, I don't think it's good to call LOCAL_FCINFO() in
> > import_relation_statistics() and import_attribute_statistics() to call
> > the guts of those functions either, but that is *consistent* with the
> > existing way pg_restore_relation_stats() and
> > pg_restore_attribute_stats() do that, so that is actually not that
> > bad.  Also, as you mentioned above, it's inefficient for the new API
> > functions to lock an already-locked relation, and validate an
> > already-validated attname/attnum, but I think it would be negligible.


> Fujita-san, is it then your plan to get those two patches committed?


Are we ok with changing the functions that postgres_fdw makes from v19 to
v20? Because what I put in the v2 patch does not match the v1 patch. If we
are NOT ok with that, then we'd need to:

1. rename the two relation_stats_argnum -> relation_args_argnum and
attribute_stats_argnum -> attribute_args_argnum out of the way of the ones
created in relation_stats.h and attribute_stats.h. The actual enumeration
values can stay the same, as there are no conflicts there, just ambiguity
about which set of values they belong to.
2. rename the existing static functions relation_statistics_update ->
update_relstats and attribute_statistics_update -> update_relstats to make
way for the public functions of the same names.
3. Create new relation_statistics_update and attribute_statistics_update,
with the isnull/values, and have those fcinfo-invoke the respective
pg_restore functions just like v1 does.

That would at least make the user API consistent in v19 vs v20.

I'm on vacation this week, but time is short for beta2, so I'll make an
exception to get the above into beta2. Let me know if you want me to write
that up.


^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-17 10:23             ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-17 16:00               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-18 10:42                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-18 21:23                   ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-19 13:45                     ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-20 04:11                       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-22 19:08                         ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-23 12:10                           ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-23 15:53                             ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-29 08:14                               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-29 11:50                                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-29 18:17                                   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-30 01:10                                     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
@ 2026-06-30 12:29                                       ` Etsuro Fujita <[email protected]>
  2026-06-30 15:55                                         ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  0 siblings, 1 reply; 33+ messages in thread

From: Etsuro Fujita @ 2026-06-30 12:29 UTC (permalink / raw)
  To: Corey Huinker <[email protected]>; +Cc: Robert Haas <[email protected]>; pgsql-hackers

On Tue, Jun 30, 2026 at 10:10 AM Corey Huinker <[email protected]> wrote:
> On Mon, Jun 29, 2026 at 2:17 PM Robert Haas <[email protected]> wrote:
>> On Mon, Jun 29, 2026 at 7:50 AM Etsuro Fujita <[email protected]> wrote:
>> > I like this refactoring, but while it's rather mechanical, it's pretty
>> > large, so I think it's too late to do the refactoring at this time
>> > just before the beta 2 release.  So I'd vote for going with your
>> > v1-0001 and v1-0002 and doing the refactoring in v20.  As mentioned by
>> > Robert, I don't think it's good to call LOCAL_FCINFO() in
>> > import_relation_statistics() and import_attribute_statistics() to call
>> > the guts of those functions either, but that is *consistent* with the
>> > existing way pg_restore_relation_stats() and
>> > pg_restore_attribute_stats() do that, so that is actually not that
>> > bad.  Also, as you mentioned above, it's inefficient for the new API
>> > functions to lock an already-locked relation, and validate an
>> > already-validated attname/attnum, but I think it would be negligible.
>>
>> Fujita-san, is it then your plan to get those two patches committed?

Yes, it is.

> Are we ok with changing the functions that postgres_fdw makes from v19 to v20? Because what I put in the v2 patch does not match the v1 patch. If we are NOT ok with that, then we'd need to:
>
> 1. rename the two relation_stats_argnum -> relation_args_argnum and attribute_stats_argnum -> attribute_args_argnum out of the way of the ones created in relation_stats.h and attribute_stats.h. The actual enumeration values can stay the same, as there are no conflicts there, just ambiguity about which set of values they belong to.
> 2. rename the existing static functions relation_statistics_update -> update_relstats and attribute_statistics_update -> update_relstats to make way for the public functions of the same names.
> 3. Create new relation_statistics_update and attribute_statistics_update, with the isnull/values, and have those fcinfo-invoke the respective pg_restore functions just like v1 does.
>
> That would at least make the user API consistent in v19 vs v20.
>
> I'm on vacation this week, but time is short for beta2, so I'll make an exception to get the above into beta2. Let me know if you want me to write that up.

Thanks!

Here is an updated patch, which merges your v1-0001 and v1-0002.  One
notable change I made to your version is the signatures of
import_relation_statistics() and import_attribute_statistics().  As
mentioned upthread, the proposed signatures of these functions would
be too tailored for postgres_fdw, and might not be useful for other
FDWs, so I changed them to have NullableDatum arguments for stats-data
inputs like this:

bool
import_relation_statistics(Relation rel,
                           NullableDatum *relpages,
                           NullableDatum *reltuples,
                           NullableDatum *relallvisible,
                           NullableDatum *relallfrozen)

bool
import_attribute_statistics(Relation rel, AttrNumber attnum, bool inherited,
                            NullableDatum *null_frac,
                            NullableDatum *avg_width,
                            NullableDatum *n_distinct,
                            NullableDatum *most_common_vals,
                            NullableDatum *most_common_freqs,
                            NullableDatum *histogram_bounds,
                            NullableDatum *correlation,
                            NullableDatum *most_common_elems,
                            NullableDatum *most_common_elem_freqs,
                            NullableDatum *elem_count_histogram,
                            NullableDatum *range_length_histogram,
                            NullableDatum *range_empty_frac,
                            NullableDatum *range_bounds_histogram)

As postgresImportForeignStatistics() is provided for FDWs that want to
calculate statistics remotely, so I'm assuming that they have enough
knowledge about the stats-data structures.  I think we could instead
use the values/isnull arrays as proposed in your v2, but the problem
about doing so is error messages in functions in stat_utils.c like
this:

        ereport(WARNING,
                (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
                 errmsg("argument \"%s\" must not be a multidimensional array",
                        arginfo[argnum].argname)));

Note that the message uses the word "argument", so I think it's better
for the new functions as well to have individual arguments for
stats-data inputs.

Also, I removed the attname argument from
import_attribute_statistics(), to 1) make the signature (and the
internal processing) simple and 2) match it to
delete_attribute_statistics().

In relation to this change, I moved helper functions set_XXX_arg()
that you added to relation_stats.c and attribute_stats.c to
postgres_fdw.c.  The helper functions had soft error handling, but in
the postgres_fdw use, there is no need for that, so I removed that
handling as well.

What do you think?  If there is no problem with this change, we
wouldn't need to worry about the stability of postgres_fdw (and other
FDWs) in v20.

Best regards,
Etsuro Fujita


Attachments:

  [application/octet-stream] Remove-SPI-from-stat-import-in-postgres-fdw-efujita.patch (25.8K, ../../CAPmGK14dY-Cex2rc08TinQjHR4Q1L6nqPBfdEBRZusP7n_TZSA@mail.gmail.com/2-Remove-SPI-from-stat-import-in-postgres-fdw-efujita.patch)
  download | inline diff:
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index e90289e4ab1..51d09b27267 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -12927,6 +12927,43 @@ 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_ftable's stats should now exactly match dtest_table's
+-- 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)
+
+-- 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)
+
+-- 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..b171d26b532 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"
@@ -46,6 +45,7 @@
 #include "storage/latch.h"
 #include "utils/builtins.h"
 #include "utils/float.h"
+#include "utils/fmgroids.h"
 #include "utils/guc.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
@@ -335,7 +335,6 @@ typedef struct
 {
 	PGresult   *rel;
 	PGresult   *att;
-	int			server_version_num;
 } RemoteStatsResults;
 
 /* Column order in relation stats query */
@@ -367,136 +366,6 @@ enum AttStatsColumns
 	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,
-};
-
 /*
  * SQL functions
  */
@@ -714,14 +583,18 @@ 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 char *get_opt_value(PGresult *res, int row, int col);
+static void set_text_arg(NullableDatum *arg, const char *s);
+static void set_int32_arg(NullableDatum *arg, const char *s);
+static void set_uint32_arg(NullableDatum *arg, const char *s);
+static void set_float_arg(NullableDatum *arg, const char *s);
+static void set_floatarr_arg(NullableDatum *arg, const char *s);
 static void produce_tuple_asynchronously(AsyncRequest *areq, bool fetch);
 static void fetch_more_data_begin(AsyncRequest *areq);
 static void complete_pending_request(AsyncRequest *areq);
@@ -5661,7 +5534,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)
@@ -5725,7 +5598,7 @@ fetch_remote_statistics(Relation relation,
 	 */
 	user = GetUserMapping(GetUserId(), table->serverid);
 	conn = GetConnection(user, false, NULL);
-	remstats->server_version_num = server_version_num = PQserverVersion(conn);
+	server_version_num = PQserverVersion(conn);
 
 	/* Fetch relation stats. */
 	remstats->rel = relstats = fetch_relstats(conn, relation);
@@ -6128,58 +6001,31 @@ match_attrmap(PGresult *res,
  * 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		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();
+	PGresult   *res;
+	NullableDatum args[ATTSTATS_NUM_FIELDS - 1];
 
 	/*
 	 * We import attribute statistics first, if any, because those are more
 	 * prone to errors.  This avoids making a modification of pg_class that
 	 * will just get rolled back by a failed attribute import.
 	 */
-	if (remstats->att != NULL)
+	res = remstats->att;
+	if (res != NULL)
 	{
-		Assert(PQnfields(remstats->att) == ATTSTATS_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] = ' ';
+		Assert(PQnfields(res) == ATTSTATS_NUM_FIELDS);
+		Assert(PQntuples(res) >= 1);
 
 		for (int mapidx = 0; mapidx < attrcnt; mapidx++)
 		{
 			int			row = remattrmap[mapidx].res_index;
-			Datum	   *values2 = values + 1;
-			char	   *nulls2 = nulls + 1;
+			AttrNumber	attnum = remattrmap[mapidx].local_attnum;
 
 			/* All mappings should have been assigned a result set row. */
 			Assert(row >= 0);
@@ -6191,128 +6037,192 @@ import_fetched_statistics(const char *schemaname,
 
 			/*
 			 * 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())
+			delete_attribute_statistics(relation, attnum, false);
+
+			set_float_arg(&args[0],
+						  get_opt_value(res, row, ATTSTATS_NULL_FRAC));
+			set_int32_arg(&args[1],
+						  get_opt_value(res, row, ATTSTATS_AVG_WIDTH));
+			set_float_arg(&args[2],
+						  get_opt_value(res, row, ATTSTATS_N_DISTINCT));
+			set_text_arg(&args[3],
+						 get_opt_value(res, row, ATTSTATS_MOST_COMMON_VALS));
+			set_floatarr_arg(&args[4],
+							 get_opt_value(res, row, ATTSTATS_MOST_COMMON_FREQS));
+			set_text_arg(&args[5],
+						 get_opt_value(res, row, ATTSTATS_HISTOGRAM_BOUNDS));
+			set_float_arg(&args[6],
+						  get_opt_value(res, row, ATTSTATS_CORRELATION));
+			set_text_arg(&args[7],
+						 get_opt_value(res, row, ATTSTATS_MOST_COMMON_ELEMS));
+			set_floatarr_arg(&args[8],
+							 get_opt_value(res, row, ATTSTATS_MOST_COMMON_ELEM_FREQS));
+			set_floatarr_arg(&args[9],
+							 get_opt_value(res, row, ATTSTATS_ELEM_COUNT_HISTOGRAM));
+			set_text_arg(&args[10],
+						 get_opt_value(res, row, ATTSTATS_RANGE_LENGTH_HISTOGRAM));
+			set_float_arg(&args[11],
+						  get_opt_value(res, row, ATTSTATS_RANGE_EMPTY_FRAC));
+			set_text_arg(&args[12],
+						 get_opt_value(res, row, ATTSTATS_RANGE_BOUNDS_HISTOGRAM));
+
+			if (!import_attribute_statistics(relation, attnum, false,
+											 &args[0], &args[1], &args[2],
+											 &args[3], &args[4], &args[5],
+											 &args[6], &args[7], &args[8],
+											 &args[9], &args[10], &args[11],
+											 &args[12]))
 			{
 				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));
-				goto import_cleanup;
+				return false;
 			}
 		}
 	}
 
 	/*
-	 * 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.
-	 */
-	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())
+	 * Import relation stats.
+	 */
+	res = remstats->rel;
+	Assert(res != NULL);
+	Assert(PQnfields(res) == RELSTATS_NUM_FIELDS);
+	Assert(PQntuples(res) == 1);
+
+	set_uint32_arg(&args[0], get_opt_value(res, 0, RELSTATS_RELPAGES));
+	Assert(!args[0].isnull);
+	set_float_arg(&args[1], get_opt_value(res, 0, RELSTATS_RELTUPLES));
+	Assert(!args[1].isnull);
+	args[2].value = (Datum) 0;
+	args[2].isnull = true;
+	args[3].value = (Datum) 0;
+	args[3].isnull = true;
+
+	if (!import_relation_statistics(relation,
+									&args[0], &args[1], &args[2], &args[3]))
 	{
 		ereport(WARNING,
 				errmsg("could not import statistics for foreign table \"%s.%s\" --- relation statistics import failed for this foreign table",
 					   schemaname, relname));
-		goto import_cleanup;
+		return false;
 	}
 
-	ok = true;
+	return true;
+}
 
-import_cleanup:
-	if (attimport_plan)
-		SPI_freeplan(attimport_plan);
-	if (attclear_plan)
-		SPI_freeplan(attclear_plan);
-	SPI_finish();
-	return ok;
+/*
+ * Conenience routine to fetch
+ */
+static char *
+get_opt_value(PGresult *res, int row, int col)
+{
+	if (PQgetisnull(res, row, col))
+		return NULL;
+	return PQgetvalue(res, row, col);
 }
 
 /*
- * Move a string value from a result set to a Text value of a Datum array.
+ * Convenience routine for setting optional text arguments
  */
 static void
-map_field_to_arg(PGresult *res, int row, int field,
-				 int arg, Datum *values, char *nulls)
+set_text_arg(NullableDatum *arg, const char *s)
 {
-	if (PQgetisnull(res, row, field))
+	if (s)
 	{
-		values[arg] = (Datum) 0;
-		nulls[arg] = 'n';
+		arg->value = CStringGetTextDatum(s);
+		arg->isnull = false;
 	}
 	else
 	{
-		const char *s = PQgetvalue(res, row, field);
+		arg->value = (Datum) 0;
+		arg->isnull = true;
+	}
+}
+
+/*
+ * Convenience routine for setting optional int32 arguments
+ */
+static void
+set_int32_arg(NullableDatum *arg, const char *s)
+{
+	if (s)
+	{
+		int32		val = pg_strtoint32(s);
 
-		values[arg] = CStringGetTextDatum(s);
-		nulls[arg] = ' ';
+		arg->value = Int32GetDatum(val);
+		arg->isnull = false;
+	}
+	else
+	{
+		arg->value = (Datum) 0;
+		arg->isnull = true;
 	}
 }
 
 /*
- * Check the 1x1 result set of a pg_restore_*_stats() command for success.
+ * Convenience routine for setting optional uint32 arguments
  */
-static bool
-import_spi_query_ok(void)
+static void
+set_uint32_arg(NullableDatum *arg, const char *s)
 {
-	TupleDesc	tupdesc;
-	Datum		dat;
-	bool		isnull;
+	if (s)
+	{
+		uint32		val = uint32in_subr(s, NULL, "uint32", NULL);
 
-	Assert(SPI_tuptable != NULL);
-	Assert(SPI_processed == 1);
+		arg->value = UInt32GetDatum(val);
+		arg->isnull = false;
+	}
+	else
+	{
+		arg->value = (Datum) 0;
+		arg->isnull = true;
+	}
+}
+
+/*
+ * Convenience routine for setting optional float arguments
+ */
+static void
+set_float_arg(NullableDatum *arg, const char *s)
+{
+	if (s)
+	{
+		float4		val = float4in_internal((char *) s, NULL, "float", s, NULL);
 
-	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);
+		arg->value = Float4GetDatum(val);
+		arg->isnull = false;
+	}
+	else
+	{
+		arg->value = (Datum) 0;
+		arg->isnull = true;
+	}
+}
 
-	return DatumGetBool(dat);
+/*
+ * Convenience routine for setting optional float[] arguments
+ */
+static void
+set_floatarr_arg(NullableDatum *arg, const char *s)
+{
+	if (s)
+	{
+		FmgrInfo	flinfo;
+		Datum		val;
+
+		fmgr_info(F_ARRAY_IN, &flinfo);
+		val = InputFunctionCall(&flinfo, (char *) s, FLOAT4OID, -1);
+
+		arg->value = val;
+		arg->isnull = false;
+	}
+	else
+	{
+		arg->value = (Datum) 0;
+		arg->isnull = true;
+	}
 }
 
 /*
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index dfc58beb0d2..0d0622b31ce 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -4584,6 +4584,34 @@ ANALYZE dtest_table;
 
 ANALYZE VERBOSE dtest_ftable;             -- should work
 
+-- dtest_ftable's stats should now exactly match dtest_table's
+-- 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;
+
+-- 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';
+
+-- 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;
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 1cc4d657231..0d88e18096b 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -688,3 +688,67 @@ pg_restore_attribute_stats(PG_FUNCTION_ARGS)
 
 	PG_RETURN_BOOL(result);
 }
+
+/*
+ * Import attribute statistics from NullableDatum inputs for all statitical
+ * values.
+ */
+bool
+import_attribute_statistics(Relation rel, AttrNumber attnum, bool inherited,
+							NullableDatum *null_frac,
+							NullableDatum *avg_width,
+							NullableDatum *n_distinct,
+							NullableDatum *most_common_vals,
+							NullableDatum *most_common_freqs,
+							NullableDatum *histogram_bounds,
+							NullableDatum *correlation,
+							NullableDatum *most_common_elems,
+							NullableDatum *most_common_elem_freqs,
+							NullableDatum *elem_count_histogram,
+							NullableDatum *range_length_histogram,
+							NullableDatum *range_empty_frac,
+							NullableDatum *range_bounds_histogram)
+{
+	LOCAL_FCINFO(newfcinfo, NUM_ATTRIBUTE_STATS_ARGS);
+
+	InitFunctionCallInfoData(*newfcinfo, NULL, NUM_ATTRIBUTE_STATS_ARGS,
+							 InvalidOid, NULL, NULL);
+
+	newfcinfo->args[ATTRELSCHEMA_ARG].value =
+		CStringGetTextDatum(get_namespace_name(RelationGetNamespace(rel)));
+	newfcinfo->args[ATTRELSCHEMA_ARG].isnull = false;
+	newfcinfo->args[ATTRELNAME_ARG].value =
+		CStringGetTextDatum(RelationGetRelationName(rel));
+	newfcinfo->args[ATTRELNAME_ARG].isnull = false;
+	newfcinfo->args[ATTNAME_ARG].value = (Datum) 0;
+	newfcinfo->args[ATTNAME_ARG].isnull = true;
+	newfcinfo->args[ATTNUM_ARG].value = Int16GetDatum(attnum);
+	newfcinfo->args[ATTNUM_ARG].isnull = false;
+	newfcinfo->args[INHERITED_ARG].value = BoolGetDatum(inherited);
+	newfcinfo->args[INHERITED_ARG].isnull = false;
+
+	newfcinfo->args[NULL_FRAC_ARG] = *null_frac;
+	newfcinfo->args[AVG_WIDTH_ARG] = *avg_width;
+	newfcinfo->args[N_DISTINCT_ARG] = *n_distinct;
+	newfcinfo->args[MOST_COMMON_VALS_ARG] = *most_common_vals;
+	newfcinfo->args[MOST_COMMON_FREQS_ARG] = *most_common_freqs;
+	newfcinfo->args[HISTOGRAM_BOUNDS_ARG] = *histogram_bounds;
+	newfcinfo->args[CORRELATION_ARG] = *correlation;
+	newfcinfo->args[MOST_COMMON_ELEMS_ARG] = *most_common_elems;
+	newfcinfo->args[MOST_COMMON_ELEM_FREQS_ARG] = *most_common_elem_freqs;
+	newfcinfo->args[ELEM_COUNT_HISTOGRAM_ARG] = *elem_count_histogram;
+	newfcinfo->args[RANGE_LENGTH_HISTOGRAM_ARG] = *range_length_histogram;
+	newfcinfo->args[RANGE_EMPTY_FRAC_ARG] = *range_empty_frac;
+	newfcinfo->args[RANGE_BOUNDS_HISTOGRAM_ARG] = *range_bounds_histogram;
+
+	return attribute_statistics_update(newfcinfo);
+}
+
+/*
+ * Delete attribute statistics.
+ */
+bool
+delete_attribute_statistics(Relation rel, AttrNumber attnum, bool inherited)
+{
+	return delete_pg_statistic(RelationGetRelid(rel), attnum, inherited);
+}
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index d6631e9a9a4..1bd4b6e0dba 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -21,6 +21,7 @@
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
 #include "nodes/makefuncs.h"
+#include "statistics/statistics.h"
 #include "statistics/stat_utils.h"
 #include "utils/builtins.h"
 #include "utils/fmgroids.h"
@@ -241,3 +242,34 @@ pg_restore_relation_stats(PG_FUNCTION_ARGS)
 
 	PG_RETURN_BOOL(result);
 }
+
+/*
+ * Import relation statistics from NullableDatum inputs for all statitical
+ * values.
+ */
+bool
+import_relation_statistics(Relation rel,
+						   NullableDatum *relpages,
+						   NullableDatum *reltuples,
+						   NullableDatum *relallvisible,
+						   NullableDatum *relallfrozen)
+{
+	LOCAL_FCINFO(newfcinfo, NUM_RELATION_STATS_ARGS);
+
+	InitFunctionCallInfoData(*newfcinfo, NULL, NUM_RELATION_STATS_ARGS,
+							 InvalidOid, NULL, NULL);
+
+	newfcinfo->args[RELSCHEMA_ARG].value =
+		CStringGetTextDatum(get_namespace_name(RelationGetNamespace(rel)));
+	newfcinfo->args[RELSCHEMA_ARG].isnull = false;
+	newfcinfo->args[RELNAME_ARG].value =
+		CStringGetTextDatum(RelationGetRelationName(rel));
+	newfcinfo->args[RELNAME_ARG].isnull = false;
+
+	newfcinfo->args[RELPAGES_ARG] = *relpages;
+	newfcinfo->args[RELTUPLES_ARG] = *reltuples;
+	newfcinfo->args[RELALLVISIBLE_ARG] = *relallvisible;
+	newfcinfo->args[RELALLFROZEN_ARG] = *relallfrozen;
+
+	return relation_statistics_update(newfcinfo);
+}
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 8f9b9d237fd..b61b85858ff 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -128,4 +128,27 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern bool import_relation_statistics(Relation rel,
+									   NullableDatum *relpages,
+									   NullableDatum *reltuples,
+									   NullableDatum *relallvisible,
+									   NullableDatum *relallfrozen);
+extern bool import_attribute_statistics(Relation rel,
+										AttrNumber attnum, bool inherited,
+										NullableDatum *null_frac,
+										NullableDatum *avg_width,
+										NullableDatum *n_distinct,
+										NullableDatum *most_common_vals,
+										NullableDatum *most_common_freqs,
+										NullableDatum *histogram_bounds,
+										NullableDatum *correlation,
+										NullableDatum *most_common_elems,
+										NullableDatum *most_common_elem_freqs,
+										NullableDatum *elem_count_histogram,
+										NullableDatum *range_length_histogram,
+										NullableDatum *range_empty_frac,
+										NullableDatum *range_bounds_histogram);
+extern bool delete_attribute_statistics(Relation rel,
+										AttrNumber attnum, bool inherited);
+
 #endif							/* STATISTICS_H */


^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-17 10:23             ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-17 16:00               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-18 10:42                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-18 21:23                   ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-19 13:45                     ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-20 04:11                       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-22 19:08                         ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-23 12:10                           ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-23 15:53                             ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-29 08:14                               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-29 11:50                                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-29 18:17                                   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-30 01:10                                     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-30 12:29                                       ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
@ 2026-06-30 15:55                                         ` Corey Huinker <[email protected]>
  2026-06-30 16:21                                           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-07-01 12:09                                           ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  0 siblings, 2 replies; 33+ messages in thread

From: Corey Huinker @ 2026-06-30 15:55 UTC (permalink / raw)
  To: Etsuro Fujita <[email protected]>; +Cc: Robert Haas <[email protected]>; pgsql-hackers

On Tue, Jun 30, 2026 at 7:30 AM Etsuro Fujita <[email protected]>
wrote:

> On Tue, Jun 30, 2026 at 10:10 AM Corey Huinker <[email protected]>
> wrote:
> > On Mon, Jun 29, 2026 at 2:17 PM Robert Haas <[email protected]>
> wrote:
> >> On Mon, Jun 29, 2026 at 7:50 AM Etsuro Fujita <[email protected]>
> wrote:
> >> > I like this refactoring, but while it's rather mechanical, it's pretty
> >> > large, so I think it's too late to do the refactoring at this time
> >> > just before the beta 2 release.  So I'd vote for going with your
> >> > v1-0001 and v1-0002 and doing the refactoring in v20.  As mentioned by
> >> > Robert, I don't think it's good to call LOCAL_FCINFO() in
> >> > import_relation_statistics() and import_attribute_statistics() to call
> >> > the guts of those functions either, but that is *consistent* with the
> >> > existing way pg_restore_relation_stats() and
> >> > pg_restore_attribute_stats() do that, so that is actually not that
> >> > bad.  Also, as you mentioned above, it's inefficient for the new API
> >> > functions to lock an already-locked relation, and validate an
> >> > already-validated attname/attnum, but I think it would be negligible.
> >>
> >> Fujita-san, is it then your plan to get those two patches committed?
>
> Yes, it is.
>
> > Are we ok with changing the functions that postgres_fdw makes from v19
> to v20? Because what I put in the v2 patch does not match the v1 patch. If
> we are NOT ok with that, then we'd need to:
> >
> > 1. rename the two relation_stats_argnum -> relation_args_argnum and
> attribute_stats_argnum -> attribute_args_argnum out of the way of the ones
> created in relation_stats.h and attribute_stats.h. The actual enumeration
> values can stay the same, as there are no conflicts there, just ambiguity
> about which set of values they belong to.
> > 2. rename the existing static functions relation_statistics_update ->
> update_relstats and attribute_statistics_update -> update_relstats to make
> way for the public functions of the same names.
> > 3. Create new relation_statistics_update and
> attribute_statistics_update, with the isnull/values, and have those
> fcinfo-invoke the respective pg_restore functions just like v1 does.
> >
> > That would at least make the user API consistent in v19 vs v20.
> >
> > I'm on vacation this week, but time is short for beta2, so I'll make an
> exception to get the above into beta2. Let me know if you want me to write
> that up.
>
> Thanks!
>
> Here is an updated patch, which merges your v1-0001 and v1-0002.  One
> notable change I made to your version is the signatures of
> import_relation_statistics() and import_attribute_statistics().  As
> mentioned upthread, the proposed signatures of these functions would
> be too tailored for postgres_fdw, and might not be useful for other
> FDWs, so I changed them to have NullableDatum arguments for stats-data
> inputs like this:
>
> bool
> import_relation_statistics(Relation rel,
>                            NullableDatum *relpages,
>                            NullableDatum *reltuples,
>                            NullableDatum *relallvisible,
>                            NullableDatum *relallfrozen)
>
> bool
> import_attribute_statistics(Relation rel, AttrNumber attnum, bool
> inherited,
>                             NullableDatum *null_frac,
>                             NullableDatum *avg_width,
>                             NullableDatum *n_distinct,
>                             NullableDatum *most_common_vals,
>                             NullableDatum *most_common_freqs,
>                             NullableDatum *histogram_bounds,
>                             NullableDatum *correlation,
>                             NullableDatum *most_common_elems,
>                             NullableDatum *most_common_elem_freqs,
>                             NullableDatum *elem_count_histogram,
>                             NullableDatum *range_length_histogram,
>                             NullableDatum *range_empty_frac,
>                             NullableDatum *range_bounds_histogram)
>
> As postgresImportForeignStatistics() is provided for FDWs that want to
> calculate statistics remotely, so I'm assuming that they have enough
> knowledge about the stats-data structures.




>   I think we could instead
> use the values/isnull arrays as proposed in your v2, but the problem
> about doing so is error messages in functions in stat_utils.c like
> this:
>
>         ereport(WARNING,
>                 (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
>                  errmsg("argument \"%s\" must not be a multidimensional
> array",
>                         arginfo[argnum].argname)));
>
> Note that the message uses the word "argument", so I think it's better
> for the new functions as well to have individual arguments for
> stats-data inputs.


That's a good example of how our error messages get a bit clumsy with so
many ways of calling things, so I'm fine with your solution. It also
eliminates the possibility that a caller fails to notice a newly added stat
parameter and instead calls the input function with the older short array
length.

My initial concern was that this would bloat up postgres_fdw with type
conversion code, but the patch shows that it isn't that bad.

One thing we do lose in this is the assurance that the Datums passed are of
the correct type. Presently that is checked via
stats_fill_fcinfo_from_arg_pairs, which allows the *_statistics_update()
functions to trust the type correctness of the Datums that they're passed,
but I can be convinced that it isn't a big problem.


> Also, I removed the attname argument from
> import_attribute_statistics(), to 1) make the signature (and the
> internal processing) simple and 2) match it to
> delete_attribute_statistics().
>

I think that's reasonable. Internal callers have already resolved which
attribute they want to modify. The attname parameter was there so that
pg_restore_* calls could survive dump/restores where there were dropped
columns so the attnum has changed.

In relation to this change, I moved helper functions set_XXX_arg()
> that you added to relation_stats.c and attribute_stats.c to
> postgres_fdw.c.  The helper functions had soft error handling, but in
> the postgres_fdw use, there is no need for that, so I removed that
> handling as well.
>

My inclination would be to move the set_*_arg functions could be moved to
some common utility file in v20, as other FDWs will find themselves with
the same need.

As for removing the error-handling, it might still be handy because it
would allow us to give a more context-aware error message, rather than the
very narrow "this_string is an invalid this_type", for string that the user
most certainly never saw. Having said that, it's something we could easily
change back to error-safe if we wanted to.

What do you think?  If there is no problem with this change, we
> wouldn't need to worry about the stability of postgres_fdw (and other
> FDWs) in v20.
>

If you're fine with the names import_relation_statistics and
import_attribute_statistics, then yes. Those names made sense to me in the
narrow case of being called from an FDW handler and handing the function a
bunch of strings, but in a more general case with properly typed datums,
the name feels less appropriate, but it's just a name and I wouldn't let it
get in the way of the overall patch.


^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-17 10:23             ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-17 16:00               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-18 10:42                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-18 21:23                   ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-19 13:45                     ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-20 04:11                       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-22 19:08                         ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-23 12:10                           ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-23 15:53                             ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-29 08:14                               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-29 11:50                                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-29 18:17                                   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-30 01:10                                     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-30 12:29                                       ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-30 15:55                                         ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
@ 2026-06-30 16:21                                           ` Corey Huinker <[email protected]>
  1 sibling, 0 replies; 33+ messages in thread

From: Corey Huinker @ 2026-06-30 16:21 UTC (permalink / raw)
  To: Etsuro Fujita <[email protected]>; +Cc: Robert Haas <[email protected]>; pgsql-hackers

>
>
> My initial concern was that this would bloat up postgres_fdw with type
> conversion code, but the patch shows that it isn't that bad.
>

One additional thought - in an offline conversation I had with Robert Haas,
he had taken the position that any FDW was likely pulling values across a
wire protocol and then synthesizing postgres-looking statistics from that,
so going with cstring inputs was fine, possibly preferable. I'm
highlighting that we're now passing in NullableDatums in case he wanted to
object to that change.


^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-17 10:23             ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-17 16:00               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-18 10:42                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-18 21:23                   ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-19 13:45                     ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-20 04:11                       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-22 19:08                         ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-23 12:10                           ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-23 15:53                             ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-29 08:14                               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-29 11:50                                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-29 18:17                                   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-30 01:10                                     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-30 12:29                                       ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-30 15:55                                         ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
@ 2026-07-01 12:09                                           ` Etsuro Fujita <[email protected]>
  2026-07-01 20:34                                             ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  1 sibling, 1 reply; 33+ messages in thread

From: Etsuro Fujita @ 2026-07-01 12:09 UTC (permalink / raw)
  To: Corey Huinker <[email protected]>; +Cc: Robert Haas <[email protected]>; pgsql-hackers

On Wed, Jul 1, 2026 at 12:55 AM Corey Huinker <[email protected]> wrote:
> On Tue, Jun 30, 2026 at 7:30 AM Etsuro Fujita <[email protected]> wrote:

>> As postgresImportForeignStatistics() is provided for FDWs that want to
>> calculate statistics remotely, so I'm assuming that they have enough
>> knowledge about the stats-data structures.

CORRECTION: s/postgresImportForeignStatistics()/ImportForeignStatistics()/
 Sorry for that.

>> In relation to this change, I moved helper functions set_XXX_arg()
>> that you added to relation_stats.c and attribute_stats.c to
>> postgres_fdw.c.  The helper functions had soft error handling, but in
>> the postgres_fdw use, there is no need for that, so I removed that
>> handling as well.
>
> My inclination would be to move the set_*_arg functions could be moved to some common utility file in v20, as other FDWs will find themselves with the same need.

Seems like a good idea.  I moved the set_*_arg functions to
stat_utils.c, and renamed them to stats_set_*_arg.  Attached is a new
version of the patch.

> As for removing the error-handling, it might still be handy because it would allow us to give a more context-aware error message, rather than the very narrow "this_string is an invalid this_type", for string that the user most certainly never saw. Having said that, it's something we could easily change back to error-safe if we wanted to.

Ok, I think we could do so later if needed.

>> What do you think?  If there is no problem with this change, we
>> wouldn't need to worry about the stability of postgres_fdw (and other
>> FDWs) in v20.
>
> If you're fine with the names import_relation_statistics and import_attribute_statistics, then yes. Those names made sense to me in the narrow case of being called from an FDW handler and handing the function a bunch of strings, but in a more general case with properly typed datums, the name feels less appropriate, but it's just a name and I wouldn't let it get in the way of the overall patch.

I like the names, but I'm open to suggestions.

I modified the patch further.  To support these differences:

"There are important differences in the parameters needed by the
different types of functions. The pg_restore_*() SQL function calls
need to identify the schema+relname of the relation being modified,
and already have all of the values as typed Datums, whereas the
internal API calls already have an identified and locked open
Relation, but all of the statistical parameters are just cstrings."

I incorporated your v2-0010 and v2-0012 into the patch: 1) separate
the guts of relation_statistics_update() and
attribute_statistics_update() into new functions
relation_statistics_update_internal() and
attribute_statistics_update_internal(), and 2) modify
import_relation_statistics() and import_attribute_statistics() to call
these new functions instead.  Also, I changed the stats-data
argument-type in import_relation_statistics() and
import_attribute_statistics() from NullableDatum * to const
NullableDatum *.

Best regards,
Etsuro Fujita


Attachments:

  [application/octet-stream] v2-Remove-SPI-from-stat-import-in-postgres-fdw-efujita.patch (33.0K, ../../CAPmGK17rNA=dknxHCXbUoQ_xe2PQcPq0d-bhTr47wrUfXrDLFA@mail.gmail.com/2-v2-Remove-SPI-from-stat-import-in-postgres-fdw-efujita.patch)
  download | inline diff:
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index e90289e4ab1..51d09b27267 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -12927,6 +12927,43 @@ 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_ftable's stats should now exactly match dtest_table's
+-- 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)
+
+-- 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)
+
+-- 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..b143e7369c0 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,7 @@
 #include "parser/parsetree.h"
 #include "postgres_fdw.h"
 #include "statistics/statistics.h"
+#include "statistics/stat_utils.h"
 #include "storage/latch.h"
 #include "utils/builtins.h"
 #include "utils/float.h"
@@ -335,7 +335,6 @@ typedef struct
 {
 	PGresult   *rel;
 	PGresult   *att;
-	int			server_version_num;
 } RemoteStatsResults;
 
 /* Column order in relation stats query */
@@ -367,136 +366,6 @@ enum AttStatsColumns
 	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,
-};
-
 /*
  * SQL functions
  */
@@ -714,14 +583,13 @@ 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 char *get_opt_value(PGresult *res, int row, int col);
 static void produce_tuple_asynchronously(AsyncRequest *areq, bool fetch);
 static void fetch_more_data_begin(AsyncRequest *areq);
 static void complete_pending_request(AsyncRequest *areq);
@@ -5661,7 +5529,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)
@@ -5725,7 +5593,7 @@ fetch_remote_statistics(Relation relation,
 	 */
 	user = GetUserMapping(GetUserId(), table->serverid);
 	conn = GetConnection(user, false, NULL);
-	remstats->server_version_num = server_version_num = PQserverVersion(conn);
+	server_version_num = PQserverVersion(conn);
 
 	/* Fetch relation stats. */
 	remstats->rel = relstats = fetch_relstats(conn, relation);
@@ -6128,58 +5996,31 @@ match_attrmap(PGresult *res,
  * 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		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();
+	PGresult   *res;
+	NullableDatum args[ATTSTATS_NUM_FIELDS - 1];
 
 	/*
 	 * We import attribute statistics first, if any, because those are more
 	 * prone to errors.  This avoids making a modification of pg_class that
 	 * will just get rolled back by a failed attribute import.
 	 */
-	if (remstats->att != NULL)
+	res = remstats->att;
+	if (res != NULL)
 	{
-		Assert(PQnfields(remstats->att) == ATTSTATS_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] = ' ';
+		Assert(PQnfields(res) == ATTSTATS_NUM_FIELDS);
+		Assert(PQntuples(res) >= 1);
 
 		for (int mapidx = 0; mapidx < attrcnt; mapidx++)
 		{
 			int			row = remattrmap[mapidx].res_index;
-			Datum	   *values2 = values + 1;
-			char	   *nulls2 = nulls + 1;
+			AttrNumber	attnum = remattrmap[mapidx].local_attnum;
 
 			/* All mappings should have been assigned a result set row. */
 			Assert(row >= 0);
@@ -6191,128 +6032,90 @@ import_fetched_statistics(const char *schemaname,
 
 			/*
 			 * 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())
+			delete_attribute_statistics(relation, attnum, false);
+
+			stats_set_float_arg(&args[0],
+								get_opt_value(res, row, ATTSTATS_NULL_FRAC));
+			stats_set_int32_arg(&args[1],
+								get_opt_value(res, row, ATTSTATS_AVG_WIDTH));
+			stats_set_float_arg(&args[2],
+								get_opt_value(res, row, ATTSTATS_N_DISTINCT));
+			stats_set_text_arg(&args[3],
+							   get_opt_value(res, row, ATTSTATS_MOST_COMMON_VALS));
+			stats_set_floatarr_arg(&args[4],
+								   get_opt_value(res, row, ATTSTATS_MOST_COMMON_FREQS));
+			stats_set_text_arg(&args[5],
+							   get_opt_value(res, row, ATTSTATS_HISTOGRAM_BOUNDS));
+			stats_set_float_arg(&args[6],
+								get_opt_value(res, row, ATTSTATS_CORRELATION));
+			stats_set_text_arg(&args[7],
+							   get_opt_value(res, row, ATTSTATS_MOST_COMMON_ELEMS));
+			stats_set_floatarr_arg(&args[8],
+								   get_opt_value(res, row, ATTSTATS_MOST_COMMON_ELEM_FREQS));
+			stats_set_floatarr_arg(&args[9],
+								   get_opt_value(res, row, ATTSTATS_ELEM_COUNT_HISTOGRAM));
+			stats_set_text_arg(&args[10],
+							   get_opt_value(res, row, ATTSTATS_RANGE_LENGTH_HISTOGRAM));
+			stats_set_float_arg(&args[11],
+								get_opt_value(res, row, ATTSTATS_RANGE_EMPTY_FRAC));
+			stats_set_text_arg(&args[12],
+							   get_opt_value(res, row, ATTSTATS_RANGE_BOUNDS_HISTOGRAM));
+
+			if (!import_attribute_statistics(relation, attnum, false,
+											 &args[0], &args[1], &args[2],
+											 &args[3], &args[4], &args[5],
+											 &args[6], &args[7], &args[8],
+											 &args[9], &args[10], &args[11],
+											 &args[12]))
 			{
 				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));
-				goto import_cleanup;
+				return false;
 			}
 		}
 	}
 
 	/*
-	 * 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.
-	 */
-	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())
+	 * Import relation stats.
+	 */
+	res = remstats->rel;
+	Assert(res != NULL);
+	Assert(PQnfields(res) == RELSTATS_NUM_FIELDS);
+	Assert(PQntuples(res) == 1);
+
+	stats_set_uint32_arg(&args[0], get_opt_value(res, 0, RELSTATS_RELPAGES));
+	Assert(!args[0].isnull);
+	stats_set_float_arg(&args[1], get_opt_value(res, 0, RELSTATS_RELTUPLES));
+	Assert(!args[1].isnull);
+	args[2].value = (Datum) 0;
+	args[2].isnull = true;
+	args[3].value = (Datum) 0;
+	args[3].isnull = true;
+
+	if (!import_relation_statistics(relation,
+									&args[0], &args[1], &args[2], &args[3]))
 	{
 		ereport(WARNING,
 				errmsg("could not import statistics for foreign table \"%s.%s\" --- relation statistics import failed for this foreign table",
 					   schemaname, relname));
-		goto import_cleanup;
-	}
-
-	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';
+		return false;
 	}
-	else
-	{
-		const char *s = PQgetvalue(res, row, field);
 
-		values[arg] = CStringGetTextDatum(s);
-		nulls[arg] = ' ';
-	}
+	return true;
 }
 
 /*
- * Check the 1x1 result set of a pg_restore_*_stats() command for success.
+ * Conenience routine to fetch
  */
-static bool
-import_spi_query_ok(void)
+static char *
+get_opt_value(PGresult *res, int row, int col)
 {
-	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);
+	if (PQgetisnull(res, row, col))
+		return NULL;
+	return PQgetvalue(res, row, col);
 }
 
 /*
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index dfc58beb0d2..0d0622b31ce 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -4584,6 +4584,34 @@ ANALYZE dtest_table;
 
 ANALYZE VERBOSE dtest_ftable;             -- should work
 
+-- dtest_ftable's stats should now exactly match dtest_table's
+-- 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;
+
+-- 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';
+
+-- 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;
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 1cc4d657231..7c3259861ee 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -105,6 +105,11 @@ static struct StatsArgInfo cleararginfo[] =
 };
 
 static bool attribute_statistics_update(FunctionCallInfo fcinfo);
+static bool attribute_statistics_update_internal(Oid reloid,
+												 const char *attname,
+												 AttrNumber attnum,
+												 bool inherited,
+												 FunctionCallInfo fcinfo);
 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);
@@ -136,38 +141,6 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	bool		inherited;
 	Oid			locked_table = InvalidOid;
 
-	Relation	starel;
-	HeapTuple	statup;
-
-	Oid			atttypid = InvalidOid;
-	int32		atttypmod;
-	char		atttyptype;
-	Oid			atttypcoll = InvalidOid;
-	Oid			eq_opr = InvalidOid;
-	Oid			lt_opr = InvalidOid;
-
-	Oid			elemtypid = InvalidOid;
-	Oid			elem_eq_opr = InvalidOid;
-
-	FmgrInfo	array_in_fn;
-
-	bool		do_mcv = !PG_ARGISNULL(MOST_COMMON_FREQS_ARG) &&
-		!PG_ARGISNULL(MOST_COMMON_VALS_ARG);
-	bool		do_histogram = !PG_ARGISNULL(HISTOGRAM_BOUNDS_ARG);
-	bool		do_correlation = !PG_ARGISNULL(CORRELATION_ARG);
-	bool		do_mcelem = !PG_ARGISNULL(MOST_COMMON_ELEMS_ARG) &&
-		!PG_ARGISNULL(MOST_COMMON_ELEM_FREQS_ARG);
-	bool		do_dechist = !PG_ARGISNULL(ELEM_COUNT_HISTOGRAM_ARG);
-	bool		do_bounds_histogram = !PG_ARGISNULL(RANGE_BOUNDS_HISTOGRAM_ARG);
-	bool		do_range_length_histogram = !PG_ARGISNULL(RANGE_LENGTH_HISTOGRAM_ARG) &&
-		!PG_ARGISNULL(RANGE_EMPTY_FRAC_ARG);
-
-	Datum		values[Natts_pg_statistic] = {0};
-	bool		nulls[Natts_pg_statistic] = {0};
-	bool		replaces[Natts_pg_statistic] = {0};
-
-	bool		result = true;
-
 	stats_check_required_arg(fcinfo, attarginfo, ATTRELSCHEMA_ARG);
 	stats_check_required_arg(fcinfo, attarginfo, ATTRELNAME_ARG);
 
@@ -231,6 +204,47 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG);
 	inherited = PG_GETARG_BOOL(INHERITED_ARG);
 
+	return attribute_statistics_update_internal(reloid, attname, attnum,
+												inherited, fcinfo);
+}
+
+static bool
+attribute_statistics_update_internal(Oid reloid,
+									 const char *attname, AttrNumber attnum,
+									 bool inherited, FunctionCallInfo fcinfo)
+{
+	Relation	starel;
+	HeapTuple	statup;
+
+	Oid			atttypid = InvalidOid;
+	int32		atttypmod;
+	char		atttyptype;
+	Oid			atttypcoll = InvalidOid;
+	Oid			eq_opr = InvalidOid;
+	Oid			lt_opr = InvalidOid;
+
+	Oid			elemtypid = InvalidOid;
+	Oid			elem_eq_opr = InvalidOid;
+
+	FmgrInfo	array_in_fn;
+
+	bool		do_mcv = !PG_ARGISNULL(MOST_COMMON_FREQS_ARG) &&
+		!PG_ARGISNULL(MOST_COMMON_VALS_ARG);
+	bool		do_histogram = !PG_ARGISNULL(HISTOGRAM_BOUNDS_ARG);
+	bool		do_correlation = !PG_ARGISNULL(CORRELATION_ARG);
+	bool		do_mcelem = !PG_ARGISNULL(MOST_COMMON_ELEMS_ARG) &&
+		!PG_ARGISNULL(MOST_COMMON_ELEM_FREQS_ARG);
+	bool		do_dechist = !PG_ARGISNULL(ELEM_COUNT_HISTOGRAM_ARG);
+	bool		do_bounds_histogram = !PG_ARGISNULL(RANGE_BOUNDS_HISTOGRAM_ARG);
+	bool		do_range_length_histogram = !PG_ARGISNULL(RANGE_LENGTH_HISTOGRAM_ARG) &&
+		!PG_ARGISNULL(RANGE_EMPTY_FRAC_ARG);
+
+	Datum		values[Natts_pg_statistic] = {0};
+	bool		nulls[Natts_pg_statistic] = {0};
+	bool		replaces[Natts_pg_statistic] = {0};
+
+	bool		result = true;
+
 	/*
 	 * Check argument sanity. If some arguments are unusable, emit a WARNING
 	 * and set the corresponding argument to NULL in fcinfo.
@@ -688,3 +702,79 @@ pg_restore_attribute_stats(PG_FUNCTION_ARGS)
 
 	PG_RETURN_BOOL(result);
 }
+
+/*
+ * Import attribute statistics from NullableDatum inputs for all statitical
+ * values.
+ */
+bool
+import_attribute_statistics(Relation rel, AttrNumber attnum, bool inherited,
+							const NullableDatum *null_frac,
+							const NullableDatum *avg_width,
+							const NullableDatum *n_distinct,
+							const NullableDatum *most_common_vals,
+							const NullableDatum *most_common_freqs,
+							const NullableDatum *histogram_bounds,
+							const NullableDatum *correlation,
+							const NullableDatum *most_common_elems,
+							const NullableDatum *most_common_elem_freqs,
+							const NullableDatum *elem_count_histogram,
+							const NullableDatum *range_length_histogram,
+							const NullableDatum *range_empty_frac,
+							const NullableDatum *range_bounds_histogram)
+{
+	LOCAL_FCINFO(newfcinfo, NUM_ATTRIBUTE_STATS_ARGS);
+	Oid			reloid = RelationGetRelid(rel);
+	char	   *relname = RelationGetRelationName(rel);
+	char	   *attname  = get_attname(reloid, attnum, true);
+
+	InitFunctionCallInfoData(*newfcinfo, NULL, NUM_ATTRIBUTE_STATS_ARGS,
+							 InvalidOid, NULL, NULL);
+
+	newfcinfo->args[ATTRELSCHEMA_ARG].value =
+		CStringGetTextDatum(get_namespace_name(RelationGetNamespace(rel)));
+	newfcinfo->args[ATTRELSCHEMA_ARG].isnull = false;
+	newfcinfo->args[ATTRELNAME_ARG].value = CStringGetTextDatum(relname);
+	newfcinfo->args[ATTRELNAME_ARG].isnull = false;
+
+	/* 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)));
+
+	newfcinfo->args[ATTNAME_ARG].value = CStringGetTextDatum(attname);
+	newfcinfo->args[ATTNAME_ARG].isnull = false;
+	newfcinfo->args[ATTNUM_ARG].value = Int16GetDatum(attnum);
+	newfcinfo->args[ATTNUM_ARG].isnull = false;
+	newfcinfo->args[INHERITED_ARG].value = BoolGetDatum(inherited);
+	newfcinfo->args[INHERITED_ARG].isnull = false;
+
+	newfcinfo->args[NULL_FRAC_ARG] = *null_frac;
+	newfcinfo->args[AVG_WIDTH_ARG] = *avg_width;
+	newfcinfo->args[N_DISTINCT_ARG] = *n_distinct;
+	newfcinfo->args[MOST_COMMON_VALS_ARG] = *most_common_vals;
+	newfcinfo->args[MOST_COMMON_FREQS_ARG] = *most_common_freqs;
+	newfcinfo->args[HISTOGRAM_BOUNDS_ARG] = *histogram_bounds;
+	newfcinfo->args[CORRELATION_ARG] = *correlation;
+	newfcinfo->args[MOST_COMMON_ELEMS_ARG] = *most_common_elems;
+	newfcinfo->args[MOST_COMMON_ELEM_FREQS_ARG] = *most_common_elem_freqs;
+	newfcinfo->args[ELEM_COUNT_HISTOGRAM_ARG] = *elem_count_histogram;
+	newfcinfo->args[RANGE_LENGTH_HISTOGRAM_ARG] = *range_length_histogram;
+	newfcinfo->args[RANGE_EMPTY_FRAC_ARG] = *range_empty_frac;
+	newfcinfo->args[RANGE_BOUNDS_HISTOGRAM_ARG] = *range_bounds_histogram;
+
+	return attribute_statistics_update_internal(reloid, attname, attnum,
+												inherited, newfcinfo);
+}
+
+/*
+ * Delete attribute statistics.
+ */
+bool
+delete_attribute_statistics(Relation rel, AttrNumber attnum, bool inherited)
+{
+	return delete_pg_statistic(RelationGetRelid(rel), attnum, inherited);
+}
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index d6631e9a9a4..1442fdfc589 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -21,6 +21,7 @@
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
 #include "nodes/makefuncs.h"
+#include "statistics/statistics.h"
 #include "statistics/stat_utils.h"
 #include "utils/builtins.h"
 #include "utils/fmgroids.h"
@@ -57,6 +58,8 @@ static struct StatsArgInfo relarginfo[] =
 };
 
 static bool relation_statistics_update(FunctionCallInfo fcinfo);
+static bool relation_statistics_update_internal(Oid reloid,
+												FunctionCallInfo fcinfo);
 
 /*
  * Internal function for modifying statistics for a relation.
@@ -64,25 +67,9 @@ static bool relation_statistics_update(FunctionCallInfo fcinfo);
 static bool
 relation_statistics_update(FunctionCallInfo fcinfo)
 {
-	bool		result = true;
 	char	   *nspname;
 	char	   *relname;
 	Oid			reloid;
-	Relation	crel;
-	BlockNumber relpages = 0;
-	bool		update_relpages = false;
-	float		reltuples = 0;
-	bool		update_reltuples = false;
-	BlockNumber relallvisible = 0;
-	bool		update_relallvisible = false;
-	BlockNumber relallfrozen = 0;
-	bool		update_relallfrozen = false;
-	HeapTuple	ctup;
-	Form_pg_class pgcform;
-	int			replaces[4] = {0};
-	Datum		values[4] = {0};
-	bool		nulls[4] = {0};
-	int			nreplaces = 0;
 	Oid			locked_table = InvalidOid;
 
 	stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG);
@@ -101,6 +88,29 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 									  ShareUpdateExclusiveLock, 0,
 									  RangeVarCallbackForStats, &locked_table);
 
+	return relation_statistics_update_internal(reloid, fcinfo);
+}
+
+static bool
+relation_statistics_update_internal(Oid reloid, FunctionCallInfo fcinfo)
+{
+	BlockNumber relpages = 0;
+	bool		update_relpages = false;
+	float		reltuples = 0;
+	bool		update_reltuples = false;
+	BlockNumber relallvisible = 0;
+	bool		update_relallvisible = false;
+	BlockNumber relallfrozen = 0;
+	bool		update_relallfrozen = false;
+	Relation	crel;
+	HeapTuple	ctup;
+	Form_pg_class pgcform;
+	int			replaces[4] = {0};
+	Datum		values[4] = {0};
+	bool		nulls[4] = {0};
+	int			nreplaces = 0;
+	bool		result = true;
+
 	if (!PG_ARGISNULL(RELPAGES_ARG))
 	{
 		relpages = PG_GETARG_UINT32(RELPAGES_ARG);
@@ -241,3 +251,35 @@ pg_restore_relation_stats(PG_FUNCTION_ARGS)
 
 	PG_RETURN_BOOL(result);
 }
+
+/*
+ * Import relation statistics from NullableDatum inputs for all statitical
+ * values.
+ */
+bool
+import_relation_statistics(Relation rel,
+						   const NullableDatum *relpages,
+						   const NullableDatum *reltuples,
+						   const NullableDatum *relallvisible,
+						   const NullableDatum *relallfrozen)
+{
+	LOCAL_FCINFO(newfcinfo, NUM_RELATION_STATS_ARGS);
+
+	InitFunctionCallInfoData(*newfcinfo, NULL, NUM_RELATION_STATS_ARGS,
+							 InvalidOid, NULL, NULL);
+
+	newfcinfo->args[RELSCHEMA_ARG].value =
+		CStringGetTextDatum(get_namespace_name(RelationGetNamespace(rel)));
+	newfcinfo->args[RELSCHEMA_ARG].isnull = false;
+	newfcinfo->args[RELNAME_ARG].value =
+		CStringGetTextDatum(RelationGetRelationName(rel));
+	newfcinfo->args[RELNAME_ARG].isnull = false;
+
+	newfcinfo->args[RELPAGES_ARG] = *relpages;
+	newfcinfo->args[RELTUPLES_ARG] = *reltuples;
+	newfcinfo->args[RELALLVISIBLE_ARG] = *relallvisible;
+	newfcinfo->args[RELALLFROZEN_ARG] = *relallfrozen;
+
+	return relation_statistics_update_internal(RelationGetRelid(rel),
+											   newfcinfo);
+}
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index a673e3c704b..0d906f5fa76 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -32,6 +32,8 @@
 #include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/float.h"
+#include "utils/fmgroids.h"
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
 #include "utils/syscache.h"
@@ -753,3 +755,105 @@ statatt_init_empty_tuple(Oid reloid, int16 attnum, bool inherited,
 		nulls[Anum_pg_statistic_stacoll1 + slotnum - 1] = false;
 	}
 }
+
+/*
+ * Convenience routine for setting optional text arguments
+ */
+void
+stats_set_text_arg(NullableDatum *arg, const char *s)
+{
+	if (s)
+	{
+		arg->value = CStringGetTextDatum(s);
+		arg->isnull = false;
+	}
+	else
+	{
+		arg->value = (Datum) 0;
+		arg->isnull = true;
+	}
+}
+
+/*
+ * Convenience routine for setting optional int32 arguments
+ */
+void
+stats_set_int32_arg(NullableDatum *arg, const char *s)
+{
+	if (s)
+	{
+		int32		val = pg_strtoint32(s);
+
+		arg->value = Int32GetDatum(val);
+		arg->isnull = false;
+	}
+	else
+	{
+		arg->value = (Datum) 0;
+		arg->isnull = true;
+	}
+}
+
+/*
+ * Convenience routine for setting optional uint32 arguments
+ */
+void
+stats_set_uint32_arg(NullableDatum *arg, const char *s)
+{
+	if (s)
+	{
+		uint32		val = uint32in_subr(s, NULL, "uint32", NULL);
+
+		arg->value = UInt32GetDatum(val);
+		arg->isnull = false;
+	}
+	else
+	{
+		arg->value = (Datum) 0;
+		arg->isnull = true;
+	}
+}
+
+/*
+ * Convenience routine for setting optional float arguments
+ */
+void
+stats_set_float_arg(NullableDatum *arg, const char *s)
+{
+	if (s)
+	{
+		float4		val = float4in_internal((char *) s, NULL, "float", s, NULL);
+
+		arg->value = Float4GetDatum(val);
+		arg->isnull = false;
+	}
+	else
+	{
+		arg->value = (Datum) 0;
+		arg->isnull = true;
+	}
+}
+
+/*
+ * Convenience routine for setting optional float[] arguments
+ */
+void
+stats_set_floatarr_arg(NullableDatum *arg, const char *s)
+{
+	if (s)
+	{
+		FmgrInfo	flinfo;
+		Datum		val;
+
+		fmgr_info(F_ARRAY_IN, &flinfo);
+		val = InputFunctionCall(&flinfo, (char *) s, FLOAT4OID, -1);
+
+		arg->value = val;
+		arg->isnull = false;
+	}
+	else
+	{
+		arg->value = (Datum) 0;
+		arg->isnull = true;
+	}
+}
diff --git a/src/include/statistics/stat_utils.h b/src/include/statistics/stat_utils.h
index 74da7790579..17ca9f18c53 100644
--- a/src/include/statistics/stat_utils.h
+++ b/src/include/statistics/stat_utils.h
@@ -58,4 +58,10 @@ extern Datum statatt_build_stavalues(const char *staname, FmgrInfo *array_in, Da
 extern bool statatt_get_elem_type(Oid atttypid, char atttyptype,
 								  Oid *elemtypid, Oid *elem_eq_opr);
 
+extern void stats_set_text_arg(NullableDatum *arg, const char *s);
+extern void stats_set_int32_arg(NullableDatum *arg, const char *s);
+extern void stats_set_uint32_arg(NullableDatum *arg, const char *s);
+extern void stats_set_float_arg(NullableDatum *arg, const char *s);
+extern void stats_set_floatarr_arg(NullableDatum *arg, const char *s);
+
 #endif							/* STATS_UTILS_H */
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 8f9b9d237fd..7224e7a4e32 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -128,4 +128,27 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern bool import_relation_statistics(Relation rel,
+									   const NullableDatum *relpages,
+									   const NullableDatum *reltuples,
+									   const NullableDatum *relallvisible,
+									   const NullableDatum *relallfrozen);
+extern bool import_attribute_statistics(Relation rel,
+										AttrNumber attnum, bool inherited,
+										const NullableDatum *null_frac,
+										const NullableDatum *avg_width,
+										const NullableDatum *n_distinct,
+										const NullableDatum *most_common_vals,
+										const NullableDatum *most_common_freqs,
+										const NullableDatum *histogram_bounds,
+										const NullableDatum *correlation,
+										const NullableDatum *most_common_elems,
+										const NullableDatum *most_common_elem_freqs,
+										const NullableDatum *elem_count_histogram,
+										const NullableDatum *range_length_histogram,
+										const NullableDatum *range_empty_frac,
+										const NullableDatum *range_bounds_histogram);
+extern bool delete_attribute_statistics(Relation rel,
+										AttrNumber attnum, bool inherited);
+
 #endif							/* STATISTICS_H */


^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-17 10:23             ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-17 16:00               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-18 10:42                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-18 21:23                   ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-19 13:45                     ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-20 04:11                       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-22 19:08                         ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-23 12:10                           ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-23 15:53                             ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-29 08:14                               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-29 11:50                                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-29 18:17                                   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-30 01:10                                     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-30 12:29                                       ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-30 15:55                                         ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-07-01 12:09                                           ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
@ 2026-07-01 20:34                                             ` Corey Huinker <[email protected]>
  2026-07-03 09:47                                               ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  0 siblings, 1 reply; 33+ messages in thread

From: Corey Huinker @ 2026-07-01 20:34 UTC (permalink / raw)
  To: Etsuro Fujita <[email protected]>; +Cc: Robert Haas <[email protected]>; pgsql-hackers

On Wed, Jul 1, 2026 at 8:10 AM Etsuro Fujita <[email protected]>
wrote:

> On Wed, Jul 1, 2026 at 12:55 AM Corey Huinker <[email protected]>
> wrote:
> > On Tue, Jun 30, 2026 at 7:30 AM Etsuro Fujita <[email protected]>
> wrote:
>
> >> As postgresImportForeignStatistics() is provided for FDWs that want to
> >> calculate statistics remotely, so I'm assuming that they have enough
> >> knowledge about the stats-data structures.
>
> CORRECTION: s/postgresImportForeignStatistics()/ImportForeignStatistics()/
>  Sorry for that.
>
> >> In relation to this change, I moved helper functions set_XXX_arg()
> >> that you added to relation_stats.c and attribute_stats.c to
> >> postgres_fdw.c.  The helper functions had soft error handling, but in
> >> the postgres_fdw use, there is no need for that, so I removed that
> >> handling as well.
> >
> > My inclination would be to move the set_*_arg functions could be moved
> to some common utility file in v20, as other FDWs will find themselves with
> the same need.
>
> Seems like a good idea.  I moved the set_*_arg functions to
> stat_utils.c, and renamed them to stats_set_*_arg.  Attached is a new
> version of the patch.
>

I'm concerned that by making these non-error-safe function calls available,
we create a barrier to making those same functions error-safe in the future.

> As for removing the error-handling, it might still be handy because it
> would allow us to give a more context-aware error message, rather than the
> very narrow "this_string is an invalid this_type", for string that the user
> most certainly never saw. Having said that, it's something we could easily
> change back to error-safe if we wanted to.
>
> Ok, I think we could do so later if needed.
>

I agree we could do it later if they were private functions, no problem.
But if they're available outside of postgres_fdw then changing them to be
error-safe potentially disrupts other callers. Someone please let me know
if I'm being unnecessarily cautious here.

I modified the patch further.  To support these differences:
>
> "There are important differences in the parameters needed by the
> different types of functions. The pg_restore_*() SQL function calls
> need to identify the schema+relname of the relation being modified,
> and already have all of the values as typed Datums, whereas the
> internal API calls already have an identified and locked open
> Relation, but all of the statistical parameters are just cstrings."
>
> I incorporated your v2-0010 and v2-0012 into the patch: 1) separate
> the guts of relation_statistics_update() and
> attribute_statistics_update() into new functions
> relation_statistics_update_internal() and
> attribute_statistics_update_internal(), and 2) modify
> import_relation_statistics() and import_attribute_statistics() to call
> these new functions instead.  Also, I changed the stats-data
> argument-type in import_relation_statistics() and
> import_attribute_statistics() from NullableDatum * to const
> NullableDatum *.
>
> Best regards,
> Etsuro Fujita
>

Other Thoughts:

1. The internal functions should accept a server_version_number parameter,
and we should document that setting that parameter to 0 mean that we can
assume the current version. I know that we presently have no translation
issues going forward with statistics, but someday that won't be the case,
and if we don't have this parameter in place then it'll be too late to fix
it.

2. Did you put import_relation_statistics and import_attribute_statistics
in statistics.h because you see them as long-term publicly visible
functions, and what's in stats_utils.h to be more subject to change? If so,
I could get behind that. If not, then I fall back to my position that they
seem like they belong in new relation_stats.h and attribute_stats.h, or
stats_utils.h.

3. The import_stats_functions in their current "bridging" form should do
null checks on all of the NullableDatum pointers, or at least Assert()s on
them.


^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-17 10:23             ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-17 16:00               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-18 10:42                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-18 21:23                   ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-19 13:45                     ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-20 04:11                       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-22 19:08                         ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-23 12:10                           ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-23 15:53                             ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-29 08:14                               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-29 11:50                                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-29 18:17                                   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-30 01:10                                     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-30 12:29                                       ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-30 15:55                                         ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-07-01 12:09                                           ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-07-01 20:34                                             ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
@ 2026-07-03 09:47                                               ` Etsuro Fujita <[email protected]>
  2026-07-08 12:04                                                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  0 siblings, 1 reply; 33+ messages in thread

From: Etsuro Fujita @ 2026-07-03 09:47 UTC (permalink / raw)
  To: Corey Huinker <[email protected]>; +Cc: Robert Haas <[email protected]>; pgsql-hackers

On Thu, Jul 2, 2026 at 5:35 AM Corey Huinker <[email protected]> wrote:
> On Wed, Jul 1, 2026 at 8:10 AM Etsuro Fujita <[email protected]> wrote:
>> On Wed, Jul 1, 2026 at 12:55 AM Corey Huinker <[email protected]> wrote:

>> >> In relation to this change, I moved helper functions set_XXX_arg()
>> >> that you added to relation_stats.c and attribute_stats.c to
>> >> postgres_fdw.c.  The helper functions had soft error handling, but in
>> >> the postgres_fdw use, there is no need for that, so I removed that
>> >> handling as well.
>> >
>> > My inclination would be to move the set_*_arg functions could be moved to some common utility file in v20, as other FDWs will find themselves with the same need.
>>
>> Seems like a good idea.  I moved the set_*_arg functions to
>> stat_utils.c, and renamed them to stats_set_*_arg.  Attached is a new
>> version of the patch.
>
> I'm concerned that by making these non-error-safe function calls available, we create a barrier to making those same functions error-safe in the future.
>
>> > As for removing the error-handling, it might still be handy because it would allow us to give a more context-aware error message, rather than the very narrow "this_string is an invalid this_type", for string that the user most certainly never saw. Having said that, it's something we could easily change back to error-safe if we wanted to.
>>
>> Ok, I think we could do so later if needed.
>
> I agree we could do it later if they were private functions, no problem. But if they're available outside of postgres_fdw then changing them to be error-safe potentially disrupts other callers. Someone please let me know if I'm being unnecessarily cautious here.

Ah, I misunderstood your comments.  I agree with you here, so I'll
move back the functions to postgres_fdw.c.

> Other Thoughts:
>
> 1. The internal functions should accept a server_version_number parameter, and we should document that setting that parameter to 0 mean that we can assume the current version. I know that we presently have no translation issues going forward with statistics, but someday that won't be the case, and if we don't have this parameter in place then it'll be too late to fix it.

Good idea!  Will do.

> 2. Did you put import_relation_statistics and import_attribute_statistics in statistics.h because you see them as long-term publicly visible functions, and what's in stats_utils.h to be more subject to change? If so, I could get behind that. If not, then I fall back to my position that they seem like they belong in new relation_stats.h and attribute_stats.h, or stats_utils.h.

The answer is the former.  I'll remove the changes made to
stats_utils.h, though.

> 3. The import_stats_functions in their current "bridging" form should do null checks on all of the NullableDatum pointers, or at least Assert()s on them.

Will do.

Thanks for the comments!

Best regards,
Etsuro Fujita






^ permalink  raw  reply  [nested|flat] 33+ messages in thread

* Re: use of SPI by postgresImportForeignStatistics
  2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 12:04 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-16 14:34   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 18:08     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 19:11       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-16 21:53         ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-16 22:50           ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-17 10:23             ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-17 16:00               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-18 10:42                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-18 21:23                   ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-19 13:45                     ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-20 04:11                       ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-22 19:08                         ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-23 12:10                           ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-23 15:53                             ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-29 08:14                               ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-29 11:50                                 ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-29 18:17                                   ` Re: use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
  2026-06-30 01:10                                     ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-06-30 12:29                                       ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-06-30 15:55                                         ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-07-01 12:09                                           ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
  2026-07-01 20:34                                             ` Re: use of SPI by postgresImportForeignStatistics Corey Huinker <[email protected]>
  2026-07-03 09:47                                               ` Re: use of SPI by postgresImportForeignStatistics Etsuro Fujita <[email protected]>
@ 2026-07-08 12:04                                                 ` Etsuro Fujita <[email protected]>
  0 siblings, 0 replies; 33+ messages in thread

From: Etsuro Fujita @ 2026-07-08 12:04 UTC (permalink / raw)
  To: Corey Huinker <[email protected]>; +Cc: Robert Haas <[email protected]>; pgsql-hackers

On Fri, Jul 3, 2026 at 6:47 PM Etsuro Fujita <[email protected]> wrote:
> On Thu, Jul 2, 2026 at 5:35 AM Corey Huinker <[email protected]> wrote:
> > On Wed, Jul 1, 2026 at 8:10 AM Etsuro Fujita <[email protected]> wrote:
> >> On Wed, Jul 1, 2026 at 12:55 AM Corey Huinker <[email protected]> wrote:
>
> >> >> In relation to this change, I moved helper functions set_XXX_arg()
> >> >> that you added to relation_stats.c and attribute_stats.c to
> >> >> postgres_fdw.c.  The helper functions had soft error handling, but in
> >> >> the postgres_fdw use, there is no need for that, so I removed that
> >> >> handling as well.
> >> >
> >> > My inclination would be to move the set_*_arg functions could be moved to some common utility file in v20, as other FDWs will find themselves with the same need.
> >>
> >> Seems like a good idea.  I moved the set_*_arg functions to
> >> stat_utils.c, and renamed them to stats_set_*_arg.  Attached is a new
> >> version of the patch.
> >
> > I'm concerned that by making these non-error-safe function calls available, we create a barrier to making those same functions error-safe in the future.
> >
> >> > As for removing the error-handling, it might still be handy because it would allow us to give a more context-aware error message, rather than the very narrow "this_string is an invalid this_type", for string that the user most certainly never saw. Having said that, it's something we could easily change back to error-safe if we wanted to.
> >>
> >> Ok, I think we could do so later if needed.
> >
> > I agree we could do it later if they were private functions, no problem. But if they're available outside of postgres_fdw then changing them to be error-safe potentially disrupts other callers. Someone please let me know if I'm being unnecessarily cautious here.
>
> Ah, I misunderstood your comments.  I agree with you here, so I'll
> move back the functions to postgres_fdw.c.

Done.

> > Other Thoughts:
> >
> > 1. The internal functions should accept a server_version_number parameter, and we should document that setting that parameter to 0 mean that we can assume the current version. I know that we presently have no translation issues going forward with statistics, but someday that won't be the case, and if we don't have this parameter in place then it'll be too late to fix it.
>
> Good idea!  Will do.

I used the name "version" for the consistency with
pg_restore_relation_stats/pg_restore_attribute_stats.  Also, as I
couldn't find a note about the arrangement in the source code, I added
this comment to match a comment in stats_fill_fcinfo_from_arg_pairs:

 * For now, the 'version' argument is ignored. In the future it can be used
 * to interpret older statistics properly.

> > 3. The import_stats_functions in their current "bridging" form should do null checks on all of the NullableDatum pointers, or at least Assert()s on them.
>
> Will do.

Done.  As the functions are provided for developers, I just added the
assertions.

Also, I updated comments/docs a little bit.  Attached is a new version
of the patch.  I'm planning to push and back-patch it, if no
objections.

Best regards,
Etsuro Fujita


Attachments:

  [application/octet-stream] v3-Remove-SPI-from-stat-import-in-postgres-fdw-efujita.patch (34.0K, ../../CAPmGK16-=HvkijvSiDfo8=JmrHQ4ra79gSnJbKzW4Oob9yPfdQ@mail.gmail.com/2-v3-Remove-SPI-from-stat-import-in-postgres-fdw-efujita.patch)
  download | inline diff:
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 5ebae1cedc2..048f624ba0c 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -13067,6 +13067,43 @@ 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_ftable's stats should now exactly match dtest_table's
+-- 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)
+
+-- 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)
+
+-- 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 0ea72a64ce5..e6e0a4ab9b5 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"
@@ -47,6 +46,7 @@
 #include "storage/latch.h"
 #include "utils/builtins.h"
 #include "utils/float.h"
+#include "utils/fmgroids.h"
 #include "utils/guc.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
@@ -337,9 +337,9 @@ typedef struct
 {
 	PGresult   *rel;
 	PGresult   *att;
-	int			server_version_num;
 	double		livetuples;
 	double		deadtuples;
+	int			version;
 } RemoteStatsResults;
 
 /* Column order in relation stats query */
@@ -371,136 +371,6 @@ enum AttStatsColumns
 	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,
-};
-
 /*
  * SQL functions
  */
@@ -718,14 +588,18 @@ 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 char *get_opt_value(PGresult *res, int row, int col);
+static void set_text_arg(NullableDatum *arg, const char *s);
+static void set_int32_arg(NullableDatum *arg, const char *s);
+static void set_uint32_arg(NullableDatum *arg, const char *s);
+static void set_float_arg(NullableDatum *arg, const char *s);
+static void set_floatarr_arg(NullableDatum *arg, const char *s);
 static void produce_tuple_asynchronously(AsyncRequest *areq, bool fetch);
 static void fetch_more_data_begin(AsyncRequest *areq);
 static void complete_pending_request(AsyncRequest *areq);
@@ -5668,7 +5542,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 +5612,7 @@ fetch_remote_statistics(Relation relation,
 	 */
 	user = GetUserMapping(GetUserId(), table->serverid);
 	conn = GetConnection(user, false, NULL);
-	remstats->server_version_num = server_version_num = PQserverVersion(conn);
+	remstats->version = server_version_num = PQserverVersion(conn);
 
 	/* Fetch relation stats. */
 	remstats->rel = relstats = fetch_relstats(conn, relation);
@@ -6144,191 +6018,232 @@ match_attrmap(PGresult *res,
  * 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		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] = ' ';
+	PGresult   *res;
+	NullableDatum args[ATTSTATS_NUM_FIELDS];
 
-	SPI_connect();
+	/* Set the 'version' parameter, which is common to both statistics. */
+	args[0].value = UInt32GetDatum(remstats->version);
+	args[0].isnull = false;
 
 	/*
 	 * We import attribute statistics first, if any, because those are more
 	 * prone to errors.  This avoids making a modification of pg_class that
 	 * will just get rolled back by a failed attribute import.
 	 */
-	if (remstats->att != NULL)
+	res = remstats->att;
+	if (res != NULL)
 	{
-		Assert(PQnfields(remstats->att) == ATTSTATS_NUM_FIELDS);
-		Assert(PQntuples(remstats->att) >= 1);
-
-		attimport_plan = SPI_prepare(attimport_sql, ATTIMPORT_SQL_NUM_FIELDS,
-									 attimport_argtypes);
-		if (attimport_plan == NULL)
-			elog(ERROR, "failed to prepare attimport_sql query");
-
-		attclear_plan = SPI_prepare(attclear_sql, ATTCLEAR_SQL_NUM_FIELDS,
-									attclear_argtypes);
-		if (attclear_plan == NULL)
-			elog(ERROR, "failed to prepare attclear_sql query");
-
-		nulls[ATTIMPORT_SQL_ATTNUM] = ' ';
+		Assert(PQnfields(res) == ATTSTATS_NUM_FIELDS);
+		Assert(PQntuples(res) >= 1);
 
 		for (int mapidx = 0; mapidx < attrcnt; mapidx++)
 		{
 			int			row = remattrmap[mapidx].res_index;
-			Datum	   *values2 = values + 1;
-			char	   *nulls2 = nulls + 1;
+			AttrNumber	attnum = remattrmap[mapidx].local_attnum;
 
 			/* All mappings should have been assigned a result set row. */
 			Assert(row >= 0);
 
-			/*
-			 * Check for user-requested abort.
-			 */
+			/* Check for user-requested abort. */
 			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())
+			/* Clear existing attribute statistics. */
+			delete_attribute_statistics(relation, attnum, false);
+
+			/* Set the remaining parameters. */
+			set_float_arg(&args[1],
+						  get_opt_value(res, row, ATTSTATS_NULL_FRAC));
+			set_int32_arg(&args[2],
+						  get_opt_value(res, row, ATTSTATS_AVG_WIDTH));
+			set_float_arg(&args[3],
+						  get_opt_value(res, row, ATTSTATS_N_DISTINCT));
+			set_text_arg(&args[4],
+						 get_opt_value(res, row, ATTSTATS_MOST_COMMON_VALS));
+			set_floatarr_arg(&args[5],
+							 get_opt_value(res, row, ATTSTATS_MOST_COMMON_FREQS));
+			set_text_arg(&args[6],
+						 get_opt_value(res, row, ATTSTATS_HISTOGRAM_BOUNDS));
+			set_float_arg(&args[7],
+						  get_opt_value(res, row, ATTSTATS_CORRELATION));
+			set_text_arg(&args[8],
+						 get_opt_value(res, row, ATTSTATS_MOST_COMMON_ELEMS));
+			set_floatarr_arg(&args[9],
+							 get_opt_value(res, row, ATTSTATS_MOST_COMMON_ELEM_FREQS));
+			set_floatarr_arg(&args[10],
+							 get_opt_value(res, row, ATTSTATS_ELEM_COUNT_HISTOGRAM));
+			set_text_arg(&args[11],
+						 get_opt_value(res, row, ATTSTATS_RANGE_LENGTH_HISTOGRAM));
+			set_float_arg(&args[12],
+						  get_opt_value(res, row, ATTSTATS_RANGE_EMPTY_FRAC));
+			set_text_arg(&args[13],
+						 get_opt_value(res, row, ATTSTATS_RANGE_BOUNDS_HISTOGRAM));
+
+			/* Try to import the statistics. */
+			if (!import_attribute_statistics(relation, attnum, false,
+											 &args[0], &args[1], &args[2],
+											 &args[3], &args[4], &args[5],
+											 &args[6], &args[7], &args[8],
+											 &args[9], &args[10], &args[11],
+											 &args[12], &args[13]))
 			{
 				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));
-				goto import_cleanup;
+				return false;
 			}
 		}
 	}
 
 	/*
-	 * 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.
-	 */
-	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,
-								  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())
+	 * Import relation statistics.
+	 */
+	res = remstats->rel;
+	Assert(res != NULL);
+	Assert(PQnfields(res) == RELSTATS_NUM_FIELDS);
+	Assert(PQntuples(res) == 1);
+
+	/* Set the remaining parameters. */
+	set_uint32_arg(&args[1], get_opt_value(res, 0, RELSTATS_RELPAGES));
+	Assert(!args[1].isnull);
+	set_float_arg(&args[2], get_opt_value(res, 0, RELSTATS_RELTUPLES));
+	Assert(!args[2].isnull);
+	args[3].value = (Datum) 0;
+	args[3].isnull = true;
+	args[4].value = (Datum) 0;
+	args[4].isnull = true;
+
+	/* Try to import the statistics. */
+	if (!import_relation_statistics(relation, &args[0], &args[1],
+									&args[2], &args[3], &args[4]))
 	{
 		ereport(WARNING,
 				errmsg("could not import statistics for foreign table \"%s.%s\" --- relation statistics import failed for this foreign table",
 					   schemaname, relname));
-		goto import_cleanup;
+		return false;
 	}
 
-	ok = true;
+	return true;
+}
 
-import_cleanup:
-	if (attimport_plan)
-		SPI_freeplan(attimport_plan);
-	if (attclear_plan)
-		SPI_freeplan(attclear_plan);
-	SPI_finish();
-	return ok;
+/*
+ * Conenience routine to fetch the value for the row/column of the PGresult
+ */
+static char *
+get_opt_value(PGresult *res, int row, int col)
+{
+	if (PQgetisnull(res, row, col))
+		return NULL;
+	return PQgetvalue(res, row, col);
 }
 
 /*
- * Move a string value from a result set to a Text value of a Datum array.
+ * Convenience routine for setting optional text arguments
  */
-static void
-map_field_to_arg(PGresult *res, int row, int field,
-				 int arg, Datum *values, char *nulls)
+void
+set_text_arg(NullableDatum *arg, const char *s)
 {
-	if (PQgetisnull(res, row, field))
+	if (s)
 	{
-		values[arg] = (Datum) 0;
-		nulls[arg] = 'n';
+		arg->value = CStringGetTextDatum(s);
+		arg->isnull = false;
 	}
 	else
 	{
-		const char *s = PQgetvalue(res, row, field);
+		arg->value = (Datum) 0;
+		arg->isnull = true;
+	}
+}
 
-		values[arg] = CStringGetTextDatum(s);
-		nulls[arg] = ' ';
+/*
+ * Convenience routine for setting optional int32 arguments
+ */
+void
+set_int32_arg(NullableDatum *arg, const char *s)
+{
+	if (s)
+	{
+		int32		val = pg_strtoint32(s);
+
+		arg->value = Int32GetDatum(val);
+		arg->isnull = false;
+	}
+	else
+	{
+		arg->value = (Datum) 0;
+		arg->isnull = true;
 	}
 }
 
 /*
- * Check the 1x1 result set of a pg_restore_*_stats() command for success.
+ * Convenience routine for setting optional uint32 arguments
  */
-static bool
-import_spi_query_ok(void)
+void
+set_uint32_arg(NullableDatum *arg, const char *s)
 {
-	TupleDesc	tupdesc;
-	Datum		dat;
-	bool		isnull;
+	if (s)
+	{
+		uint32		val = uint32in_subr(s, NULL, "uint32", NULL);
 
-	Assert(SPI_tuptable != NULL);
-	Assert(SPI_processed == 1);
+		arg->value = UInt32GetDatum(val);
+		arg->isnull = false;
+	}
+	else
+	{
+		arg->value = (Datum) 0;
+		arg->isnull = true;
+	}
+}
+
+/*
+ * Convenience routine for setting optional float arguments
+ */
+void
+set_float_arg(NullableDatum *arg, const char *s)
+{
+	if (s)
+	{
+		float4		val = float4in_internal((char *) s, NULL, "float", s, NULL);
+
+		arg->value = Float4GetDatum(val);
+		arg->isnull = false;
+	}
+	else
+	{
+		arg->value = (Datum) 0;
+		arg->isnull = true;
+	}
+}
+
+/*
+ * Convenience routine for setting optional float[] arguments
+ */
+void
+set_floatarr_arg(NullableDatum *arg, const char *s)
+{
+	if (s)
+	{
+		FmgrInfo	flinfo;
+		Datum		val;
 
-	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);
+		fmgr_info(F_ARRAY_IN, &flinfo);
+		val = InputFunctionCall(&flinfo, (char *) s, FLOAT4OID, -1);
 
-	return DatumGetBool(dat);
+		arg->value = val;
+		arg->isnull = false;
+	}
+	else
+	{
+		arg->value = (Datum) 0;
+		arg->isnull = true;
+	}
 }
 
 /*
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index e868da00ace..ed2c8b58e60 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -4652,6 +4652,34 @@ ANALYZE dtest_table;
 
 ANALYZE VERBOSE dtest_ftable;             -- should work
 
+-- dtest_ftable's stats should now exactly match dtest_table's
+-- 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;
+
+-- 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';
+
+-- 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;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 8685a078c52..0103fdacfdf 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -1438,8 +1438,9 @@ ImportForeignStatistics(Relation relation,
      <productname>PostgreSQL</productname> is found in
      <filename>src/backend/command/analyze.c</filename>.
      It's recommended to import table-level and column-level statistics for the
-     foreign table using <function>pg_restore_relation_stats</function> and
-     <function>pg_restore_attribute_stats</function>, respectively.
+     foreign table using <function>import_relation_statistics</function>,
+     <function>import_attribute_statistics</function>, and
+     <function>delete_attribute_statistics</function>.
     </para>
 
     <para>
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 1cc4d657231..ba22671cbf9 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -105,6 +105,11 @@ static struct StatsArgInfo cleararginfo[] =
 };
 
 static bool attribute_statistics_update(FunctionCallInfo fcinfo);
+static bool attribute_statistics_update_internal(Oid reloid,
+												 const char *attname,
+												 AttrNumber attnum,
+												 bool inherited,
+												 FunctionCallInfo fcinfo);
 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);
@@ -136,38 +141,6 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	bool		inherited;
 	Oid			locked_table = InvalidOid;
 
-	Relation	starel;
-	HeapTuple	statup;
-
-	Oid			atttypid = InvalidOid;
-	int32		atttypmod;
-	char		atttyptype;
-	Oid			atttypcoll = InvalidOid;
-	Oid			eq_opr = InvalidOid;
-	Oid			lt_opr = InvalidOid;
-
-	Oid			elemtypid = InvalidOid;
-	Oid			elem_eq_opr = InvalidOid;
-
-	FmgrInfo	array_in_fn;
-
-	bool		do_mcv = !PG_ARGISNULL(MOST_COMMON_FREQS_ARG) &&
-		!PG_ARGISNULL(MOST_COMMON_VALS_ARG);
-	bool		do_histogram = !PG_ARGISNULL(HISTOGRAM_BOUNDS_ARG);
-	bool		do_correlation = !PG_ARGISNULL(CORRELATION_ARG);
-	bool		do_mcelem = !PG_ARGISNULL(MOST_COMMON_ELEMS_ARG) &&
-		!PG_ARGISNULL(MOST_COMMON_ELEM_FREQS_ARG);
-	bool		do_dechist = !PG_ARGISNULL(ELEM_COUNT_HISTOGRAM_ARG);
-	bool		do_bounds_histogram = !PG_ARGISNULL(RANGE_BOUNDS_HISTOGRAM_ARG);
-	bool		do_range_length_histogram = !PG_ARGISNULL(RANGE_LENGTH_HISTOGRAM_ARG) &&
-		!PG_ARGISNULL(RANGE_EMPTY_FRAC_ARG);
-
-	Datum		values[Natts_pg_statistic] = {0};
-	bool		nulls[Natts_pg_statistic] = {0};
-	bool		replaces[Natts_pg_statistic] = {0};
-
-	bool		result = true;
-
 	stats_check_required_arg(fcinfo, attarginfo, ATTRELSCHEMA_ARG);
 	stats_check_required_arg(fcinfo, attarginfo, ATTRELNAME_ARG);
 
@@ -231,6 +204,50 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG);
 	inherited = PG_GETARG_BOOL(INHERITED_ARG);
 
+	return attribute_statistics_update_internal(reloid, attname, attnum,
+												inherited, fcinfo);
+}
+
+/*
+ * Workhorse function for attribute_statistics_update.
+ */
+static bool
+attribute_statistics_update_internal(Oid reloid,
+									 const char *attname, AttrNumber attnum,
+									 bool inherited, FunctionCallInfo fcinfo)
+{
+	Relation	starel;
+	HeapTuple	statup;
+
+	Oid			atttypid = InvalidOid;
+	int32		atttypmod;
+	char		atttyptype;
+	Oid			atttypcoll = InvalidOid;
+	Oid			eq_opr = InvalidOid;
+	Oid			lt_opr = InvalidOid;
+
+	Oid			elemtypid = InvalidOid;
+	Oid			elem_eq_opr = InvalidOid;
+
+	FmgrInfo	array_in_fn;
+
+	bool		do_mcv = !PG_ARGISNULL(MOST_COMMON_FREQS_ARG) &&
+		!PG_ARGISNULL(MOST_COMMON_VALS_ARG);
+	bool		do_histogram = !PG_ARGISNULL(HISTOGRAM_BOUNDS_ARG);
+	bool		do_correlation = !PG_ARGISNULL(CORRELATION_ARG);
+	bool		do_mcelem = !PG_ARGISNULL(MOST_COMMON_ELEMS_ARG) &&
+		!PG_ARGISNULL(MOST_COMMON_ELEM_FREQS_ARG);
+	bool		do_dechist = !PG_ARGISNULL(ELEM_COUNT_HISTOGRAM_ARG);
+	bool		do_bounds_histogram = !PG_ARGISNULL(RANGE_BOUNDS_HISTOGRAM_ARG);
+	bool		do_range_length_histogram = !PG_ARGISNULL(RANGE_LENGTH_HISTOGRAM_ARG) &&
+		!PG_ARGISNULL(RANGE_EMPTY_FRAC_ARG);
+
+	Datum		values[Natts_pg_statistic] = {0};
+	bool		nulls[Natts_pg_statistic] = {0};
+	bool		replaces[Natts_pg_statistic] = {0};
+
+	bool		result = true;
+
 	/*
 	 * Check argument sanity. If some arguments are unusable, emit a WARNING
 	 * and set the corresponding argument to NULL in fcinfo.
@@ -688,3 +705,96 @@ pg_restore_attribute_stats(PG_FUNCTION_ARGS)
 
 	PG_RETURN_BOOL(result);
 }
+
+/*
+ * Import attribute statistics from NullableDatum inputs for all statitical
+ * values.
+ *
+ * For now, the 'version' argument is ignored. In the future it can be used
+ * to interpret older statistics properly.
+ */
+bool
+import_attribute_statistics(Relation rel, AttrNumber attnum, bool inherited,
+							const NullableDatum *version,
+							const NullableDatum *null_frac,
+							const NullableDatum *avg_width,
+							const NullableDatum *n_distinct,
+							const NullableDatum *most_common_vals,
+							const NullableDatum *most_common_freqs,
+							const NullableDatum *histogram_bounds,
+							const NullableDatum *correlation,
+							const NullableDatum *most_common_elems,
+							const NullableDatum *most_common_elem_freqs,
+							const NullableDatum *elem_count_histogram,
+							const NullableDatum *range_length_histogram,
+							const NullableDatum *range_empty_frac,
+							const NullableDatum *range_bounds_histogram)
+{
+	LOCAL_FCINFO(newfcinfo, NUM_ATTRIBUTE_STATS_ARGS);
+	Oid			reloid = RelationGetRelid(rel);
+	char	   *relname = RelationGetRelationName(rel);
+	char	   *attname  = get_attname(reloid, attnum, true);
+
+	Assert(null_frac);
+	Assert(avg_width);
+	Assert(n_distinct);
+	Assert(most_common_vals);
+	Assert(most_common_freqs);
+	Assert(histogram_bounds);
+	Assert(correlation);
+	Assert(most_common_elems);
+	Assert(most_common_elem_freqs);
+	Assert(elem_count_histogram);
+	Assert(range_length_histogram);
+	Assert(range_empty_frac);
+	Assert(range_bounds_histogram);
+
+	/* 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)));
+
+	InitFunctionCallInfoData(*newfcinfo, NULL, NUM_ATTRIBUTE_STATS_ARGS,
+							 InvalidOid, NULL, NULL);
+
+	newfcinfo->args[ATTRELSCHEMA_ARG].value =
+		CStringGetTextDatum(get_namespace_name(RelationGetNamespace(rel)));
+	newfcinfo->args[ATTRELSCHEMA_ARG].isnull = false;
+	newfcinfo->args[ATTRELNAME_ARG].value = CStringGetTextDatum(relname);
+	newfcinfo->args[ATTRELNAME_ARG].isnull = false;
+	newfcinfo->args[ATTNAME_ARG].value = CStringGetTextDatum(attname);
+	newfcinfo->args[ATTNAME_ARG].isnull = false;
+	newfcinfo->args[ATTNUM_ARG].value = Int16GetDatum(attnum);
+	newfcinfo->args[ATTNUM_ARG].isnull = false;
+	newfcinfo->args[INHERITED_ARG].value = BoolGetDatum(inherited);
+	newfcinfo->args[INHERITED_ARG].isnull = false;
+
+	newfcinfo->args[NULL_FRAC_ARG] = *null_frac;
+	newfcinfo->args[AVG_WIDTH_ARG] = *avg_width;
+	newfcinfo->args[N_DISTINCT_ARG] = *n_distinct;
+	newfcinfo->args[MOST_COMMON_VALS_ARG] = *most_common_vals;
+	newfcinfo->args[MOST_COMMON_FREQS_ARG] = *most_common_freqs;
+	newfcinfo->args[HISTOGRAM_BOUNDS_ARG] = *histogram_bounds;
+	newfcinfo->args[CORRELATION_ARG] = *correlation;
+	newfcinfo->args[MOST_COMMON_ELEMS_ARG] = *most_common_elems;
+	newfcinfo->args[MOST_COMMON_ELEM_FREQS_ARG] = *most_common_elem_freqs;
+	newfcinfo->args[ELEM_COUNT_HISTOGRAM_ARG] = *elem_count_histogram;
+	newfcinfo->args[RANGE_LENGTH_HISTOGRAM_ARG] = *range_length_histogram;
+	newfcinfo->args[RANGE_EMPTY_FRAC_ARG] = *range_empty_frac;
+	newfcinfo->args[RANGE_BOUNDS_HISTOGRAM_ARG] = *range_bounds_histogram;
+
+	return attribute_statistics_update_internal(reloid, attname, attnum,
+												inherited, newfcinfo);
+}
+
+/*
+ * Delete attribute statistics.
+ */
+bool
+delete_attribute_statistics(Relation rel, AttrNumber attnum, bool inherited)
+{
+	return delete_pg_statistic(RelationGetRelid(rel), attnum, inherited);
+}
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index d6631e9a9a4..990a7511d04 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -21,6 +21,7 @@
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
 #include "nodes/makefuncs.h"
+#include "statistics/statistics.h"
 #include "statistics/stat_utils.h"
 #include "utils/builtins.h"
 #include "utils/fmgroids.h"
@@ -57,6 +58,8 @@ static struct StatsArgInfo relarginfo[] =
 };
 
 static bool relation_statistics_update(FunctionCallInfo fcinfo);
+static bool relation_statistics_update_internal(Oid reloid,
+												FunctionCallInfo fcinfo);
 
 /*
  * Internal function for modifying statistics for a relation.
@@ -64,25 +67,9 @@ static bool relation_statistics_update(FunctionCallInfo fcinfo);
 static bool
 relation_statistics_update(FunctionCallInfo fcinfo)
 {
-	bool		result = true;
 	char	   *nspname;
 	char	   *relname;
 	Oid			reloid;
-	Relation	crel;
-	BlockNumber relpages = 0;
-	bool		update_relpages = false;
-	float		reltuples = 0;
-	bool		update_reltuples = false;
-	BlockNumber relallvisible = 0;
-	bool		update_relallvisible = false;
-	BlockNumber relallfrozen = 0;
-	bool		update_relallfrozen = false;
-	HeapTuple	ctup;
-	Form_pg_class pgcform;
-	int			replaces[4] = {0};
-	Datum		values[4] = {0};
-	bool		nulls[4] = {0};
-	int			nreplaces = 0;
 	Oid			locked_table = InvalidOid;
 
 	stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG);
@@ -101,6 +88,32 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 									  ShareUpdateExclusiveLock, 0,
 									  RangeVarCallbackForStats, &locked_table);
 
+	return relation_statistics_update_internal(reloid, fcinfo);
+}
+
+/*
+ * Workhorse function for relation_statistics_update.
+ */
+static bool
+relation_statistics_update_internal(Oid reloid, FunctionCallInfo fcinfo)
+{
+	BlockNumber relpages = 0;
+	bool		update_relpages = false;
+	float		reltuples = 0;
+	bool		update_reltuples = false;
+	BlockNumber relallvisible = 0;
+	bool		update_relallvisible = false;
+	BlockNumber relallfrozen = 0;
+	bool		update_relallfrozen = false;
+	Relation	crel;
+	HeapTuple	ctup;
+	Form_pg_class pgcform;
+	int			replaces[4] = {0};
+	Datum		values[4] = {0};
+	bool		nulls[4] = {0};
+	int			nreplaces = 0;
+	bool		result = true;
+
 	if (!PG_ARGISNULL(RELPAGES_ARG))
 	{
 		relpages = PG_GETARG_UINT32(RELPAGES_ARG);
@@ -241,3 +254,44 @@ pg_restore_relation_stats(PG_FUNCTION_ARGS)
 
 	PG_RETURN_BOOL(result);
 }
+
+/*
+ * Import relation statistics from NullableDatum inputs for all statitical
+ * values.
+ *
+ * For now, the 'version' argument is ignored. In the future it can be used
+ * to interpret older statistics properly.
+ */
+bool
+import_relation_statistics(Relation rel,
+						   const NullableDatum *version,
+						   const NullableDatum *relpages,
+						   const NullableDatum *reltuples,
+						   const NullableDatum *relallvisible,
+						   const NullableDatum *relallfrozen)
+{
+	LOCAL_FCINFO(newfcinfo, NUM_RELATION_STATS_ARGS);
+
+	Assert(relpages);
+	Assert(reltuples);
+	Assert(relallvisible);
+	Assert(relallfrozen);
+
+	InitFunctionCallInfoData(*newfcinfo, NULL, NUM_RELATION_STATS_ARGS,
+							 InvalidOid, NULL, NULL);
+
+	newfcinfo->args[RELSCHEMA_ARG].value =
+		CStringGetTextDatum(get_namespace_name(RelationGetNamespace(rel)));
+	newfcinfo->args[RELSCHEMA_ARG].isnull = false;
+	newfcinfo->args[RELNAME_ARG].value =
+		CStringGetTextDatum(RelationGetRelationName(rel));
+	newfcinfo->args[RELNAME_ARG].isnull = false;
+
+	newfcinfo->args[RELPAGES_ARG] = *relpages;
+	newfcinfo->args[RELTUPLES_ARG] = *reltuples;
+	newfcinfo->args[RELALLVISIBLE_ARG] = *relallvisible;
+	newfcinfo->args[RELALLFROZEN_ARG] = *relallfrozen;
+
+	return relation_statistics_update_internal(RelationGetRelid(rel),
+											   newfcinfo);
+}
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 8f9b9d237fd..0b163103a72 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -128,4 +128,29 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern bool import_relation_statistics(Relation rel,
+									   const NullableDatum *version,
+									   const NullableDatum *relpages,
+									   const NullableDatum *reltuples,
+									   const NullableDatum *relallvisible,
+									   const NullableDatum *relallfrozen);
+extern bool import_attribute_statistics(Relation rel,
+										AttrNumber attnum, bool inherited,
+										const NullableDatum *version,
+										const NullableDatum *null_frac,
+										const NullableDatum *avg_width,
+										const NullableDatum *n_distinct,
+										const NullableDatum *most_common_vals,
+										const NullableDatum *most_common_freqs,
+										const NullableDatum *histogram_bounds,
+										const NullableDatum *correlation,
+										const NullableDatum *most_common_elems,
+										const NullableDatum *most_common_elem_freqs,
+										const NullableDatum *elem_count_histogram,
+										const NullableDatum *range_length_histogram,
+										const NullableDatum *range_empty_frac,
+										const NullableDatum *range_bounds_histogram);
+extern bool delete_attribute_statistics(Relation rel,
+										AttrNumber attnum, bool inherited);
+
 #endif							/* STATISTICS_H */


^ permalink  raw  reply  [nested|flat] 33+ messages in thread


end of thread, other threads:[~2026-07-08 12:04 UTC | newest]

Thread overview: 33+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-06-15 17:50 use of SPI by postgresImportForeignStatistics Robert Haas <[email protected]>
2026-06-16 12:04 ` Etsuro Fujita <[email protected]>
2026-06-16 14:34   ` Robert Haas <[email protected]>
2026-06-16 18:08     ` Corey Huinker <[email protected]>
2026-06-16 19:11       ` Corey Huinker <[email protected]>
2026-06-16 21:53         ` Robert Haas <[email protected]>
2026-06-16 22:50           ` Corey Huinker <[email protected]>
2026-06-16 22:57             ` Robert Haas <[email protected]>
2026-06-17 16:03               ` Corey Huinker <[email protected]>
2026-06-17 23:19                 ` Robert Haas <[email protected]>
2026-06-17 10:23             ` Etsuro Fujita <[email protected]>
2026-06-17 16:00               ` Corey Huinker <[email protected]>
2026-06-18 10:42                 ` Etsuro Fujita <[email protected]>
2026-06-18 21:23                   ` Corey Huinker <[email protected]>
2026-06-19 12:30                     ` Etsuro Fujita <[email protected]>
2026-06-19 13:45                     ` Robert Haas <[email protected]>
2026-06-20 04:11                       ` Corey Huinker <[email protected]>
2026-06-22 19:08                         ` Corey Huinker <[email protected]>
2026-06-23 11:43                           ` jian he <[email protected]>
2026-06-23 15:32                             ` Corey Huinker <[email protected]>
2026-06-23 12:10                           ` Robert Haas <[email protected]>
2026-06-23 15:53                             ` Corey Huinker <[email protected]>
2026-06-29 08:14                               ` Corey Huinker <[email protected]>
2026-06-29 11:50                                 ` Etsuro Fujita <[email protected]>
2026-06-29 18:17                                   ` Robert Haas <[email protected]>
2026-06-30 01:10                                     ` Corey Huinker <[email protected]>
2026-06-30 12:29                                       ` Etsuro Fujita <[email protected]>
2026-06-30 15:55                                         ` Corey Huinker <[email protected]>
2026-06-30 16:21                                           ` Corey Huinker <[email protected]>
2026-07-01 12:09                                           ` Etsuro Fujita <[email protected]>
2026-07-01 20:34                                             ` Corey Huinker <[email protected]>
2026-07-03 09:47                                               ` Etsuro Fujita <[email protected]>
2026-07-08 12:04                                                 ` Etsuro Fujita <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox