misleading error message in ProcessUtilitySlow T_CreateStatsStmt
hi.
while reviewing other work, some error messages in src/backend/tcop/utility.c
seem not accurate.
static void
ProcessUtilitySlow(ParseState *pstate,
PlannedStmt *pstmt,
const char *queryString,
ProcessUtilityContext context,
ParamListInfo params,
QueryEnvironment *queryEnv,
DestReceiver *dest,
QueryCompletion *qc)
case T_CreateStatsStmt:
{
Oid relid;
CreateStatsStmt *stmt = (CreateStatsStmt *) parsetree;
RangeVar *rel = (RangeVar *) linitial(stmt->relations);
if (!IsA(rel, RangeVar))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("only a single relation is
allowed in CREATE STATISTICS")));
relid = RangeVarGetRelid(rel,
ShareUpdateExclusiveLock, false);
/* Run parse analysis ... */
stmt = transformStatsStmt(relid, stmt, queryString);
address = CreateStatistics(stmt);
}
for example:
create or replace function tftest(int) returns table(a int, b int) as $$
begin
return query select $1, $1+i from generate_series(1,5) g(i);
end;
$$ language plpgsql immutable strict;
CREATE STATISTICS alt_stat2 ON a, b FROM tftest(1);
ERROR: only a single relation is allowed in CREATE STATISTICS
this error message seem misleading?
also this error check seems duplicated in CreateStatistics?
On Thu, 21 Aug 2025 at 17:00, jian he <jian.universality@gmail.com> wrote:
hi.
Hi!
RangeVar *rel = (RangeVar *) linitial(stmt->relations);
if (!IsA(rel, RangeVar))
These two lines are weird. Looks like linitial(stmt->relations)
should be assigned to variable with type Node* first?
for example:
create or replace function tftest(int) returns table(a int, b int) as $$
begin
return query select $1, $1+i from generate_series(1,5) g(i);
end;
$$ language plpgsql immutable strict;CREATE STATISTICS alt_stat2 ON a, b FROM tftest(1);
ERROR: only a single relation is allowed in CREATE STATISTICSthis error message seem misleading?
I wouldn’t say this is misleading, but " a single relation" is indeed
not precise enough. IMO we need a more precise term to distinguish
regular relation and table func.
also this error check seems duplicated in CreateStatistics?
Indeed.
--
Best regards,
Kirill Reshke
Kirill Reshke <reshkekirill@gmail.com> writes:
On Thu, 21 Aug 2025 at 17:00, jian he <jian.universality@gmail.com> wrote:
RangeVar *rel = (RangeVar *) linitial(stmt->relations);
if (!IsA(rel, RangeVar))
These two lines are weird. Looks like linitial(stmt->relations)
should be assigned to variable with type Node* first?
We take that sort of shortcut in many places. If there's not any need
for the code to deal with more than one node type, an extra variable
that's used just for the IsA test seems like a lot of notational
overhead.
regards, tom lane
On 2025-Aug-21, Kirill Reshke wrote:
I wouldn’t say this is misleading, but " a single relation" is indeed
not precise enough. IMO we need a more precise term to distinguish
regular relation and table func.
I'm not sure. See the definition of relation in the glossary:
https://www.postgresql.org/docs/18/glossary.html#GLOSSARY-RELATION
The generic term for all objects in a database that have a name and a
list of attributes defined in a specific order. Tables, sequences,
views, foreign tables, materialized views, composite types, and
indexes are all relations.
More generically, a relation is a set of tuples; for example, the
result of a query is also a relation.
In PostgreSQL, Class is an archaic synonym for relation.
(I wonder why this says "generically" rather than "generally". Is that
word choice a mistake?) Maybe in the "For example" clause we can also
mention table functions.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
On Fri, 22 Aug 2025 at 14:46, Álvaro Herrera <alvherre@kurilemu.de> wrote:
On 2025-Aug-21, Kirill Reshke wrote:
I wouldn’t say this is misleading, but " a single relation" is indeed
not precise enough. IMO we need a more precise term to distinguish
regular relation and table func.I'm not sure. See the definition of relation in the glossary:
https://www.postgresql.org/docs/18/glossary.html#GLOSSARY-RELATIONThe generic term for all objects in a database that have a name and a
list of attributes defined in a specific order. Tables, sequences,
views, foreign tables, materialized views, composite types, and
indexes are all relations.More generically, a relation is a set of tuples; for example, the
result of a query is also a relation.In PostgreSQL, Class is an archaic synonym for relation.
(I wonder why this says "generically" rather than "generally". Is that
word choice a mistake?) Maybe in the "For example" clause we can also
mention table functions.--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
I am sorry: I am not following. CREATE STATISTIC will work only for
single HEAP (or other AM) relations. So, for "simple regular tables"
as one can say (not tablefunc).
You say: relation is a term for both HEAP relation, tablefunc relation
and much more.
I say: "a single relation" in the error message is not precise enough.
Where do we disagree?
Anyway, I would say correct error message here should be:
```
db=# CREATE STATISTICS alt_stat2 ON a, b FROM tftest(1);
ERROR: cannot define statistics for relation "alt_stat2"
DETAIL: This operation is not supported for query result.
```
--
Best regards,
Kirill Reshke
hi.
will
+ if (!IsA(rln, RangeVar))
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("CREATE STATISTICS only
supports regular tables, materialized views, foreign tables, and
partitioned tables"));
solve the problem?
On Fri, Aug 22, 2025 at 6:46 PM Álvaro Herrera <alvherre@kurilemu.de> wrote:
I'm not sure. See the definition of relation in the glossary:
https://www.postgresql.org/docs/18/glossary.html#GLOSSARY-RELATIONThe generic term for all objects in a database that have a name and a
list of attributes defined in a specific order. Tables, sequences,
views, foreign tables, materialized views, composite types, and
indexes are all relations.More generically, a relation is a set of tuples; for example, the
result of a query is also a relation.In PostgreSQL, Class is an archaic synonym for relation.
(I wonder why this says "generically" rather than "generally". Is that
word choice a mistake?)
"More generally" feels more natural to me than "more generically" in
this sentence, but I'm not a native English speaker, so I could be
wrong.
Thanks
Richard
=?utf-8?Q?=C3=81lvaro?= Herrera <alvherre@kurilemu.de> writes:
I'm not sure. See the definition of relation in the glossary:
https://www.postgresql.org/docs/18/glossary.html#GLOSSARY-RELATION
The generic term for all objects in a database that have a name and a
list of attributes defined in a specific order. Tables, sequences,
views, foreign tables, materialized views, composite types, and
indexes are all relations.
More generically, a relation is a set of tuples; for example, the
result of a query is also a relation.
In PostgreSQL, Class is an archaic synonym for relation.
(I wonder why this says "generically" rather than "generally". Is that
word choice a mistake?) Maybe in the "For example" clause we can also
mention table functions.
Yeah, I think s/generically/generally/ would be an improvement.
I'm not certain, but I think that our use of "relation" to mean
"an object with a pg_class entry" is a Postgres-ism. I do know
that the meaning of "a set of tuples" is widely used, as that's
where the term "relational database" comes from. Maybe whoever
wrote this was trying to get at that point? But this text is
hardly clear about that.
regards, tom lane
On 2025-Aug-22, Kirill Reshke wrote:
I am sorry: I am not following. CREATE STATISTIC will work only for
single HEAP (or other AM) relations. So, for "simple regular tables"
as one can say (not tablefunc).
Ah yeah, you're right, I think we should say "a single table", or maybe
"a single table or materialized view" if the latter case is supported (I
don't remember offhand if it is).
The error message is mostly concerned with rejecting the case of
multiple relations being given in the FROM clause, because we said at
the time that we needed to make the grammar flexible enough to support
that case, but make it clear that it wasn't implemented yet. (It's a
pity that we haven't implemented that thus far ... I suppose it must be
a difficult problem.)
DETAIL: This operation is not supported for query result.
I would say "This operation is only supported for tables [and
matviews]." We don't need to list all the things that aren't supported,
I think. But your example here is wrong:
db=# CREATE STATISTICS alt_stat2 ON a, b FROM tftest(1);
ERROR: cannot define statistics for relation "alt_stat2"
DETAIL: This operation is not supported for query result.
It's not alt_stat2 that's the problem (that's the extstats object being
created), but tftest(1). I don't know if we need to specify that the
problem is in the FROM clause here -- seems obvious enough to me -- but
maybe someone opines differently?
ERROR: cannot define statistics for relation "tftest"
DETAIL: The FROM clause may only contain a single table.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
"Si quieres ser creativo, aprende el arte de perder el tiempo"
On 2025-Aug-22, Tom Lane wrote:
I'm not certain, but I think that our use of "relation" to mean
"an object with a pg_class entry" is a Postgres-ism. I do know
that the meaning of "a set of tuples" is widely used, as that's
where the term "relational database" comes from. Maybe whoever
wrote this was trying to get at that point? But this text is
hardly clear about that.
Yeah, AFAIR I wrote it (or at least heavily edited the original to get
to that), and I was trying to convey exactly those two ideas. If you
want to propose improvements, they're very welcome.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
"Puedes vivir sólo una vez, pero si lo haces bien, una vez es suficiente"
=?utf-8?Q?=C3=81lvaro?= Herrera <alvherre@kurilemu.de> writes:
Yeah, AFAIR I wrote it (or at least heavily edited the original to get
to that), and I was trying to convey exactly those two ideas. If you
want to propose improvements, they're very welcome.
Hmmm ... maybe something like
Mathematically, a "relation" is a set of tuples; this is the sense
meant in the term "relational database".
In Postgres, "relation" is commonly used to mean a database object
that has a name and a list of attributes defined in a specific
order. Tables, sequences, views, foreign tables, materialized
views, composite types, and indexes are all relations. A relation
in this sense is a container or descriptor for a set of tuples.
"Class" is an alternative but archaic term. The system catalog
pg_class holds an entry for each Postgres relation.
regards, tom lane
On 2025-Aug-22, Tom Lane wrote:
Hmmm ... maybe something like
Mathematically, a "relation" is a set of tuples; this is the sense
meant in the term "relational database".In Postgres, "relation" is commonly used to mean a database object
that has a name and a list of attributes defined in a specific
order. Tables, sequences, views, foreign tables, materialized
views, composite types, and indexes are all relations. A relation
in this sense is a container or descriptor for a set of tuples."Class" is an alternative but archaic term. The system catalog
pg_class holds an entry for each Postgres relation.
Thanks, pushed like that. I changed "a database object" to "an SQL
object", because that's a term we have a definition for.
(I also wrote "PostgreSQL" where you had "Postgres". I think it might
be okay now to change the product name in various places here, but it
seems better to do it consistently across the whole page.)
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
"¿Qué importan los años? Lo que realmente importa es comprobar que
a fin de cuentas la mejor edad de la vida es estar vivo" (Mafalda)
=?utf-8?Q?=C3=81lvaro?= Herrera <alvherre@kurilemu.de> writes:
Thanks, pushed like that. I changed "a database object" to "an SQL
object", because that's a term we have a definition for.
(I also wrote "PostgreSQL" where you had "Postgres". I think it might
be okay now to change the product name in various places here, but it
seems better to do it consistently across the whole page.)
LGTM, thanks. (My wording was only meant as a draft ...)
regards, tom lane
On 2025-Aug-21, jian he wrote:
case T_CreateStatsStmt:
{
Oid relid;
CreateStatsStmt *stmt = (CreateStatsStmt *) parsetree;
RangeVar *rel = (RangeVar *) linitial(stmt->relations);if (!IsA(rel, RangeVar))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("only a single relation is
allowed in CREATE STATISTICS")));
relid = RangeVarGetRelid(rel,
ShareUpdateExclusiveLock, false);
/* Run parse analysis ... */
stmt = transformStatsStmt(relid, stmt, queryString);
address = CreateStatistics(stmt);
}
Yeah, I think there's room to argue that this ereport() is just wrongly
copy-and-pasted from other nearby errors, and that it should have
completely different wording. BTW another way to cause this is
CREATE STATISTICS alt_stat2 ON a, b FROM xmltable('foo' passing 'bar' columns a text);
I propose to use this report:
ERROR: cannot create statistics on specified relation
DETAIL: CREATE STATISTICS only supports tables, materialized views, foreign tables, and partitioned tables.
(Not sold on saying just "tables" vs. "regular tables" or "plain tables";
thoughts?)
While looking at this whole thing, I noticed that this code is somewhat
bogus in a different way: we're resolving the relation name twice, both
here in ProcessUtilitySlow() (to pass to transformStatsStmt), and later
again inside CreateStatistics(). It's really strange that we decided
not to pass the relation Oids as a list argument to CreateStatistics. I
suggest to change that as the first attached patch.
Now, such a change would be appropriate only for branch master; it seems too
invasive for stable ones. For them I propose we only change the error message,
as in the other attachment.
We should add a couple of test cases for this particular error message
also. It's bad that these changes don't break any tests.
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"Once again, thank you and all of the developers for your hard work on
PostgreSQL. This is by far the most pleasant management experience of
any database I've worked on." (Dan Harris)
http://archives.postgresql.org/pgsql-performance/2006-04/msg00247.php
Attachments:
0001-rewrite-ProcessUtilitySlow-code-for-CreateStatsStmt.patchtext/x-diff; charset=utf-8Download
From bebffcaa22a047c61b0fa588db56db60e59dc9ef Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@kurilemu.de>
Date: Thu, 28 Aug 2025 19:49:14 +0200
Subject: [PATCH] rewrite ProcessUtilitySlow code for CreateStatsStmt
---
src/backend/commands/statscmds.c | 26 +++++-----------
src/backend/commands/tablecmds.c | 4 +--
src/backend/parser/parse_utilcmd.c | 13 +++++---
src/backend/tcop/utility.c | 48 +++++++++++++++++-------------
src/include/commands/defrem.h | 2 +-
src/include/parser/parse_utilcmd.h | 2 +-
6 files changed, 49 insertions(+), 46 deletions(-)
diff --git a/src/backend/commands/statscmds.c b/src/backend/commands/statscmds.c
index e24d540cd45..07b2b5bfef3 100644
--- a/src/backend/commands/statscmds.c
+++ b/src/backend/commands/statscmds.c
@@ -59,7 +59,7 @@ compare_int16(const void *a, const void *b)
* CREATE STATISTICS
*/
ObjectAddress
-CreateStatistics(CreateStatsStmt *stmt)
+CreateStatistics(CreateStatsStmt *stmt, List *relids)
{
int16 attnums[STATS_MAX_DIMENSIONS];
int nattnums = 0;
@@ -92,28 +92,18 @@ CreateStatistics(CreateStatsStmt *stmt)
ListCell *cell;
ListCell *cell2;
- Assert(IsA(stmt, CreateStatsStmt));
-
/*
* Examine the FROM clause. Currently, we only allow it to be a single
* simple table, but later we'll probably allow multiple tables and JOIN
- * syntax. The grammar is already prepared for that, so we have to check
- * here that what we got is what we can support.
+ * syntax. Parse analysis checked the list length already, so this is
+ * just defense-in-depth.
*/
- if (list_length(stmt->relations) != 1)
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("only a single relation is allowed in CREATE STATISTICS")));
+ Assert(list_length(stmt->relations) == list_length(relids));
+ if (list_length(relids) != 1)
+ elog(ERROR, "only a single relation is allowed in CREATE STATISTICS");
- foreach(cell, stmt->relations)
+ foreach_oid(relid, relids)
{
- Node *rln = (Node *) lfirst(cell);
-
- if (!IsA(rln, RangeVar))
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("only a single relation is allowed in CREATE STATISTICS")));
-
/*
* CREATE STATISTICS will influence future execution plans but does
* not interfere with currently executing plans. So it should be
@@ -121,7 +111,7 @@ CreateStatistics(CreateStatsStmt *stmt)
* conflicting with ANALYZE and other DDL that sets statistical
* information, but not with normal queries.
*/
- rel = relation_openrv((RangeVar *) rln, ShareUpdateExclusiveLock);
+ rel = relation_open(relid, ShareUpdateExclusiveLock);
/* Restrict to allowed relation types */
if (rel->rd_rel->relkind != RELKIND_RELATION &&
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 082a3575d62..1779bae80cb 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -9654,7 +9654,7 @@ ATExecAddStatistics(AlteredTableInfo *tab, Relation rel,
/* The CreateStatsStmt has already been through transformStatsStmt */
Assert(stmt->transformed);
- address = CreateStatistics(stmt);
+ address = CreateStatistics(stmt, list_make1_oid(RelationGetRelid(rel)));
return address;
}
@@ -15631,7 +15631,7 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, char *cmd,
}
else if (IsA(stmt, CreateStatsStmt))
querytree_list = lappend(querytree_list,
- transformStatsStmt(oldRelId,
+ transformStatsStmt(list_make1_oid(oldRelId),
(CreateStatsStmt *) stmt,
cmd));
else
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index afcf54169c3..587e2ef439c 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -3138,11 +3138,11 @@ transformIndexStmt(Oid relid, IndexStmt *stmt, const char *queryString)
* transformStatsStmt - parse analysis for CREATE STATISTICS
*
* To avoid race conditions, it's important that this function relies only on
- * the passed-in relid (and not on stmt->relation) to determine the target
- * relation.
+ * the passed-in relids list (and not on stmt->relations) to determine the
+ * target relation.
*/
CreateStatsStmt *
-transformStatsStmt(Oid relid, CreateStatsStmt *stmt, const char *queryString)
+transformStatsStmt(List *relids, CreateStatsStmt *stmt, const char *queryString)
{
ParseState *pstate;
ParseNamespaceItem *nsitem;
@@ -3153,6 +3153,11 @@ transformStatsStmt(Oid relid, CreateStatsStmt *stmt, const char *queryString)
if (stmt->transformed)
return stmt;
+ if (list_length(relids) != 1)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("only a single relation is allowed in CREATE STATISTICS"));
+
/* Set up pstate */
pstate = make_parsestate(NULL);
pstate->p_sourcetext = queryString;
@@ -3162,7 +3167,7 @@ transformStatsStmt(Oid relid, CreateStatsStmt *stmt, const char *queryString)
* to its fields without qualification. Caller is responsible for locking
* relation, but we still need to open it.
*/
- rel = relation_open(relid, NoLock);
+ rel = relation_open(linitial_oid(relids), NoLock);
nsitem = addRangeTableEntryForRelation(pstate, rel,
AccessShareLock,
NULL, false, true);
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 4f4191b0ea6..e09da051310 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1867,32 +1867,40 @@ ProcessUtilitySlow(ParseState *pstate,
case T_CreateStatsStmt:
{
- Oid relid;
CreateStatsStmt *stmt = (CreateStatsStmt *) parsetree;
- RangeVar *rel = (RangeVar *) linitial(stmt->relations);
+ List *relids = NIL;
+ ListCell *cell;
- if (!IsA(rel, RangeVar))
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("only a single relation is allowed in CREATE STATISTICS")));
+ foreach(cell, stmt->relations)
+ {
+ Oid relid;
- /*
- * CREATE STATISTICS will influence future execution plans
- * but does not interfere with currently executing plans.
- * So it should be enough to take ShareUpdateExclusiveLock
- * on relation, conflicting with ANALYZE and other DDL
- * that sets statistical information, but not with normal
- * queries.
- *
- * XXX RangeVarCallbackOwnsRelation not needed here, to
- * keep the same behavior as before.
- */
- relid = RangeVarGetRelid(rel, ShareUpdateExclusiveLock, false);
+ if (!IsA(lfirst(cell), RangeVar))
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot create statistics on specified relation"),
+ errdetail("CREATE STATISTICS only supports tables, materialized views, foreign tables, and partitioned tables."));
+ /*
+ * CREATE STATISTICS will influence future execution
+ * plans but does not interfere with currently
+ * executing plans. So it should be enough to take
+ * ShareUpdateExclusiveLock on relation, conflicting
+ * with ANALYZE and other DDL that sets statistical
+ * information, but not with normal queries.
+ *
+ * XXX RangeVarCallbackOwnsRelation not needed here,
+ * to keep the same behavior as before.
+ */
+ relid = RangeVarGetRelid(castNode(RangeVar, lfirst(cell)),
+ ShareUpdateExclusiveLock, false);
+ relids = lappend_oid(relids, relid);
+ }
/* Run parse analysis ... */
- stmt = transformStatsStmt(relid, stmt, queryString);
+ stmt = transformStatsStmt(relids, stmt, queryString);
- address = CreateStatistics(stmt);
+ /* ... and execute the command */
+ address = CreateStatistics(stmt, relids);
}
break;
diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h
index dd22b5efdfd..7b8cb40cd83 100644
--- a/src/include/commands/defrem.h
+++ b/src/include/commands/defrem.h
@@ -85,7 +85,7 @@ extern void RemoveOperatorById(Oid operOid);
extern ObjectAddress AlterOperator(AlterOperatorStmt *stmt);
/* commands/statscmds.c */
-extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt);
+extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt, List *relids);
extern ObjectAddress AlterStatistics(AlterStatsStmt *stmt);
extern void RemoveStatisticsById(Oid statsOid);
extern void RemoveStatisticsDataById(Oid statsOid, bool inh);
diff --git a/src/include/parser/parse_utilcmd.h b/src/include/parser/parse_utilcmd.h
index 9f2b58de797..28165eb1198 100644
--- a/src/include/parser/parse_utilcmd.h
+++ b/src/include/parser/parse_utilcmd.h
@@ -26,7 +26,7 @@ extern AlterTableStmt *transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
List **afterStmts);
extern IndexStmt *transformIndexStmt(Oid relid, IndexStmt *stmt,
const char *queryString);
-extern CreateStatsStmt *transformStatsStmt(Oid relid, CreateStatsStmt *stmt,
+extern CreateStatsStmt *transformStatsStmt(List *relids, CreateStatsStmt *stmt,
const char *queryString);
extern void transformRuleStmt(RuleStmt *stmt, const char *queryString,
List **actions, Node **whereClause);
--
2.39.5
0001-CREATE-STATISTICS-Fix-error-message.patchtext/x-diff; charset=utf-8Download
From 0ad9908e2ad2762445807136784b18777fac4c87 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@kurilemu.de>
Date: Thu, 28 Aug 2025 19:53:44 +0200
Subject: [PATCH] CREATE STATISTICS: Fix error message
---
src/backend/tcop/utility.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 25fe3d58016..7f5076d2c1a 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1883,7 +1883,8 @@ ProcessUtilitySlow(ParseState *pstate,
if (!IsA(rel, RangeVar))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("only a single relation is allowed in CREATE STATISTICS")));
+ errmsg("cannot create statistics on specified relation"),
+ errdetail("CREATE STATISTICS only supports tables, materialized views, foreign tables, and partitioned tables.")));
/*
* CREATE STATISTICS will influence future execution plans
--
2.39.5
=?utf-8?Q?=C3=81lvaro?= Herrera <alvherre@kurilemu.de> writes:
I propose to use this report:
ERROR: cannot create statistics on specified relation
DETAIL: CREATE STATISTICS only supports tables, materialized views, foreign tables, and partitioned tables.
WFM, although I think you could shorten it to "tables, materialized
views, and foreign tables". We generally expect that partitioned
tables are included when saying "tables", no? I'm not dead set on
that either way, though.
While looking at this whole thing, I noticed that this code is somewhat
bogus in a different way: we're resolving the relation name twice, both
here in ProcessUtilitySlow() (to pass to transformStatsStmt), and later
again inside CreateStatistics(). It's really strange that we decided
not to pass the relation Oids as a list argument to CreateStatistics. I
suggest to change that as the first attached patch.
I think this is a good idea, but I'm not sure that the locking
considerations are straight in this version. Once we translate a
relation name to OID, we'd better hold lock on the rel continuously to
the end of usage of that OID. It looks like you do, but then couldn't
the
+ rel = relation_open(relid, ShareUpdateExclusiveLock);
in CreateStatistics use NoLock to signify that we expect to have
the lock already?
Alternatively, maybe the right fix is to move the parse-analysis
work into CreateStatistics altogether. I think there is not a
very good argument for ProcessUtilitySlow doing all that stuff.
It's supposed to mainly just be a dispatching switch(), IMO.
regards, tom lane
On Fri, Aug 29, 2025 at 5:46 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
=?utf-8?Q?=C3=81lvaro?= Herrera <alvherre@kurilemu.de> writes:
I propose to use this report:
ERROR: cannot create statistics on specified relation
DETAIL: CREATE STATISTICS only supports tables, materialized views, foreign tables, and partitioned tables.WFM, although I think you could shorten it to "tables, materialized
views, and foreign tables". We generally expect that partitioned
tables are included when saying "tables", no? I'm not dead set on
that either way, though.
https://www.postgresql.org/docs/current/sql-copy.html
use "COPY TO can be used only with plain tables, not views, and does
not copy rows from child tables or child partitions"
Alternatively, maybe the right fix is to move the parse-analysis
work into CreateStatistics altogether. I think there is not a
very good argument for ProcessUtilitySlow doing all that stuff.
It's supposed to mainly just be a dispatching switch(), IMO.
seems doable.
transformStatsStmt, CreateStatistics both used only twice, refactoring arguments
should be fine.
please check the attached POC, regress tests also added.
main idea is
first check CreateStatsStmt->relations,
then call transformStatsStmt, transformStatsStmt only to transform
CreateStatsStmt->exprs.
also the above complaint about the relation lock issue will be resolved.
Attachments:
v2-0001-refactor-CreateStatsStmt.patchtext/x-patch; charset=US-ASCII; name=v2-0001-refactor-CreateStatsStmt.patchDownload
From 04975c4b93e60e2554b3d247d9bc38b5385530a3 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Fri, 29 Aug 2025 12:03:49 +0800
Subject: [PATCH v2 1/1] refactor CreateStatsStmt
discussion: https://postgr.es/m/
---
src/backend/commands/statscmds.c | 14 ++++++++++++--
src/backend/commands/tablecmds.c | 2 +-
src/backend/tcop/utility.c | 25 +------------------------
src/include/commands/defrem.h | 2 +-
src/test/regress/expected/stats_ext.out | 20 ++++++++++++++++++++
src/test/regress/sql/stats_ext.sql | 12 ++++++++++++
6 files changed, 47 insertions(+), 28 deletions(-)
diff --git a/src/backend/commands/statscmds.c b/src/backend/commands/statscmds.c
index e24d540cd45..094d1a4fc1b 100644
--- a/src/backend/commands/statscmds.c
+++ b/src/backend/commands/statscmds.c
@@ -29,6 +29,7 @@
#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/optimizer.h"
+#include "parser/parse_utilcmd.h"
#include "statistics/statistics.h"
#include "utils/acl.h"
#include "utils/builtins.h"
@@ -57,9 +58,11 @@ compare_int16(const void *a, const void *b)
/*
* CREATE STATISTICS
+ * ensure queryString is not NULL when we need do parse analysis for stmt!
+ * stmt changed on the go, espepcially transformStatsStmt.
*/
ObjectAddress
-CreateStatistics(CreateStatsStmt *stmt)
+CreateStatistics(CreateStatsStmt *stmt, const char *queryString)
{
int16 attnums[STATS_MAX_DIMENSIONS];
int nattnums = 0;
@@ -112,7 +115,8 @@ CreateStatistics(CreateStatsStmt *stmt)
if (!IsA(rln, RangeVar))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("only a single relation is allowed in CREATE STATISTICS")));
+ errmsg("cannot create statistics on specified relation"),
+ errdetail("CREATE STATISTICS only supports tables, materialized views, foreign tables, and partitioned tables")));
/*
* CREATE STATISTICS will influence future execution plans but does
@@ -150,6 +154,12 @@ CreateStatistics(CreateStatsStmt *stmt)
Assert(rel);
relid = RelationGetRelid(rel);
+ if (queryString != NULL && !stmt->transformed)
+ {
+ /* Run parse analysis ... */
+ (void) transformStatsStmt(relid, stmt, queryString);
+ }
+
/*
* If the node has a name, split it up and determine creation namespace.
* If not, put the object in the same namespace as the relation, and cons
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 082a3575d62..b8dd9306b93 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -9654,7 +9654,7 @@ ATExecAddStatistics(AlteredTableInfo *tab, Relation rel,
/* The CreateStatsStmt has already been through transformStatsStmt */
Assert(stmt->transformed);
- address = CreateStatistics(stmt);
+ address = CreateStatistics(stmt, NULL);
return address;
}
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 4f4191b0ea6..8d35ae971d5 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1867,32 +1867,9 @@ ProcessUtilitySlow(ParseState *pstate,
case T_CreateStatsStmt:
{
- Oid relid;
CreateStatsStmt *stmt = (CreateStatsStmt *) parsetree;
- RangeVar *rel = (RangeVar *) linitial(stmt->relations);
- if (!IsA(rel, RangeVar))
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("only a single relation is allowed in CREATE STATISTICS")));
-
- /*
- * CREATE STATISTICS will influence future execution plans
- * but does not interfere with currently executing plans.
- * So it should be enough to take ShareUpdateExclusiveLock
- * on relation, conflicting with ANALYZE and other DDL
- * that sets statistical information, but not with normal
- * queries.
- *
- * XXX RangeVarCallbackOwnsRelation not needed here, to
- * keep the same behavior as before.
- */
- relid = RangeVarGetRelid(rel, ShareUpdateExclusiveLock, false);
-
- /* Run parse analysis ... */
- stmt = transformStatsStmt(relid, stmt, queryString);
-
- address = CreateStatistics(stmt);
+ address = CreateStatistics(stmt, queryString);
}
break;
diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h
index dd22b5efdfd..684f62109fd 100644
--- a/src/include/commands/defrem.h
+++ b/src/include/commands/defrem.h
@@ -85,7 +85,7 @@ extern void RemoveOperatorById(Oid operOid);
extern ObjectAddress AlterOperator(AlterOperatorStmt *stmt);
/* commands/statscmds.c */
-extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt);
+extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt, const char *queryString);
extern ObjectAddress AlterStatistics(AlterStatsStmt *stmt);
extern void RemoveStatisticsById(Oid statsOid);
extern void RemoveStatisticsDataById(Oid statsOid, bool inh);
diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out
index c66e09f8b16..fb920312b4d 100644
--- a/src/test/regress/expected/stats_ext.out
+++ b/src/test/regress/expected/stats_ext.out
@@ -88,6 +88,26 @@ ERROR: statistics creation on system columns is not supported
-- statistics without a less-than operator not supported
CREATE STATISTICS tst (ndistinct) ON w from ext_stats_test1;
ERROR: column "w" cannot be used in statistics because its type xid has no default btree operator class
+CREATE STATISTICS tst ON (x is not null) FROM ext_stats_test s, lateral(values(s.x));
+ERROR: only a single relation is allowed in CREATE STATISTICS
+CREATE STATISTICS tst ON (x is not null) FROM (SELECT * FROM ext_stats_test);
+ERROR: cannot create statistics on specified relation
+DETAIL: CREATE STATISTICS only supports tables, materialized views, foreign tables, and partitioned tables
+CREATE STATISTICS tst ON (x is not null) FROM ext_stats_test s TABLESAMPLE system (x) REPEATABLE (200);
+ERROR: cannot create statistics on specified relation
+DETAIL: CREATE STATISTICS only supports tables, materialized views, foreign tables, and partitioned tables
+CREATE STATISTICS tst ON (x is not null) FROM JSON_TABLE(jsonb '123', '$' COLUMNS (item int PATH '$'));
+ERROR: cannot create statistics on specified relation
+DETAIL: CREATE STATISTICS only supports tables, materialized views, foreign tables, and partitioned tables
+CREATE FUNCTION tftest(int) returns table(a int, b int) as $$
+BEGIN
+ RETURN QUERY SELECT $1, $1+i FROM generate_series(1,5) g(i);
+END;
+$$ LANGUAGE plpgsql IMMUTABLE STRICT;
+CREATE STATISTICS alt_stat2 ON a, b FROM tftest(1);
+ERROR: cannot create statistics on specified relation
+DETAIL: CREATE STATISTICS only supports tables, materialized views, foreign tables, and partitioned tables
+DROP FUNCTION tftest;
DROP TABLE ext_stats_test1;
-- Ensure stats are dropped sanely, and test IF NOT EXISTS while at it
CREATE TABLE ab1 (a INTEGER, b INTEGER, c INTEGER);
diff --git a/src/test/regress/sql/stats_ext.sql b/src/test/regress/sql/stats_ext.sql
index 9ce4c670ecb..f63048f4c96 100644
--- a/src/test/regress/sql/stats_ext.sql
+++ b/src/test/regress/sql/stats_ext.sql
@@ -58,6 +58,18 @@ CREATE STATISTICS tst on (tableoid::int+1) from ext_stats_test1;
CREATE STATISTICS tst (ndistinct) ON xmin from ext_stats_test1;
-- statistics without a less-than operator not supported
CREATE STATISTICS tst (ndistinct) ON w from ext_stats_test1;
+
+CREATE STATISTICS tst ON (x is not null) FROM ext_stats_test s, lateral(values(s.x));
+CREATE STATISTICS tst ON (x is not null) FROM (SELECT * FROM ext_stats_test);
+CREATE STATISTICS tst ON (x is not null) FROM ext_stats_test s TABLESAMPLE system (x) REPEATABLE (200);
+CREATE STATISTICS tst ON (x is not null) FROM JSON_TABLE(jsonb '123', '$' COLUMNS (item int PATH '$'));
+CREATE FUNCTION tftest(int) returns table(a int, b int) as $$
+BEGIN
+ RETURN QUERY SELECT $1, $1+i FROM generate_series(1,5) g(i);
+END;
+$$ LANGUAGE plpgsql IMMUTABLE STRICT;
+CREATE STATISTICS alt_stat2 ON a, b FROM tftest(1);
+DROP FUNCTION tftest;
DROP TABLE ext_stats_test1;
-- Ensure stats are dropped sanely, and test IF NOT EXISTS while at it
--
2.34.1
On 2025-Aug-29, jian he wrote:
On Fri, Aug 29, 2025 at 5:46 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
WFM, although I think you could shorten it to "tables, materialized
views, and foreign tables". We generally expect that partitioned
tables are included when saying "tables", no? I'm not dead set on
that either way, though.https://www.postgresql.org/docs/current/sql-copy.html
use "COPY TO can be used only with plain tables, not views, and does
not copy rows from child tables or child partitions"
I'm inclined to think that we should only mention partitioned tables
specifically when they for some reason deviate from what we do for
regular tables, i.e., what Tom is saying. I don't think we've had an
explicit, consistent rule for that thus far, so there may be places
where we fail to follow it.
Anyway, I have pushed the error message change.
Alternatively, maybe the right fix is to move the parse-analysis
work into CreateStatistics altogether. I think there is not a
very good argument for ProcessUtilitySlow doing all that stuff.
It's supposed to mainly just be a dispatching switch(), IMO.seems doable.
transformStatsStmt, CreateStatistics both used only twice, refactoring
arguments should be fine.
please check the attached POC, regress tests also added.
Yeah, I like how this turned out. I found out this was introduced in
commit a4d75c86bf15.
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
On Fri, Aug 29, 2025 at 8:48 PM Álvaro Herrera <alvherre@kurilemu.de> wrote:
Alternatively, maybe the right fix is to move the parse-analysis
work into CreateStatistics altogether. I think there is not a
very good argument for ProcessUtilitySlow doing all that stuff.
It's supposed to mainly just be a dispatching switch(), IMO.seems doable.
transformStatsStmt, CreateStatistics both used only twice, refactoring
arguments should be fine.
please check the attached POC, regress tests also added.Yeah, I like how this turned out. I found out this was introduced in
commit a4d75c86bf15.
Previously, CreateStatistics and ProcessUtilitySlow opened the same relation
twice with a ShareUpdateExclusiveLock. This refactor ensures the relation is
opened with ShareUpdateExclusiveLock only once and also reduces redundant error
checks.
The logic is now more intuitive: we first error checking
CreateStatsStmt->relations and then call transformStatsStmt to parse analysis
CreateStatsStmt->exprs.
please check the attached refactor CreateStatsStmt.
Attachments:
v3-0001-refactor-CreateStatsStmt.patchtext/x-patch; charset=US-ASCII; name=v3-0001-refactor-CreateStatsStmt.patchDownload
From f11f1c97ef4a18a31d5442e4e745a530caa02d07 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Thu, 4 Sep 2025 12:52:13 +0800
Subject: [PATCH v3 1/1] refactor CreateStatsStmt
move all CreateStatsStmt logic into CreateStatistics.
Previously, CreateStatistics and ProcessUtilitySlow opened the same relation
twice with a ShareUpdateExclusiveLock. This refactor ensures the relation is
opened with ShareUpdateExclusiveLock only once and also reduces redundant error
checks.
The logic is now more intuitive: we first error checking
CreateStatsStmt->relations and then call transformStatsStmt to parse analysis
CreateStatsStmt->exprs.
discussion: https://postgr.es/m/CACJufxFcvo=fM6J9RC88n1U=eS9+5QdftwUNQMu5h7OgjmqDwQ@mail.gmail.com
---
src/backend/commands/statscmds.c | 17 ++++++++++++++---
src/backend/commands/tablecmds.c | 2 +-
src/backend/tcop/utility.c | 30 +-----------------------------
src/include/commands/defrem.h | 2 +-
4 files changed, 17 insertions(+), 34 deletions(-)
diff --git a/src/backend/commands/statscmds.c b/src/backend/commands/statscmds.c
index e24d540cd45..100eb82db64 100644
--- a/src/backend/commands/statscmds.c
+++ b/src/backend/commands/statscmds.c
@@ -29,6 +29,7 @@
#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/optimizer.h"
+#include "parser/parse_utilcmd.h"
#include "statistics/statistics.h"
#include "utils/acl.h"
#include "utils/builtins.h"
@@ -57,9 +58,11 @@ compare_int16(const void *a, const void *b)
/*
* CREATE STATISTICS
+ * Ensure queryString is not NULL when we need do parse analysis for stmt!
+ * stmt changed on the go, see transformStatsStmt.
*/
ObjectAddress
-CreateStatistics(CreateStatsStmt *stmt)
+CreateStatistics(CreateStatsStmt *stmt, const char *queryString)
{
int16 attnums[STATS_MAX_DIMENSIONS];
int nattnums = 0;
@@ -111,8 +114,9 @@ CreateStatistics(CreateStatsStmt *stmt)
if (!IsA(rln, RangeVar))
ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("only a single relation is allowed in CREATE STATISTICS")));
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot create statistics on the specified relation"),
+ errdetail("CREATE STATISTICS only supports tables, foreign tables and materialized views."));
/*
* CREATE STATISTICS will influence future execution plans but does
@@ -150,6 +154,13 @@ CreateStatistics(CreateStatsStmt *stmt)
Assert(rel);
relid = RelationGetRelid(rel);
+ if (queryString != NULL)
+ {
+ /* Run parse analysis ... */
+ (void) transformStatsStmt(relid, stmt, queryString);
+ }
+ Assert(stmt->transformed);
+
/*
* If the node has a name, split it up and determine creation namespace.
* If not, put the object in the same namespace as the relation, and cons
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 082a3575d62..b8dd9306b93 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -9654,7 +9654,7 @@ ATExecAddStatistics(AlteredTableInfo *tab, Relation rel,
/* The CreateStatsStmt has already been through transformStatsStmt */
Assert(stmt->transformed);
- address = CreateStatistics(stmt);
+ address = CreateStatistics(stmt, NULL);
return address;
}
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 5f442bc3bd4..027356f6e77 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1866,35 +1866,7 @@ ProcessUtilitySlow(ParseState *pstate,
break;
case T_CreateStatsStmt:
- {
- Oid relid;
- CreateStatsStmt *stmt = (CreateStatsStmt *) parsetree;
- RangeVar *rel = (RangeVar *) linitial(stmt->relations);
-
- if (!IsA(rel, RangeVar))
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("cannot create statistics on the specified relation"),
- errdetail("CREATE STATISTICS only supports tables, foreign tables and materialized views.")));
-
- /*
- * CREATE STATISTICS will influence future execution plans
- * but does not interfere with currently executing plans.
- * So it should be enough to take ShareUpdateExclusiveLock
- * on relation, conflicting with ANALYZE and other DDL
- * that sets statistical information, but not with normal
- * queries.
- *
- * XXX RangeVarCallbackOwnsRelation not needed here, to
- * keep the same behavior as before.
- */
- relid = RangeVarGetRelid(rel, ShareUpdateExclusiveLock, false);
-
- /* Run parse analysis ... */
- stmt = transformStatsStmt(relid, stmt, queryString);
-
- address = CreateStatistics(stmt);
- }
+ address = CreateStatistics((CreateStatsStmt *) parsetree, queryString);
break;
case T_AlterStatsStmt:
diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h
index dd22b5efdfd..684f62109fd 100644
--- a/src/include/commands/defrem.h
+++ b/src/include/commands/defrem.h
@@ -85,7 +85,7 @@ extern void RemoveOperatorById(Oid operOid);
extern ObjectAddress AlterOperator(AlterOperatorStmt *stmt);
/* commands/statscmds.c */
-extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt);
+extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt, const char *queryString);
extern ObjectAddress AlterStatistics(AlterStatsStmt *stmt);
extern void RemoveStatisticsById(Oid statsOid);
extern void RemoveStatisticsDataById(Oid statsOid, bool inh);
--
2.34.1
On 29.08.25 14:48, Álvaro Herrera wrote:
On 2025-Aug-29, jian he wrote:
On Fri, Aug 29, 2025 at 5:46 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
WFM, although I think you could shorten it to "tables, materialized
views, and foreign tables". We generally expect that partitioned
tables are included when saying "tables", no? I'm not dead set on
that either way, though.https://www.postgresql.org/docs/current/sql-copy.html
use "COPY TO can be used only with plain tables, not views, and does
not copy rows from child tables or child partitions"I'm inclined to think that we should only mention partitioned tables
specifically when they for some reason deviate from what we do for
regular tables, i.e., what Tom is saying. I don't think we've had an
explicit, consistent rule for that thus far, so there may be places
where we fail to follow it.Anyway, I have pushed the error message change.
I think this message is still wrong. The check doesn't even look at the
relation kind, which is what the message is implying. (If the message
were about relkinds, then it should use
errdetail_relkind_not_supported().) It checks that the from list entry
is a table name instead of some other thing like VALUES or JOIN. So it
should be something like
CREATE STATISTICS only supports plain table names in the FROM clause
The same could also be accomplished by changing the grammar from
FROM from_list
to
FROM qualified_name_list
On 05.09.25 14:30, Peter Eisentraut wrote:
On 29.08.25 14:48, Álvaro Herrera wrote:
On 2025-Aug-29, jian he wrote:
On Fri, Aug 29, 2025 at 5:46 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
WFM, although I think you could shorten it to "tables, materialized
views, and foreign tables". We generally expect that partitioned
tables are included when saying "tables", no? I'm not dead set on
that either way, though.https://www.postgresql.org/docs/current/sql-copy.html
use "COPY TO can be used only with plain tables, not views, and does
not copy rows from child tables or child partitions"I'm inclined to think that we should only mention partitioned tables
specifically when they for some reason deviate from what we do for
regular tables, i.e., what Tom is saying. I don't think we've had an
explicit, consistent rule for that thus far, so there may be places
where we fail to follow it.Anyway, I have pushed the error message change.
I think this message is still wrong. The check doesn't even look at the
relation kind, which is what the message is implying. (If the message
were about relkinds, then it should use
errdetail_relkind_not_supported().) It checks that the from list entry
is a table name instead of some other thing like VALUES or JOIN. So it
should be something likeCREATE STATISTICS only supports plain table names in the FROM clause
I propose the attached patch to fix this. I think this restores the
original meaning better.
Attachments:
v4-0001-CREATE-STATISTICS-improve-misleading-error-messag.patchtext/plain; charset=UTF-8; name=v4-0001-CREATE-STATISTICS-improve-misleading-error-messag.patchDownload
From 7c55d765dc1ec2f8a28e8ae1fe3551d32ed5a964 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Fri, 12 Sep 2025 15:30:16 +0200
Subject: [PATCH v4] CREATE STATISTICS: improve misleading error message
The previous change (commit f225473cbae) was still not on target,
because it talked about relation kinds, which are not what is being
checked here. Provide a more accurate message.
Discussion: https://postgr.es/m/CACJufxEZ48toGH0Em_6vdsT57Y3L8pLF=DZCQ_gCii6=C3MeXw@mail.gmail.com
---
src/backend/tcop/utility.c | 5 ++---
src/test/regress/expected/stats_ext.out | 21 +++++++--------------
2 files changed, 9 insertions(+), 17 deletions(-)
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 5f442bc3bd4..68605656b31 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1873,9 +1873,8 @@ ProcessUtilitySlow(ParseState *pstate,
if (!IsA(rel, RangeVar))
ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("cannot create statistics on the specified relation"),
- errdetail("CREATE STATISTICS only supports tables, foreign tables and materialized views.")));
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("CREATE STATISTICS only supports relation names in the FROM clause")));
/*
* CREATE STATISTICS will influence future execution plans
diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out
index a1f83b58b23..fdc0aa130bd 100644
--- a/src/test/regress/expected/stats_ext.out
+++ b/src/test/regress/expected/stats_ext.out
@@ -56,29 +56,22 @@ CREATE STATISTICS tst (unrecognized) ON x, y FROM ext_stats_test;
ERROR: unrecognized statistics kind "unrecognized"
-- unsupported targets
CREATE STATISTICS tst ON a FROM (VALUES (x)) AS foo;
-ERROR: cannot create statistics on the specified relation
-DETAIL: CREATE STATISTICS only supports tables, foreign tables and materialized views.
+ERROR: CREATE STATISTICS only supports relation names in the FROM clause
CREATE STATISTICS tst ON a FROM foo NATURAL JOIN bar;
-ERROR: cannot create statistics on the specified relation
-DETAIL: CREATE STATISTICS only supports tables, foreign tables and materialized views.
+ERROR: CREATE STATISTICS only supports relation names in the FROM clause
CREATE STATISTICS tst ON a FROM (SELECT * FROM ext_stats_test) AS foo;
-ERROR: cannot create statistics on the specified relation
-DETAIL: CREATE STATISTICS only supports tables, foreign tables and materialized views.
+ERROR: CREATE STATISTICS only supports relation names in the FROM clause
CREATE STATISTICS tst ON a FROM ext_stats_test s TABLESAMPLE system (x);
-ERROR: cannot create statistics on the specified relation
-DETAIL: CREATE STATISTICS only supports tables, foreign tables and materialized views.
+ERROR: CREATE STATISTICS only supports relation names in the FROM clause
CREATE STATISTICS tst ON a FROM XMLTABLE('foo' PASSING 'bar' COLUMNS a text);
-ERROR: cannot create statistics on the specified relation
-DETAIL: CREATE STATISTICS only supports tables, foreign tables and materialized views.
+ERROR: CREATE STATISTICS only supports relation names in the FROM clause
CREATE STATISTICS tst ON a FROM JSON_TABLE(jsonb '123', '$' COLUMNS (item int));
-ERROR: cannot create statistics on the specified relation
-DETAIL: CREATE STATISTICS only supports tables, foreign tables and materialized views.
+ERROR: CREATE STATISTICS only supports relation names in the FROM clause
CREATE FUNCTION tftest(int) returns table(a int, b int) as $$
SELECT $1, $1+i FROM generate_series(1,5) g(i);
$$ LANGUAGE sql IMMUTABLE STRICT;
CREATE STATISTICS alt_stat2 ON a FROM tftest(1);
-ERROR: cannot create statistics on the specified relation
-DETAIL: CREATE STATISTICS only supports tables, foreign tables and materialized views.
+ERROR: CREATE STATISTICS only supports relation names in the FROM clause
DROP FUNCTION tftest;
-- incorrect expressions
CREATE STATISTICS tst ON (y) FROM ext_stats_test; -- single column reference
--
2.51.0
Peter Eisentraut <peter@eisentraut.org> writes:
I propose the attached patch to fix this. I think this restores the
original meaning better.
I'm okay with this wording change, but I would stay with
ERRCODE_FEATURE_NOT_SUPPORTED rather than calling this
a "syntax error". It's not a syntax error IMV, but a
potential feature that we have deliberately left syntax
space for, even though we don't yet have ideas about
a workable implementation.
regards, tom lane
On 12.09.25 15:49, Tom Lane wrote:
Peter Eisentraut <peter@eisentraut.org> writes:
I propose the attached patch to fix this. I think this restores the
original meaning better.I'm okay with this wording change, but I would stay with
ERRCODE_FEATURE_NOT_SUPPORTED rather than calling this
a "syntax error". It's not a syntax error IMV, but a
potential feature that we have deliberately left syntax
space for, even though we don't yet have ideas about
a workable implementation.
Ok, done that way.
hi.
moving all code in ProcessUtilitySlow (case T_CreateStatsStmt:)
to CreateStatistic seems more intuitive to me.
so I rebased v3.
Attachments:
v5-0001-refactor-CreateStatsStmt.patchtext/x-patch; charset=US-ASCII; name=v5-0001-refactor-CreateStatsStmt.patchDownload
From 929e3d9a1bcf36937b1f362963f78595bfd47f88 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Thu, 2 Oct 2025 11:24:40 +0800
Subject: [PATCH v5 1/1] refactor CreateStatsStmt
move all CreateStatsStmt logic into CreateStatistics.
Previously, CreateStatistics and ProcessUtilitySlow opened the same relation
twice with a ShareUpdateExclusiveLock. This refactor ensures the relation is
opened with ShareUpdateExclusiveLock only once and also remove redundant error
checks.
in function: CreateStatsStmt we first error checking CreateStatsStmt->relations
and then call transformStatsStmt to parse analysis CreateStatsStmt->exprs.
discussion: https://postgr.es/m/CACJufxFcvo=fM6J9RC88n1U=eS9+5QdftwUNQMu5h7OgjmqDwQ@mail.gmail.com
---
src/backend/commands/statscmds.c | 14 ++++++++++++--
src/backend/commands/tablecmds.c | 2 +-
src/backend/tcop/utility.c | 29 +----------------------------
src/include/commands/defrem.h | 2 +-
4 files changed, 15 insertions(+), 32 deletions(-)
diff --git a/src/backend/commands/statscmds.c b/src/backend/commands/statscmds.c
index e24d540cd45..400bb293fa4 100644
--- a/src/backend/commands/statscmds.c
+++ b/src/backend/commands/statscmds.c
@@ -29,6 +29,7 @@
#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/optimizer.h"
+#include "parser/parse_utilcmd.h"
#include "statistics/statistics.h"
#include "utils/acl.h"
#include "utils/builtins.h"
@@ -57,9 +58,12 @@ compare_int16(const void *a, const void *b)
/*
* CREATE STATISTICS
+ * queryString is the source text of the CREATE STATISTICS command;
+ * It must be supplied if the CreateStatsStmt not being transformed
+ * (run transformStatsStmt), else it can be NULL.
*/
ObjectAddress
-CreateStatistics(CreateStatsStmt *stmt)
+CreateStatistics(CreateStatsStmt *stmt, const char *queryString)
{
int16 attnums[STATS_MAX_DIMENSIONS];
int nattnums = 0;
@@ -112,7 +116,7 @@ CreateStatistics(CreateStatsStmt *stmt)
if (!IsA(rln, RangeVar))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("only a single relation is allowed in CREATE STATISTICS")));
+ errmsg("CREATE STATISTICS only supports relation names in the FROM clause")));
/*
* CREATE STATISTICS will influence future execution plans but does
@@ -150,6 +154,12 @@ CreateStatistics(CreateStatsStmt *stmt)
Assert(rel);
relid = RelationGetRelid(rel);
+ /* Run parse analysis ... */
+ if (queryString != NULL)
+ stmt = transformStatsStmt(relid, stmt, queryString);
+
+ Assert(stmt->transformed);
+
/*
* If the node has a name, split it up and determine creation namespace.
* If not, put the object in the same namespace as the relation, and cons
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index fc89352b661..19c11970787 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -9658,7 +9658,7 @@ ATExecAddStatistics(AlteredTableInfo *tab, Relation rel,
/* The CreateStatsStmt has already been through transformStatsStmt */
Assert(stmt->transformed);
- address = CreateStatistics(stmt);
+ address = CreateStatistics(stmt, NULL);
return address;
}
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 918db53dd5e..027356f6e77 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1866,34 +1866,7 @@ ProcessUtilitySlow(ParseState *pstate,
break;
case T_CreateStatsStmt:
- {
- Oid relid;
- CreateStatsStmt *stmt = (CreateStatsStmt *) parsetree;
- RangeVar *rel = (RangeVar *) linitial(stmt->relations);
-
- if (!IsA(rel, RangeVar))
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("CREATE STATISTICS only supports relation names in the FROM clause")));
-
- /*
- * CREATE STATISTICS will influence future execution plans
- * but does not interfere with currently executing plans.
- * So it should be enough to take ShareUpdateExclusiveLock
- * on relation, conflicting with ANALYZE and other DDL
- * that sets statistical information, but not with normal
- * queries.
- *
- * XXX RangeVarCallbackOwnsRelation not needed here, to
- * keep the same behavior as before.
- */
- relid = RangeVarGetRelid(rel, ShareUpdateExclusiveLock, false);
-
- /* Run parse analysis ... */
- stmt = transformStatsStmt(relid, stmt, queryString);
-
- address = CreateStatistics(stmt);
- }
+ address = CreateStatistics((CreateStatsStmt *) parsetree, queryString);
break;
case T_AlterStatsStmt:
diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h
index dd22b5efdfd..684f62109fd 100644
--- a/src/include/commands/defrem.h
+++ b/src/include/commands/defrem.h
@@ -85,7 +85,7 @@ extern void RemoveOperatorById(Oid operOid);
extern ObjectAddress AlterOperator(AlterOperatorStmt *stmt);
/* commands/statscmds.c */
-extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt);
+extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt, const char *queryString);
extern ObjectAddress AlterStatistics(AlterStatsStmt *stmt);
extern void RemoveStatisticsById(Oid statsOid);
extern void RemoveStatisticsDataById(Oid statsOid, bool inh);
--
2.34.1
On 2025-Oct-02, jian he wrote:
hi.
moving all code in ProcessUtilitySlow (case T_CreateStatsStmt:)
to CreateStatistic seems more intuitive to me.so I rebased v3.
Yeah, this looks good. But I think we can go a little further. Because
CreateStatistics() now calls transformStatsStmt(), we don't need to do
the same in ATPostAlterTypeParse(), which allows us to simplify the code
there. In turn this means we can pass the whole Relation rather than
merely the OID, so transformStatsStmt doesn't need to open it. I attach
v4 with those changes. Comments?
For a moment it looked weird to me to pass a Relation to the parse
analysis routine, but I see that other functions declared in
parse_utilcmd.h are already doing that.
One more thing I noticed is that we're scribbling on the parser node,
which we can easily avoid by creating a copy of the node in
transformStatsStmt() before doing any changes. I'm not too clear
on whether this is really necessary or not. We do it in other places,
but we haven't done it here for a long while, and nothing seems to break
because of that.
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 89c8315117b..7797c707f73 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -3150,6 +3150,9 @@ transformStatsStmt(Relation rel, CreateStatsStmt *stmt, const char *queryString)
if (stmt->transformed)
return stmt;
+ /* create a copy to scribble on */
+ stmt = copyObject(stmt);
+
/* Set up pstate */
pstate = make_parsestate(NULL);
pstate->p_sourcetext = queryString;
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
"Uno puede defenderse de los ataques; contra los elogios se esta indefenso"
Attachments:
v4-0001-Restructure-CreateStatsStmt-parse-analysis-proces.patchtext/x-diff; charset=utf-8Download
From 4e690419874eb128bb3371f9c39ed95d8e731f64 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@kurilemu.de>
Date: Fri, 17 Oct 2025 13:55:15 +0200
Subject: [PATCH v4] Restructure CreateStatsStmt parse-analysis processing
Previously, a lot of code would be run in the ProcessUtility pipeline,
where it is not welcome (that's supposed to be just a giant dispatcher
switch). Move the parse analysis code to CreateStatistics() instead,
which then means we don't need to open the relation twice; it also
allows us to give a better error message when something other than a
relation name is given in the FROM clause. It also means we no longer
need to do the transform step in ATPostAlterTypeParse(), which saves a
bit of code.
Author: Jian He <jian.universality@gmail.com>
Discussion: https://postgr.es/m/CACJufxFcvo=fM6J9RC88n1U=eS9+5QdftwUNQMu5h7OgjmqDwQ@mail.gmail.com
---
src/backend/commands/statscmds.c | 13 +++++++++++--
src/backend/commands/tablecmds.c | 16 +---------------
src/backend/parser/parse_utilcmd.c | 13 +++----------
src/backend/tcop/utility.c | 29 +----------------------------
src/include/commands/defrem.h | 2 +-
src/include/parser/parse_utilcmd.h | 2 +-
6 files changed, 18 insertions(+), 57 deletions(-)
diff --git a/src/backend/commands/statscmds.c b/src/backend/commands/statscmds.c
index 27bf67e7c4b..632228c13ed 100644
--- a/src/backend/commands/statscmds.c
+++ b/src/backend/commands/statscmds.c
@@ -30,6 +30,7 @@
#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/optimizer.h"
+#include "parser/parse_utilcmd.h"
#include "statistics/statistics.h"
#include "utils/acl.h"
#include "utils/builtins.h"
@@ -58,9 +59,14 @@ compare_int16(const void *a, const void *b)
/*
* CREATE STATISTICS
+ *
+ * queryString is the source text of the CREATE STATISTICS command, if any.
+ *
+ * Normally stmt has not already undergone transformation, but in some cases
+ * it might have, such as CREATE TABLE (LIKE).
*/
ObjectAddress
-CreateStatistics(CreateStatsStmt *stmt)
+CreateStatistics(CreateStatsStmt *stmt, const char *queryString)
{
int16 attnums[STATS_MAX_DIMENSIONS];
int nattnums = 0;
@@ -113,7 +119,7 @@ CreateStatistics(CreateStatsStmt *stmt)
if (!IsA(rln, RangeVar))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("only a single relation is allowed in CREATE STATISTICS")));
+ errmsg("CREATE STATISTICS only supports relation names in the FROM clause")));
/*
* CREATE STATISTICS will influence future execution plans but does
@@ -151,6 +157,9 @@ CreateStatistics(CreateStatsStmt *stmt)
Assert(rel);
relid = RelationGetRelid(rel);
+ /* Run parse analysis */
+ stmt = transformStatsStmt(rel, stmt, queryString);
+
/*
* If the node has a name, split it up and determine creation namespace.
* If not, put the object in the same namespace as the relation, and cons
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 5fd8b51312c..a1732b71e55 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -9650,16 +9650,7 @@ static ObjectAddress
ATExecAddStatistics(AlteredTableInfo *tab, Relation rel,
CreateStatsStmt *stmt, bool is_rebuild, LOCKMODE lockmode)
{
- ObjectAddress address;
-
- Assert(IsA(stmt, CreateStatsStmt));
-
- /* The CreateStatsStmt has already been through transformStatsStmt */
- Assert(stmt->transformed);
-
- address = CreateStatistics(stmt);
-
- return address;
+ return CreateStatistics(stmt, NULL);
}
/*
@@ -15632,11 +15623,6 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, char *cmd,
querytree_list = lappend(querytree_list, stmt);
querytree_list = list_concat(querytree_list, afterStmts);
}
- else if (IsA(stmt, CreateStatsStmt))
- querytree_list = lappend(querytree_list,
- transformStatsStmt(oldRelId,
- (CreateStatsStmt *) stmt,
- cmd));
else
querytree_list = lappend(querytree_list, stmt);
}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index e96b38a59d5..89c8315117b 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -3137,16 +3137,14 @@ transformIndexStmt(Oid relid, IndexStmt *stmt, const char *queryString)
* transformStatsStmt - parse analysis for CREATE STATISTICS
*
* To avoid race conditions, it's important that this function relies only on
- * the passed-in relid (and not on stmt->relation) to determine the target
- * relation.
+ * the passed-in rel (and not on stmt->relation) as the target relation.
*/
CreateStatsStmt *
-transformStatsStmt(Oid relid, CreateStatsStmt *stmt, const char *queryString)
+transformStatsStmt(Relation rel, CreateStatsStmt *stmt, const char *queryString)
{
ParseState *pstate;
ParseNamespaceItem *nsitem;
ListCell *l;
- Relation rel;
/* Nothing to do if statement already transformed. */
if (stmt->transformed)
@@ -3158,10 +3156,8 @@ transformStatsStmt(Oid relid, CreateStatsStmt *stmt, const char *queryString)
/*
* Put the parent table into the rtable so that the expressions can refer
- * to its fields without qualification. Caller is responsible for locking
- * relation, but we still need to open it.
+ * to its fields without qualification.
*/
- rel = relation_open(relid, NoLock);
nsitem = addRangeTableEntryForRelation(pstate, rel,
AccessShareLock,
NULL, false, true);
@@ -3196,9 +3192,6 @@ transformStatsStmt(Oid relid, CreateStatsStmt *stmt, const char *queryString)
free_parsestate(pstate);
- /* Close relation */
- table_close(rel, NoLock);
-
/* Mark statement as successfully transformed */
stmt->transformed = true;
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 918db53dd5e..027356f6e77 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1866,34 +1866,7 @@ ProcessUtilitySlow(ParseState *pstate,
break;
case T_CreateStatsStmt:
- {
- Oid relid;
- CreateStatsStmt *stmt = (CreateStatsStmt *) parsetree;
- RangeVar *rel = (RangeVar *) linitial(stmt->relations);
-
- if (!IsA(rel, RangeVar))
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("CREATE STATISTICS only supports relation names in the FROM clause")));
-
- /*
- * CREATE STATISTICS will influence future execution plans
- * but does not interfere with currently executing plans.
- * So it should be enough to take ShareUpdateExclusiveLock
- * on relation, conflicting with ANALYZE and other DDL
- * that sets statistical information, but not with normal
- * queries.
- *
- * XXX RangeVarCallbackOwnsRelation not needed here, to
- * keep the same behavior as before.
- */
- relid = RangeVarGetRelid(rel, ShareUpdateExclusiveLock, false);
-
- /* Run parse analysis ... */
- stmt = transformStatsStmt(relid, stmt, queryString);
-
- address = CreateStatistics(stmt);
- }
+ address = CreateStatistics((CreateStatsStmt *) parsetree, queryString);
break;
case T_AlterStatsStmt:
diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h
index dd22b5efdfd..684f62109fd 100644
--- a/src/include/commands/defrem.h
+++ b/src/include/commands/defrem.h
@@ -85,7 +85,7 @@ extern void RemoveOperatorById(Oid operOid);
extern ObjectAddress AlterOperator(AlterOperatorStmt *stmt);
/* commands/statscmds.c */
-extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt);
+extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt, const char *queryString);
extern ObjectAddress AlterStatistics(AlterStatsStmt *stmt);
extern void RemoveStatisticsById(Oid statsOid);
extern void RemoveStatisticsDataById(Oid statsOid, bool inh);
diff --git a/src/include/parser/parse_utilcmd.h b/src/include/parser/parse_utilcmd.h
index 4965fac4495..67255799f0a 100644
--- a/src/include/parser/parse_utilcmd.h
+++ b/src/include/parser/parse_utilcmd.h
@@ -26,7 +26,7 @@ extern AlterTableStmt *transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
List **afterStmts);
extern IndexStmt *transformIndexStmt(Oid relid, IndexStmt *stmt,
const char *queryString);
-extern CreateStatsStmt *transformStatsStmt(Oid relid, CreateStatsStmt *stmt,
+extern CreateStatsStmt *transformStatsStmt(Relation rel, CreateStatsStmt *stmt,
const char *queryString);
extern void transformRuleStmt(RuleStmt *stmt, const char *queryString,
List **actions, Node **whereClause);
--
2.47.3
On Fri, Oct 17, 2025 at 8:13 PM Álvaro Herrera <alvherre@kurilemu.de> wrote:
Yeah, this looks good. But I think we can go a little further. Because
CreateStatistics() now calls transformStatsStmt(), we don't need to do
the same in ATPostAlterTypeParse(), which allows us to simplify the code
there. In turn this means we can pass the whole Relation rather than
merely the OID, so transformStatsStmt doesn't need to open it. I attach
v4 with those changes. Comments?
/*
* transformStatsStmt - parse analysis for CREATE STATISTICS
*
* To avoid race conditions, it's important that this function relies only on
* the passed-in rel (and not on stmt->relation) as the target relation.
*/
CreateStatsStmt *
transformStatsStmt(Relation rel, CreateStatsStmt *stmt, const char *queryString)
{
......
}
the (Relation rel) effectively comes from "stmt->relations", which
conflict with the above
comments.
For a moment it looked weird to me to pass a Relation to the parse
analysis routine, but I see that other functions declared in
parse_utilcmd.h are already doing that.One more thing I noticed is that we're scribbling on the parser node,
which we can easily avoid by creating a copy of the node in
transformStatsStmt() before doing any changes. I'm not too clear
on whether this is really necessary or not. We do it in other places,
but we haven't done it here for a long while, and nothing seems to break
because of that.diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index 89c8315117b..7797c707f73 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -3150,6 +3150,9 @@ transformStatsStmt(Relation rel, CreateStatsStmt *stmt, const char *queryString) if (stmt->transformed) return stmt;+ /* create a copy to scribble on */ + stmt = copyObject(stmt); + /* Set up pstate */ pstate = make_parsestate(NULL); pstate->p_sourcetext = queryString;
in src/backend/parser/parse_utilcmd.c, we have only two occurences of
copyObject.
On 2025-Oct-20, jian he wrote:
On Fri, Oct 17, 2025 at 8:13 PM Álvaro Herrera <alvherre@kurilemu.de> wrote:
/*
* transformStatsStmt - parse analysis for CREATE STATISTICS
*
* To avoid race conditions, it's important that this function relies only on
* the passed-in rel (and not on stmt->relation) as the target relation.
*/
CreateStatsStmt *
transformStatsStmt(Relation rel, CreateStatsStmt *stmt, const char *queryString)
the (Relation rel) effectively comes from "stmt->relations", which
conflict with the above comments.
Hmm? It does not. The whole point is that the relation name (RangeVar
stmt->relations) has already been resolved to an OID and locked, which
is what we pass as 'Relation rel'. Trying to resolve the name to an OID
again opens us up for race conditions. This is alleviated if the
relation has already been locked, so maybe we can relax this comment;
but it's not outright contradictory AFAICS.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
"Oh, great altar of passive entertainment, bestow upon me thy discordant images
at such speed as to render linear thought impossible" (Calvin a la TV)
On Mon, Oct 20, 2025 at 3:31 PM Álvaro Herrera <alvherre@kurilemu.de> wrote:
On 2025-Oct-20, jian he wrote:
On Fri, Oct 17, 2025 at 8:13 PM Álvaro Herrera <alvherre@kurilemu.de> wrote:
/*
* transformStatsStmt - parse analysis for CREATE STATISTICS
*
* To avoid race conditions, it's important that this function relies only on
* the passed-in rel (and not on stmt->relation) as the target relation.
*/
CreateStatsStmt *
transformStatsStmt(Relation rel, CreateStatsStmt *stmt, const char *queryString)the (Relation rel) effectively comes from "stmt->relations", which
conflict with the above comments.Hmm? It does not. The whole point is that the relation name (RangeVar
stmt->relations) has already been resolved to an OID and locked, which
is what we pass as 'Relation rel'. Trying to resolve the name to an OID
again opens us up for race conditions. This is alleviated if the
relation has already been locked, so maybe we can relax this comment;
but it's not outright contradictory AFAICS.
I think I understand your point.
When ALTER TABLE SET DATA TYPE invokes CreateStatistics, if the Relation (rel)
returned by CreateStatistics->relation_openrv is not the same as
ATPostAlterTypeParse.oldRelId, the regression test would already fail.
@@ -15632,11 +15623,6 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId,
Oid refRelId, char *cmd,
querytree_list = lappend(querytree_list, stmt);
querytree_list = list_concat(querytree_list, afterStmts);
}
- else if (IsA(stmt, CreateStatsStmt))
- querytree_list = lappend(querytree_list,
- transformStatsStmt(oldRelId,
- (CreateStatsStmt *) stmt,
- cmd));
The above "cmd" is the queryString, which is useful for error reporting.
create table t(a int);
create statistics xxx on (a + 1 is not null) from t;
alter table t alter column a set data type text;
with patch, v4-0001-Restructure-CreateStatsStmt-parse-analysis-proces.patch
ERROR: operator does not exist: text + integer
DETAIL: No operator of that name accepts the given argument types.
HINT: You might need to add explicit type casts.
In the master branch, the error message also includes the error position.
ERROR: operator does not exist: text + integer
LINE 1: alter table t alter column a set data type text;
^
DETAIL: No operator of that name accepts the given argument types.
HINT: You might need to add explicit type casts.
On 2025-Oct-20, jian he wrote:
@@ -15632,11 +15623,6 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, char *cmd, querytree_list = lappend(querytree_list, stmt); querytree_list = list_concat(querytree_list, afterStmts); } - else if (IsA(stmt, CreateStatsStmt)) - querytree_list = lappend(querytree_list, - transformStatsStmt(oldRelId, - (CreateStatsStmt *) stmt, - cmd));The above "cmd" is the queryString, which is useful for error reporting.
create table t(a int);
create statistics xxx on (a + 1 is not null) from t;
alter table t alter column a set data type text;with patch, v4-0001-Restructure-CreateStatsStmt-parse-analysis-proces.patch
ERROR: operator does not exist: text + integer
DETAIL: No operator of that name accepts the given argument types.
HINT: You might need to add explicit type casts.In the master branch, the error message also includes the error position.
ERROR: operator does not exist: text + integer
LINE 1: alter table t alter column a set data type text;
^
DETAIL: No operator of that name accepts the given argument types.
HINT: You might need to add explicit type casts.
Interesting, thanks for the example.
I think this example illustrates very well why the 'cmd' is rather
useless here -- note how the error message refers to an operator that
appears nowhere in the query string. The user says "SET DATA TYPE text"
and then we complain about the "text+integer" operator? That makes no
sense and I don't think it's in any way useful.
I think a user-friendly thing we could do is add an errcontext callback
that shows that we're trying to reapply a statistics object. We should
discuss that in a separate thread for a separate patch though, and also
I don't think that thread should prevent this rework from being applied
now. We could add this test scenario to the regression tests though, if
only to show how it would change later.
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
hi.
new patch attached.
Summary of the problem we’re trying to solve:
1. ProcessUtility for T_CreateStatsStmt, there’s a lot of code that doesn’t
belong there.
2. The current pattern of repeatedly performing RangeVar lookups isn’t ideal —
we should perform the lookup once and then use the corresponding relation OID
for subsequent operations.
The main part this patch is copied from
/messages/by-id/CA+TgmobyMXzoEzscCRDCggHRCTp1TW=Dm9pEhmwOYKos43WDAg@mail.gmail.com
Below is a copy of the commit message.
------------------
Previously, a lot of code would be run in the ProcessUtility pipeline,
where it is not welcome (that's supposed to be just a giant dispatcher
switch). Move the parse analysis code to CreateStatistics() instead,
which then means we don't need to open the relation twice; it also
allows us to give a better error message when something other than a
relation name is given in the FROM clause.
Generally we should avoid look up the same less-than-fully-qualified name
multiple times, we might get different answers due to concurrent activity, and
that might create a security vulnerability, along the lines of CVE-2014-0062.
Refactor so that a CreateStatsStmt carries both the untransformed FROM clause
and the range table that we get when it's transformed. That way, once the name
lookups have been done, we can propagate the results forward to future
processing steps and avoid ever repeating the lookup.
------------------
Attachments:
v5-0001-Restructure-CreateStatsStmt-parse-analysis-processing.patchtext/x-patch; charset=UTF-8; name=v5-0001-Restructure-CreateStatsStmt-parse-analysis-processing.patchDownload
From 1f6c467793843354e492cc7b5cd7e1137822250a Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Thu, 13 Nov 2025 11:19:26 +0800
Subject: [PATCH v5 1/1] Restructure CreateStatsStmt parse-analysis processing
Previously, a lot of code would be run in the ProcessUtility pipeline,
where it is not welcome (that's supposed to be just a giant dispatcher
switch). Move the parse analysis code to CreateStatistics() instead,
which then means we don't need to open the relation twice; it also
allows us to give a better error message when something other than a
relation name is given in the FROM clause.
Generally we should avoid look up the same less-than-fully-qualified name
multiple times, we might get different answers due to concurrent activity, and
that might create a security vulnerability, along the lines of CVE-2014-0062.
Refactor so that a CreateStatsStmt carries both the untransformed FROM clause
and the range table that we get when it's transformed. That way, once the name
lookups have been done, we can propagate the results forward to future
processing steps and avoid ever repeating the lookup.
discussion: https://postgr.es/m/CACJufxGXLU400QBBgdoboaza0xK58rQqsCAPrxbAMUmo0f8xCQ@mail.gmail.com
discussion: https://postgr.es/m/CA+TgmobyMXzoEzscCRDCggHRCTp1TW=Dm9pEhmwOYKos43WDAg@mail.gmail.com
commitfest: https://commitfest.postgresql.org/patch/6106
---
src/backend/commands/statscmds.c | 65 ++++------------
src/backend/commands/tablecmds.c | 17 ++++-
src/backend/parser/gram.y | 6 +-
src/backend/parser/parse_utilcmd.c | 118 +++++++++++++++++++++++++++--
src/backend/tcop/utility.c | 29 +------
src/include/commands/defrem.h | 2 +-
src/include/nodes/parsenodes.h | 3 +
src/include/parser/parse_utilcmd.h | 3 +-
8 files changed, 150 insertions(+), 93 deletions(-)
diff --git a/src/backend/commands/statscmds.c b/src/backend/commands/statscmds.c
index 0cf0ea43f19..0e08229de53 100644
--- a/src/backend/commands/statscmds.c
+++ b/src/backend/commands/statscmds.c
@@ -30,6 +30,7 @@
#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/optimizer.h"
+#include "parser/parse_utilcmd.h"
#include "statistics/statistics.h"
#include "utils/acl.h"
#include "utils/builtins.h"
@@ -60,7 +61,7 @@ compare_int16(const void *a, const void *b)
* CREATE STATISTICS
*/
ObjectAddress
-CreateStatistics(CreateStatsStmt *stmt, bool check_rights)
+CreateStatistics(CreateStatsStmt *stmt, bool check_rights, const char *queryString)
{
int16 attnums[STATS_MAX_DIMENSIONS];
int nattnums = 0;
@@ -78,6 +79,7 @@ CreateStatistics(CreateStatsStmt *stmt, bool check_rights)
Datum exprsDatum;
Relation statrel;
Relation rel = NULL;
+ RangeTblEntry *rte;
Oid relid;
ObjectAddress parentobject,
myself;
@@ -95,58 +97,23 @@ CreateStatistics(CreateStatsStmt *stmt, bool check_rights)
Assert(IsA(stmt, CreateStatsStmt));
+ /* Run parse analysis */
+ stmt = transformStatsStmt(stmt, queryString);
+
+ /* XXX internal error is better than Assert? error message need polish */
+ if (list_length(stmt->rtable) != list_length(stmt->from_clause))
+ elog(ERROR, "CreateStatsStmt length of rtable should equal to from_clause");
+
/*
- * Examine the FROM clause. Currently, we only allow it to be a single
- * simple table, but later we'll probably allow multiple tables and JOIN
- * syntax. The grammar is already prepared for that, so we have to check
- * here that what we got is what we can support.
+ * Double-check that the range table is of a form we can support.
*/
- if (list_length(stmt->relations) != 1)
+ if (list_length(stmt->rtable) != 1)
ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("only a single relation is allowed in CREATE STATISTICS")));
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("only a single relation is allowed in CREATE STATISTICS"));
- foreach(cell, stmt->relations)
- {
- Node *rln = (Node *) lfirst(cell);
-
- if (!IsA(rln, RangeVar))
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("only a single relation is allowed in CREATE STATISTICS")));
-
- /*
- * CREATE STATISTICS will influence future execution plans but does
- * not interfere with currently executing plans. So it should be
- * enough to take only ShareUpdateExclusiveLock on relation,
- * conflicting with ANALYZE and other DDL that sets statistical
- * information, but not with normal queries.
- */
- rel = relation_openrv((RangeVar *) rln, ShareUpdateExclusiveLock);
-
- /* Restrict to allowed relation types */
- if (rel->rd_rel->relkind != RELKIND_RELATION &&
- rel->rd_rel->relkind != RELKIND_MATVIEW &&
- rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE &&
- rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
- ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("cannot define statistics for relation \"%s\"",
- RelationGetRelationName(rel)),
- errdetail_relkind_not_supported(rel->rd_rel->relkind)));
-
- /* You must own the relation to create stats on it */
- if (!object_ownercheck(RelationRelationId, RelationGetRelid(rel), stxowner))
- aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(rel->rd_rel->relkind),
- RelationGetRelationName(rel));
-
- /* Creating statistics on system catalogs is not allowed */
- if (!allowSystemTableMods && IsSystemRelation(rel))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("permission denied: \"%s\" is a system catalog",
- RelationGetRelationName(rel))));
- }
+ rte = linitial_node(RangeTblEntry, stmt->rtable);
+ rel = relation_open(rte->relid, NoLock);
Assert(rel);
relid = RelationGetRelid(rel);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 23ebaa3f230..5d778f8d7d6 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -9682,7 +9682,7 @@ ATExecAddStatistics(AlteredTableInfo *tab, Relation rel,
/* The CreateStatsStmt has already been through transformStatsStmt */
Assert(stmt->transformed);
- address = CreateStatistics(stmt, !is_rebuild);
+ address = CreateStatistics(stmt, !is_rebuild, NULL);
return address;
}
@@ -15658,10 +15658,19 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, char *cmd,
querytree_list = list_concat(querytree_list, afterStmts);
}
else if (IsA(stmt, CreateStatsStmt))
+ {
+ RangeTblEntry *rte;
+ CreateStatsStmt *ss = castNode(CreateStatsStmt, stmt);
+
+ rte = makeNode(RangeTblEntry);
+ rte->rtekind = RTE_RELATION;
+ rte->relid = oldRelId;
+ rte->rellockmode = ShareUpdateExclusiveLock;
+ ss->rtable = list_make1(rte);
+
querytree_list = lappend(querytree_list,
- transformStatsStmt(oldRelId,
- (CreateStatsStmt *) stmt,
- cmd));
+ transformStatsStmt(ss, cmd));
+ }
else
querytree_list = lappend(querytree_list, stmt);
}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c3a0a354a9c..ca3cb446acd 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4778,7 +4778,8 @@ CreateStatsStmt:
n->defnames = $3;
n->stat_types = $4;
n->exprs = $6;
- n->relations = $8;
+ n->from_clause = $8;
+ n->rtable = NIL;
n->stxcomment = NULL;
n->if_not_exists = false;
$$ = (Node *) n;
@@ -4791,7 +4792,8 @@ CreateStatsStmt:
n->defnames = $6;
n->stat_types = $7;
n->exprs = $9;
- n->relations = $11;
+ n->from_clause = $11;
+ n->rtable = NIL;
n->stxcomment = NULL;
n->if_not_exists = true;
$$ = (Node *) n;
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index e96b38a59d5..410d8e84e87 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -2049,6 +2049,7 @@ generateClonedExtStatsStmt(RangeVar *heapRel, Oid heapRelid,
HeapTuple ht_stats;
Form_pg_statistic_ext statsrec;
CreateStatsStmt *stats;
+ RangeTblEntry *rte;
List *stat_types = NIL;
List *def_names = NIL;
bool isnull;
@@ -2152,7 +2153,15 @@ generateClonedExtStatsStmt(RangeVar *heapRel, Oid heapRelid,
stats->defnames = NULL;
stats->stat_types = stat_types;
stats->exprs = def_names;
- stats->relations = list_make1(heapRel);
+ stats->from_clause = list_make1(heapRel);
+
+ /* Create a range table entry. */
+ rte = makeNode(RangeTblEntry);
+ rte->rtekind = RTE_RELATION;
+ rte->relid = heapRelid;
+ rte->rellockmode = ShareUpdateExclusiveLock;
+ stats->rtable = list_make1(rte);
+
stats->stxcomment = NULL;
stats->transformed = true; /* don't need transformStatsStmt again */
stats->if_not_exists = false;
@@ -3136,32 +3145,46 @@ transformIndexStmt(Oid relid, IndexStmt *stmt, const char *queryString)
/*
* transformStatsStmt - parse analysis for CREATE STATISTICS
*
- * To avoid race conditions, it's important that this function relies only on
- * the passed-in relid (and not on stmt->relation) to determine the target
- * relation.
+ * This is used by CREATE STATISTICS. Changing table column’s data type or
+ * generated expression will trigger a rebuild of the related table’s
+ * statistics, during which this will also be used.
*/
CreateStatsStmt *
-transformStatsStmt(Oid relid, CreateStatsStmt *stmt, const char *queryString)
+transformStatsStmt(CreateStatsStmt *stmt, const char *queryString)
{
ParseState *pstate;
ParseNamespaceItem *nsitem;
ListCell *l;
Relation rel;
+ RangeTblEntry *rte;
/* Nothing to do if statement already transformed. */
if (stmt->transformed)
return stmt;
+ /* Construct range table if not done yet. */
+ if (stmt->rtable == NIL)
+ stmt->rtable = transformStatsFromClause(stmt->from_clause);
+
+ /*
+ * The range table should have been derived from a FROM clause consisting
+ * of a single RangeVar, so we expect a single RangeTblEntry.
+ */
+ Assert(list_length(stmt->rtable) == 1);
+ rte = linitial(stmt->rtable);
+ Assert(IsA(rte, RangeTblEntry));
+
+ /* See transformStatsFromClause for notes on lock level. */
+ rel = table_open(rte->relid, NoLock);
+
/* Set up pstate */
pstate = make_parsestate(NULL);
pstate->p_sourcetext = queryString;
/*
* Put the parent table into the rtable so that the expressions can refer
- * to its fields without qualification. Caller is responsible for locking
- * relation, but we still need to open it.
+ * to its fields without qualification.
*/
- rel = relation_open(relid, NoLock);
nsitem = addRangeTableEntryForRelation(pstate, rel,
AccessShareLock,
NULL, false, true);
@@ -3205,6 +3228,85 @@ transformStatsStmt(Oid relid, CreateStatsStmt *stmt, const char *queryString)
return stmt;
}
+/*
+ * Accepts a list of RangeVar objects and returns a corresponding list of
+ * RangeTblEntry objects. For use while transforming a CreateStatsStmt.
+ *
+ * At present, the only case we know how to handle is a FROM clause
+ * consisting of a single RangeVar.
+ */
+List *
+transformStatsFromClause(List *from_clause)
+{
+ RangeTblEntry *rte;
+ ListCell *cell;
+ Relation rel = NULL;
+
+ /*
+ * Examine the FROM clause. Currently, we only allow it to be a single
+ * simple table, but later we'll probably allow multiple tables and JOIN
+ * syntax. The grammar is already prepared for that, so we have to check
+ * here that what we got is what we can support.
+ */
+ if (list_length(from_clause) != 1)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("only a single relation is allowed in CREATE STATISTICS"));
+
+ foreach(cell, from_clause)
+ {
+ Node *rln = (Node *) lfirst(cell);
+
+ if (!IsA(rln, RangeVar))
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("CREATE STATISTICS only supports relation names in the FROM clause"));
+
+ /*
+ * CREATE STATISTICS will influence future execution plans but does
+ * not interfere with currently executing plans. So it should be
+ * enough to take only ShareUpdateExclusiveLock on relation,
+ * conflicting with ANALYZE and other DDL that sets statistical
+ * information, but not with normal queries.
+ */
+ rel = relation_openrv((RangeVar *) rln, ShareUpdateExclusiveLock);
+
+ /* Restrict to allowed relation types */
+ if (rel->rd_rel->relkind != RELKIND_RELATION &&
+ rel->rd_rel->relkind != RELKIND_MATVIEW &&
+ rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE &&
+ rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot define statistics for relation \"%s\"",
+ RelationGetRelationName(rel)),
+ errdetail_relkind_not_supported(rel->rd_rel->relkind));
+
+ /* You must own the relation to create stats on it */
+ if (!object_ownercheck(RelationRelationId, RelationGetRelid(rel), GetUserId()))
+ aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(rel->rd_rel->relkind),
+ RelationGetRelationName(rel));
+
+ /* Creating statistics on system catalogs is not allowed */
+ if (!allowSystemTableMods && IsSystemRelation(rel))
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied: \"%s\" is a system catalog",
+ RelationGetRelationName(rel)));
+ }
+
+ /* Create a range table entry. */
+ rte = makeNode(RangeTblEntry);
+ rte->rtekind = RTE_RELATION;
+ rte->relid = RelationGetRelid(rel);
+ rte->rellockmode = ShareUpdateExclusiveLock;
+
+ /* Close relation */
+ relation_close(rel, NoLock);
+
+ return list_make1(rte);
+}
+
/*
* transformRuleStmt -
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index d18a3a60a46..0c31113ebb5 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1874,34 +1874,7 @@ ProcessUtilitySlow(ParseState *pstate,
break;
case T_CreateStatsStmt:
- {
- Oid relid;
- CreateStatsStmt *stmt = (CreateStatsStmt *) parsetree;
- RangeVar *rel = (RangeVar *) linitial(stmt->relations);
-
- if (!IsA(rel, RangeVar))
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("CREATE STATISTICS only supports relation names in the FROM clause")));
-
- /*
- * CREATE STATISTICS will influence future execution plans
- * but does not interfere with currently executing plans.
- * So it should be enough to take ShareUpdateExclusiveLock
- * on relation, conflicting with ANALYZE and other DDL
- * that sets statistical information, but not with normal
- * queries.
- *
- * XXX RangeVarCallbackOwnsRelation not needed here, to
- * keep the same behavior as before.
- */
- relid = RangeVarGetRelid(rel, ShareUpdateExclusiveLock, false);
-
- /* Run parse analysis ... */
- stmt = transformStatsStmt(relid, stmt, queryString);
-
- address = CreateStatistics(stmt, true);
- }
+ address = CreateStatistics((CreateStatsStmt *) parsetree, true, queryString);
break;
case T_AlterStatsStmt:
diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h
index f3432b4b6a1..95397f91c5a 100644
--- a/src/include/commands/defrem.h
+++ b/src/include/commands/defrem.h
@@ -85,7 +85,7 @@ extern void RemoveOperatorById(Oid operOid);
extern ObjectAddress AlterOperator(AlterOperatorStmt *stmt);
/* commands/statscmds.c */
-extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt, bool check_rights);
+extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt, bool check_rights, const char *queryString);
extern ObjectAddress AlterStatistics(AlterStatsStmt *stmt);
extern void RemoveStatisticsById(Oid statsOid);
extern void RemoveStatisticsDataById(Oid statsOid, bool inh);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index d14294a4ece..3e9257cc8ae 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3523,6 +3523,9 @@ typedef struct CreateStatsStmt
List *stat_types; /* stat types (list of String) */
List *exprs; /* expressions to build statistics on */
List *relations; /* rels to build stats on (list of RangeVar) */
+ List *from_clause; /* FROM clause ((list of RangeVar)) */
+ List *rtable; /* It’s not derived directly from the parser, instead
+ * it comes from parse analysis. (list of RangeTblEntry) */
char *stxcomment; /* comment to apply to stats, or NULL */
bool transformed; /* true when transformStatsStmt is finished */
bool if_not_exists; /* do nothing if stats name already exists */
diff --git a/src/include/parser/parse_utilcmd.h b/src/include/parser/parse_utilcmd.h
index 4965fac4495..499e9ebef1c 100644
--- a/src/include/parser/parse_utilcmd.h
+++ b/src/include/parser/parse_utilcmd.h
@@ -26,8 +26,9 @@ extern AlterTableStmt *transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
List **afterStmts);
extern IndexStmt *transformIndexStmt(Oid relid, IndexStmt *stmt,
const char *queryString);
-extern CreateStatsStmt *transformStatsStmt(Oid relid, CreateStatsStmt *stmt,
+extern CreateStatsStmt *transformStatsStmt(CreateStatsStmt *stmt,
const char *queryString);
+extern List *transformStatsFromClause(List *from_clause);
extern void transformRuleStmt(RuleStmt *stmt, const char *queryString,
List **actions, Node **whereClause);
extern List *transformCreateSchemaStmtElements(List *schemaElts,
--
2.34.1
On 2025-Nov-13, jian he wrote:
@@ -15658,10 +15658,19 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, char *cmd, querytree_list = list_concat(querytree_list, afterStmts); } else if (IsA(stmt, CreateStatsStmt)) + { + RangeTblEntry *rte; + CreateStatsStmt *ss = castNode(CreateStatsStmt, stmt); + + rte = makeNode(RangeTblEntry); + rte->rtekind = RTE_RELATION; + rte->relid = oldRelId; + rte->rellockmode = ShareUpdateExclusiveLock; + ss->rtable = list_make1(rte); + querytree_list = lappend(querytree_list, - transformStatsStmt(oldRelId, - (CreateStatsStmt *) stmt, - cmd)); + transformStatsStmt(ss, cmd)); + } else querytree_list = lappend(querytree_list, stmt); }
Hmm, how would this part here work in the hypothetical world where a
stats object references multiple relations?
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
"No renuncies a nada. No te aferres a nada."
On Thu, Nov 13, 2025 at 5:41 PM Álvaro Herrera <alvherre@kurilemu.de> wrote:
On 2025-Nov-13, jian he wrote:
@@ -15658,10 +15658,19 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, char *cmd, querytree_list = list_concat(querytree_list, afterStmts); } else if (IsA(stmt, CreateStatsStmt)) + { + RangeTblEntry *rte; + CreateStatsStmt *ss = castNode(CreateStatsStmt, stmt); + + rte = makeNode(RangeTblEntry); + rte->rtekind = RTE_RELATION; + rte->relid = oldRelId; + rte->rellockmode = ShareUpdateExclusiveLock; + ss->rtable = list_make1(rte); + querytree_list = lappend(querytree_list, - transformStatsStmt(oldRelId, - (CreateStatsStmt *) stmt, - cmd)); + transformStatsStmt(ss, cmd)); + } else querytree_list = lappend(querytree_list, stmt); }Hmm, how would this part here work in the hypothetical world where a
stats object references multiple relations?
hi.
per
https://www.postgresql.org/docs/current/catalog-pg-statistic-ext.html
extended statistics either based on column name or expression.
If based on column name, imagine a simple case:
CREATE STATISTICS ON a FROM t1,t2;
pg_statistic_ext obviously needs another column to store other relation oids.
in this case, we can use AlteredTableInfo->changedStatisticsOids to lookup
catalog table pg_statistic_ext to find out the associated relation oids
If extended statistics is based on expression, imagine a simple case:
CREATE STATISTICS on (t1.a + t2.a) from t1, t2;
ruleutils.c can not cope with expressions like: "(t1.a + t2.a)",
so we have to wrap the expression into a Query Node, store it in
pg_statistic_ext.stxexprs.
If pg_statistic_ext does not contain all associated relation
OIDs, all these pg_get_statisticsobjdef_expressions won't work
pg_statistic_ext should contain all associated relation OIDs, in my opinion. If
pg_statistic_ext does store all related relation OIDs, then the potential issue
you mentioned above should be solvable.
On Thu, Nov 13, 2025 at 5:41 PM Álvaro Herrera <alvherre@kurilemu.de> wrote:
@@ -15658,10 +15658,19 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, char *cmd, querytree_list = list_concat(querytree_list, afterStmts); } else if (IsA(stmt, CreateStatsStmt)) + { + RangeTblEntry *rte; + CreateStatsStmt *ss = castNode(CreateStatsStmt, stmt); + + rte = makeNode(RangeTblEntry); + rte->rtekind = RTE_RELATION; + rte->relid = oldRelId; + rte->rellockmode = ShareUpdateExclusiveLock; + ss->rtable = list_make1(rte); + querytree_list = lappend(querytree_list, - transformStatsStmt(oldRelId, - (CreateStatsStmt *) stmt, - cmd)); + transformStatsStmt(ss, cmd)); + } else querytree_list = lappend(querytree_list, stmt); }Hmm, how would this part here work in the hypothetical world where a
stats object references multiple relations?
hi.
For extended statistics that span multiple relations (hypothetically),
we should have the OIDs of
all involved relations within the pg_statistic_ext catalog. Without this , it's
hard to do statistic's expression deparse, changing column data types
or generation
expressions. Catalog pg_depend do have statistics associated relation OIDs
information, relying on it for statistical expression deparsing would be more
complicated.
Once pg_statistic_ext have all relation oid information,
RememberStatisticsForRebuilding will not only collecting
AlteredTableInfo->changedStatisticsOids, it will aslo collect the OID of other
relations associated with this statistic.
For the above quoted part, we should construct a RangeTblEntry for
each associated
relation (OID).
Obviously transformStatsStmt itself needs to figure out how to
deal with multi-relation expressions.
rebased and minor polishing.
Attachments:
v6-0001-Restructure-CreateStatsStmt-parse-analysis-processing.patchtext/x-patch; charset=UTF-8; name=v6-0001-Restructure-CreateStatsStmt-parse-analysis-processing.patchDownload
From e9e4a0a85b3244da89e45ba7f258e57c109c3610 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Wed, 17 Dec 2025 13:44:46 +0800
Subject: [PATCH v6 1/1] Restructure CreateStatsStmt parse-analysis processing
Previously, a lot of code would be run in the ProcessUtility pipeline,
where it is not welcome (that's supposed to be just a giant dispatcher
switch). Move the parse analysis code to CreateStatistics() instead,
which then means we don't need to open the relation twice; it also
allows us to give a better error message when something other than a
relation name is given in the FROM clause.
Generally we should avoid look up the same less-than-fully-qualified name
multiple times, we might get different answers due to concurrent activity, and
that might create a security vulnerability, along the lines of CVE-2014-0062.
Refactor so that a CreateStatsStmt carries both the untransformed FROM clause
and the range table that we get when it's transformed. That way, once the name
lookups have been done, we can propagate the results forward to future
processing steps and avoid ever repeating the lookup.
discussion: https://postgr.es/m/CACJufxGXLU400QBBgdoboaza0xK58rQqsCAPrxbAMUmo0f8xCQ@mail.gmail.com
discussion: https://postgr.es/m/CA+TgmobyMXzoEzscCRDCggHRCTp1TW=Dm9pEhmwOYKos43WDAg@mail.gmail.com
commitfest: https://commitfest.postgresql.org/patch/6106
---
src/backend/commands/statscmds.c | 65 ++-------------
src/backend/commands/tablecmds.c | 17 +++-
src/backend/parser/gram.y | 6 +-
src/backend/parser/parse_utilcmd.c | 124 +++++++++++++++++++++++++++--
src/backend/tcop/utility.c | 29 +------
src/include/commands/defrem.h | 2 +-
src/include/nodes/parsenodes.h | 4 +
src/include/parser/parse_utilcmd.h | 3 +-
8 files changed, 149 insertions(+), 101 deletions(-)
diff --git a/src/backend/commands/statscmds.c b/src/backend/commands/statscmds.c
index 77b1a6e2dc5..e323190cb11 100644
--- a/src/backend/commands/statscmds.c
+++ b/src/backend/commands/statscmds.c
@@ -30,6 +30,7 @@
#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/optimizer.h"
+#include "parser/parse_utilcmd.h"
#include "statistics/statistics.h"
#include "utils/acl.h"
#include "utils/builtins.h"
@@ -60,7 +61,7 @@ compare_int16(const void *a, const void *b)
* CREATE STATISTICS
*/
ObjectAddress
-CreateStatistics(CreateStatsStmt *stmt, bool check_rights)
+CreateStatistics(CreateStatsStmt *stmt, bool check_rights, const char *queryString)
{
int16 attnums[STATS_MAX_DIMENSIONS];
int nattnums = 0;
@@ -78,6 +79,7 @@ CreateStatistics(CreateStatsStmt *stmt, bool check_rights)
Datum exprsDatum;
Relation statrel;
Relation rel = NULL;
+ RangeTblEntry *rte;
Oid relid;
ObjectAddress parentobject,
myself;
@@ -95,64 +97,13 @@ CreateStatistics(CreateStatsStmt *stmt, bool check_rights)
Assert(IsA(stmt, CreateStatsStmt));
- /*
- * Examine the FROM clause. Currently, we only allow it to be a single
- * simple table, but later we'll probably allow multiple tables and JOIN
- * syntax. The grammar is already prepared for that, so we have to check
- * here that what we got is what we can support.
- */
- if (list_length(stmt->relations) != 1)
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("only a single relation is allowed in CREATE STATISTICS")));
+ /* Run parse analysis */
+ stmt = transformStatsStmt(stmt, queryString);
- foreach(cell, stmt->relations)
- {
- Node *rln = (Node *) lfirst(cell);
+ Assert(list_length(stmt->rtable) == list_length(stmt->from_clause));
- if (!IsA(rln, RangeVar))
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("only a single relation is allowed in CREATE STATISTICS")));
-
- /*
- * CREATE STATISTICS will influence future execution plans but does
- * not interfere with currently executing plans. So it should be
- * enough to take only ShareUpdateExclusiveLock on relation,
- * conflicting with ANALYZE and other DDL that sets statistical
- * information, but not with normal queries.
- */
- rel = relation_openrv((RangeVar *) rln, ShareUpdateExclusiveLock);
-
- /* Restrict to allowed relation types */
- if (rel->rd_rel->relkind != RELKIND_RELATION &&
- rel->rd_rel->relkind != RELKIND_MATVIEW &&
- rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE &&
- rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
- ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("cannot define statistics for relation \"%s\"",
- RelationGetRelationName(rel)),
- errdetail_relkind_not_supported(rel->rd_rel->relkind)));
-
- /*
- * You must own the relation to create stats on it.
- *
- * NB: Concurrent changes could cause this function's lookup to find a
- * different relation than a previous lookup by the caller, so we must
- * perform this check even when check_rights == false.
- */
- if (!object_ownercheck(RelationRelationId, RelationGetRelid(rel), stxowner))
- aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(rel->rd_rel->relkind),
- RelationGetRelationName(rel));
-
- /* Creating statistics on system catalogs is not allowed */
- if (!allowSystemTableMods && IsSystemRelation(rel))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("permission denied: \"%s\" is a system catalog",
- RelationGetRelationName(rel))));
- }
+ rte = linitial_node(RangeTblEntry, stmt->rtable);
+ rel = relation_open(rte->relid, NoLock);
Assert(rel);
relid = RelationGetRelid(rel);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 6b1a00ed477..3fb9d9afe23 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -9718,7 +9718,7 @@ ATExecAddStatistics(AlteredTableInfo *tab, Relation rel,
/* The CreateStatsStmt has already been through transformStatsStmt */
Assert(stmt->transformed);
- address = CreateStatistics(stmt, !is_rebuild);
+ address = CreateStatistics(stmt, !is_rebuild, NULL);
return address;
}
@@ -15694,10 +15694,19 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, char *cmd,
querytree_list = list_concat(querytree_list, afterStmts);
}
else if (IsA(stmt, CreateStatsStmt))
+ {
+ RangeTblEntry *rte;
+ CreateStatsStmt *ss = castNode(CreateStatsStmt, stmt);
+
+ rte = makeNode(RangeTblEntry);
+ rte->rtekind = RTE_RELATION;
+ rte->relid = oldRelId;
+ rte->rellockmode = ShareUpdateExclusiveLock;
+ ss->rtable = list_make1(rte);
+
querytree_list = lappend(querytree_list,
- transformStatsStmt(oldRelId,
- (CreateStatsStmt *) stmt,
- cmd));
+ transformStatsStmt(ss, cmd));
+ }
else
querytree_list = lappend(querytree_list, stmt);
}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 28f4e11e30f..7a4ee97988d 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4830,7 +4830,8 @@ CreateStatsStmt:
n->defnames = $3;
n->stat_types = $4;
n->exprs = $6;
- n->relations = $8;
+ n->from_clause = $8;
+ n->rtable = NIL;
n->stxcomment = NULL;
n->if_not_exists = false;
$$ = (Node *) n;
@@ -4843,7 +4844,8 @@ CreateStatsStmt:
n->defnames = $6;
n->stat_types = $7;
n->exprs = $9;
- n->relations = $11;
+ n->from_clause = $11;
+ n->rtable = NIL;
n->stxcomment = NULL;
n->if_not_exists = true;
$$ = (Node *) n;
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 375b40b29af..0817c4e63f1 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -2052,6 +2052,7 @@ generateClonedExtStatsStmt(RangeVar *heapRel, Oid heapRelid,
HeapTuple ht_stats;
Form_pg_statistic_ext statsrec;
CreateStatsStmt *stats;
+ RangeTblEntry *rte;
List *stat_types = NIL;
List *def_names = NIL;
bool isnull;
@@ -2155,7 +2156,15 @@ generateClonedExtStatsStmt(RangeVar *heapRel, Oid heapRelid,
stats->defnames = NULL;
stats->stat_types = stat_types;
stats->exprs = def_names;
- stats->relations = list_make1(heapRel);
+ stats->from_clause = list_make1(heapRel);
+
+ /* Create a range table entry. */
+ rte = makeNode(RangeTblEntry);
+ rte->rtekind = RTE_RELATION;
+ rte->relid = heapRelid;
+ rte->rellockmode = ShareUpdateExclusiveLock;
+ stats->rtable = list_make1(rte);
+
stats->stxcomment = NULL;
stats->transformed = true; /* don't need transformStatsStmt again */
stats->if_not_exists = false;
@@ -3139,32 +3148,46 @@ transformIndexStmt(Oid relid, IndexStmt *stmt, const char *queryString)
/*
* transformStatsStmt - parse analysis for CREATE STATISTICS
*
- * To avoid race conditions, it's important that this function relies only on
- * the passed-in relid (and not on stmt->relation) to determine the target
- * relation.
+ * This is used by CREATE STATISTICS. Changing table column’s data type or
+ * generated expression will trigger a rebuild of the related table’s
+ * statistics, during which this will also be used.
*/
CreateStatsStmt *
-transformStatsStmt(Oid relid, CreateStatsStmt *stmt, const char *queryString)
+transformStatsStmt(CreateStatsStmt *stmt, const char *queryString)
{
ParseState *pstate;
ParseNamespaceItem *nsitem;
ListCell *l;
Relation rel;
+ RangeTblEntry *rte;
/* Nothing to do if statement already transformed. */
if (stmt->transformed)
return stmt;
+ /* Construct range table if not done yet. */
+ if (stmt->rtable == NIL)
+ stmt->rtable = transformStatsFromClause(stmt->from_clause);
+
+ /*
+ * The range table should have been derived from a FROM clause consisting
+ * of a single RangeVar, so we expect a single RangeTblEntry.
+ */
+ Assert(list_length(stmt->rtable) == 1);
+ rte = linitial(stmt->rtable);
+ Assert(IsA(rte, RangeTblEntry));
+
+ /* See transformStatsFromClause for notes on lock level. */
+ rel = table_open(rte->relid, NoLock);
+
/* Set up pstate */
pstate = make_parsestate(NULL);
pstate->p_sourcetext = queryString;
/*
* Put the parent table into the rtable so that the expressions can refer
- * to its fields without qualification. Caller is responsible for locking
- * relation, but we still need to open it.
+ * to its fields without qualification.
*/
- rel = relation_open(relid, NoLock);
nsitem = addRangeTableEntryForRelation(pstate, rel,
AccessShareLock,
NULL, false, true);
@@ -3208,6 +3231,91 @@ transformStatsStmt(Oid relid, CreateStatsStmt *stmt, const char *queryString)
return stmt;
}
+/*
+ * Accepts a list of RangeVar objects and returns a corresponding list of
+ * RangeTblEntry objects. For use while transforming a CreateStatsStmt.
+ *
+ * At present, the only case we know how to handle is a FROM clause
+ * consisting of a single RangeVar.
+ */
+List *
+transformStatsFromClause(List *from_clause)
+{
+ RangeTblEntry *rte;
+ ListCell *cell;
+ Relation rel = NULL;
+
+ /*
+ * Examine the FROM clause. Currently, we only allow it to be a single
+ * simple table, but later we'll probably allow multiple tables and JOIN
+ * syntax. The grammar is already prepared for that, so we have to check
+ * here that what we got is what we can support.
+ */
+ if (list_length(from_clause) != 1)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("only a single relation is allowed in CREATE STATISTICS"));
+
+ foreach(cell, from_clause)
+ {
+ Node *rln = (Node *) lfirst(cell);
+
+ if (!IsA(rln, RangeVar))
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("CREATE STATISTICS only supports relation names in the FROM clause"));
+
+ /*
+ * CREATE STATISTICS will influence future execution plans but does
+ * not interfere with currently executing plans. So it should be
+ * enough to take only ShareUpdateExclusiveLock on relation,
+ * conflicting with ANALYZE and other DDL that sets statistical
+ * information, but not with normal queries.
+ */
+ rel = relation_openrv((RangeVar *) rln, ShareUpdateExclusiveLock);
+
+ /* Restrict to allowed relation types */
+ if (rel->rd_rel->relkind != RELKIND_RELATION &&
+ rel->rd_rel->relkind != RELKIND_MATVIEW &&
+ rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE &&
+ rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot define statistics for relation \"%s\"",
+ RelationGetRelationName(rel)),
+ errdetail_relkind_not_supported(rel->rd_rel->relkind));
+
+ /*
+ * You must own the relation to create stats on it.
+ *
+ * NB: Concurrent changes could cause this function's lookup to find a
+ * different relation than a previous lookup by the caller, so we must
+ * perform this check unconditionally.
+ */
+ if (!object_ownercheck(RelationRelationId, RelationGetRelid(rel), GetUserId()))
+ aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(rel->rd_rel->relkind),
+ RelationGetRelationName(rel));
+
+ /* Creating statistics on system catalogs is not allowed */
+ if (!allowSystemTableMods && IsSystemRelation(rel))
+ ereport(ERROR,
+ errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied: \"%s\" is a system catalog",
+ RelationGetRelationName(rel)));
+ }
+
+ /* Create a range table entry. */
+ rte = makeNode(RangeTblEntry);
+ rte->rtekind = RTE_RELATION;
+ rte->relid = RelationGetRelid(rel);
+ rte->rellockmode = ShareUpdateExclusiveLock;
+
+ /* Close relation */
+ relation_close(rel, NoLock);
+
+ return list_make1(rte);
+}
+
/*
* transformRuleStmt -
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index d18a3a60a46..0c31113ebb5 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1874,34 +1874,7 @@ ProcessUtilitySlow(ParseState *pstate,
break;
case T_CreateStatsStmt:
- {
- Oid relid;
- CreateStatsStmt *stmt = (CreateStatsStmt *) parsetree;
- RangeVar *rel = (RangeVar *) linitial(stmt->relations);
-
- if (!IsA(rel, RangeVar))
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("CREATE STATISTICS only supports relation names in the FROM clause")));
-
- /*
- * CREATE STATISTICS will influence future execution plans
- * but does not interfere with currently executing plans.
- * So it should be enough to take ShareUpdateExclusiveLock
- * on relation, conflicting with ANALYZE and other DDL
- * that sets statistical information, but not with normal
- * queries.
- *
- * XXX RangeVarCallbackOwnsRelation not needed here, to
- * keep the same behavior as before.
- */
- relid = RangeVarGetRelid(rel, ShareUpdateExclusiveLock, false);
-
- /* Run parse analysis ... */
- stmt = transformStatsStmt(relid, stmt, queryString);
-
- address = CreateStatistics(stmt, true);
- }
+ address = CreateStatistics((CreateStatsStmt *) parsetree, true, queryString);
break;
case T_AlterStatsStmt:
diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h
index f3432b4b6a1..95397f91c5a 100644
--- a/src/include/commands/defrem.h
+++ b/src/include/commands/defrem.h
@@ -85,7 +85,7 @@ extern void RemoveOperatorById(Oid operOid);
extern ObjectAddress AlterOperator(AlterOperatorStmt *stmt);
/* commands/statscmds.c */
-extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt, bool check_rights);
+extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt, bool check_rights, const char *queryString);
extern ObjectAddress AlterStatistics(AlterStatsStmt *stmt);
extern void RemoveStatisticsById(Oid statsOid);
extern void RemoveStatisticsDataById(Oid statsOid, bool inh);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index bc7adba4a0f..326d30cfb2f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3551,6 +3551,10 @@ typedef struct CreateStatsStmt
List *stat_types; /* stat types (list of String) */
List *exprs; /* expressions to build statistics on */
List *relations; /* rels to build stats on (list of RangeVar) */
+ List *from_clause; /* FROM clause ((list of RangeVar)) */
+ List *rtable; /* It’s not derived directly from the
+ * parser, instead it comes from parse
+ * analysis. (list of RangeTblEntry) */
char *stxcomment; /* comment to apply to stats, or NULL */
bool transformed; /* true when transformStatsStmt is finished */
bool if_not_exists; /* do nothing if stats name already exists */
diff --git a/src/include/parser/parse_utilcmd.h b/src/include/parser/parse_utilcmd.h
index 4965fac4495..499e9ebef1c 100644
--- a/src/include/parser/parse_utilcmd.h
+++ b/src/include/parser/parse_utilcmd.h
@@ -26,8 +26,9 @@ extern AlterTableStmt *transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
List **afterStmts);
extern IndexStmt *transformIndexStmt(Oid relid, IndexStmt *stmt,
const char *queryString);
-extern CreateStatsStmt *transformStatsStmt(Oid relid, CreateStatsStmt *stmt,
+extern CreateStatsStmt *transformStatsStmt(CreateStatsStmt *stmt,
const char *queryString);
+extern List *transformStatsFromClause(List *from_clause);
extern void transformRuleStmt(RuleStmt *stmt, const char *queryString,
List **actions, Node **whereClause);
extern List *transformCreateSchemaStmtElements(List *schemaElts,
--
2.34.1