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]> 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-16 12:04 Etsuro Fujita <[email protected]> parent: 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-16 14:34 Robert Haas <[email protected]> parent: Etsuro Fujita <[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-16 18:08 Corey Huinker <[email protected]> parent: Robert Haas <[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-16 19:11 Corey Huinker <[email protected]> parent: Corey Huinker <[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-16 21:53 Robert Haas <[email protected]> parent: 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-16 22:50 Corey Huinker <[email protected]> parent: Robert Haas <[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-16 22:57 Robert Haas <[email protected]> parent: 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-17 10:23 Etsuro Fujita <[email protected]> parent: 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-17 16:00 Corey Huinker <[email protected]> parent: 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-17 16:03 Corey Huinker <[email protected]> parent: 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-17 23:19 Robert Haas <[email protected]> parent: Corey Huinker <[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-18 10:42 Etsuro Fujita <[email protected]> parent: 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-18 21:23 Corey Huinker <[email protected]> parent: Etsuro Fujita <[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-19 12:30 Etsuro Fujita <[email protected]> parent: Corey Huinker <[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-19 13:45 Robert Haas <[email protected]> parent: 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-20 04:11 Corey Huinker <[email protected]> parent: Robert Haas <[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-22 19:08 Corey Huinker <[email protected]> parent: Corey Huinker <[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-23 11:43 jian he <[email protected]> parent: 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-23 12:10 Robert Haas <[email protected]> parent: 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-23 15:32 Corey Huinker <[email protected]> parent: jian he <[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-23 15:53 Corey Huinker <[email protected]> parent: Robert Haas <[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-29 08:14 Corey Huinker <[email protected]> parent: Corey Huinker <[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, <_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-29 11:50 Etsuro Fujita <[email protected]> parent: Corey Huinker <[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-29 18:17 Robert Haas <[email protected]> parent: Etsuro Fujita <[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-30 01:10 Corey Huinker <[email protected]> parent: Robert Haas <[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-30 12:29 Etsuro Fujita <[email protected]> parent: 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-30 15:55 Corey Huinker <[email protected]> parent: 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-30 16:21 Corey Huinker <[email protected]> parent: 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-07-01 12:09 Etsuro Fujita <[email protected]> parent: 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-07-01 20:34 Corey Huinker <[email protected]> parent: 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-07-03 09:47 Etsuro Fujita <[email protected]> parent: Corey Huinker <[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-07-08 12:04 Etsuro Fujita <[email protected]> parent: 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