On Sat, Oct 31, 2026 at 1:24 PM Tom Lane <[email protected]> wrote:
> Good catch, but I really don't like this choice:
>
> > Actually instead of calling it from ExpandColumnRefStar, I'm calling it
> > from (one of) its callers, since it isn't always necessary.
>
> That's a recipe for future errors of omission; if someone adds another
> call of ExpandColumnRefStar, they might copy the wrong pattern.
>
> Another problem is that I'm not sure that every case of "something.*"
> produces Vars; foo().* would not, for example. It might be better to
> push the check down to someplace like expandNSItemVars.
Here is a patch that calls checkColumnRefAllowed from expandNSItemVars
instead. It allows foo().* but forbids (ROW(t.*)).f1.
I've included an optional (squashable) second patch that also calls it
from ExpandColumnRefStar, so that even when the table name can't be
resolved, we still get "cannot use column reference" instead of
"missing FROM-clause entry". That seems like the more fundamental
problem, and for defaults & partitions there is no FROM clause anyway.
I've also attached separate patch files for v14/15, since
expandNSItemVars has no pstate. I'm calling checkColumnRefAllowed from
its callers instead to preserve the ABI.
Yours,
--
Paul ~{:-)
[email protected]
From ab2f9f6efe6cdf2fc75a0d54da999b8288f84a28 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email protected]>
Date: Mon, 2 Nov 2026 13:51:38 -0800
Subject: [PATCH v2 2/2] Improve error message expanding foo.* in invalid
location
When a column reference is not allowed, we should use the same error message for
zz.a and (ROW(zz.*)).a. But currently the first gives "cannot use column
reference" and the latter "missing FROM-clause entry". This commit updates
ExpandColumnRefStar so that we get the first error message in both cases.
Author: Paul A. Jungwirth <[email protected]>
Backpatch-through: 14
---
src/backend/parser/parse_target.c | 7 +++++++
src/test/regress/expected/create_table.out | 8 ++++++++
src/test/regress/sql/create_table.sql | 2 ++
3 files changed, 17 insertions(+)
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 728936b502e..7a17ecce272 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -1127,6 +1127,13 @@ ExpandColumnRefStar(ParseState *pstate, ColumnRef *cref,
List *fields = cref->fields;
int numnames = list_length(fields);
+ /*
+ * Check to see if the column reference is in an invalid place, before
+ * looking up the name, as transformColumnRef does. expandNSItemVars
+ * would catch it anyway, but only if the name resolves.
+ */
+ checkColumnRefAllowed(pstate, cref->location);
+
if (numnames == 1)
{
/*
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out
index e7788b78c16..79d927b2dd0 100644
--- a/src/test/regress/expected/create_table.out
+++ b/src/test/regress/expected/create_table.out
@@ -133,6 +133,10 @@ CREATE TABLE default_expr_wholerow (id int DEFAULT (ROW(default_expr_wholerow.*)
ERROR: cannot use column reference in DEFAULT expression
LINE 1: ... TABLE default_expr_wholerow (id int DEFAULT (ROW(default_ex...
^
+CREATE TABLE default_expr_wholerow (id int DEFAULT (ROW(bar.*)).f1);
+ERROR: cannot use column reference in DEFAULT expression
+LINE 1: ... TABLE default_expr_wholerow (id int DEFAULT (ROW(bar.*)).f1...
+ ^
-- but a row constructor with no column reference in it is fine, even one
-- that expands a composite value that is not a table
CREATE TABLE default_expr_rowexpr (id int DEFAULT (ROW(1)).f1);
@@ -469,6 +473,10 @@ CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN ((ROW(p
ERROR: cannot use column reference in partition bound expression
LINE 1: ...fail PARTITION OF list_parted FOR VALUES IN ((ROW(part_bogus...
^
+CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN ((ROW(somename.*)).f1);
+ERROR: cannot use column reference in partition bound expression
+LINE 1: ...fail PARTITION OF list_parted FOR VALUES IN ((ROW(somename.*...
+ ^
CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (a);
ERROR: cannot use column reference in partition bound expression
LINE 1: ..._bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (a);
diff --git a/src/test/regress/sql/create_table.sql b/src/test/regress/sql/create_table.sql
index df4ace75d78..8b1de65756d 100644
--- a/src/test/regress/sql/create_table.sql
+++ b/src/test/regress/sql/create_table.sql
@@ -83,6 +83,7 @@ CREATE TABLE default_expr_column (id int DEFAULT (bar.id));
CREATE TABLE default_expr_agg_column (id int DEFAULT (avg(id)));
-- a whole-row reference is a column reference too
CREATE TABLE default_expr_wholerow (id int DEFAULT (ROW(default_expr_wholerow.*)).f1);
+CREATE TABLE default_expr_wholerow (id int DEFAULT (ROW(bar.*)).f1);
-- but a row constructor with no column reference in it is fine, even one
-- that expands a composite value that is not a table
CREATE TABLE default_expr_rowexpr (id int DEFAULT (ROW(1)).f1);
@@ -328,6 +329,7 @@ CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (somena
CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (somename.somename);
-- a whole-row reference to the partition being created is a column reference
CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN ((ROW(part_bogus_expr_fail.*)).f1);
+CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN ((ROW(somename.*)).f1);
CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (a);
CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (sum(a));
CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (sum(somename));
--
2.47.3
From 0b39071a40778fb3ad364d71094ab384001f09bb Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email protected]>
Date: Mon, 14 Sep 2026 12:41:10 -0700
Subject: [PATCH v2 1/2] Reject whole-row references where column references
are forbidden
There are a couple expression kinds that forbid column references (partition
bounds and DEFAULTs), but you could still inject one by going through a
whole-row reference: ExpandColumnRefStar wasn't doing the same check as
transformColumnRef. That allowed these errors:
-- crashes
ALTER TABLE t ADD COLUMN b int DEFAULT (ROW(t.*)).f1;
-- crashes
CREATE TABLE p1 PARTITION OF p FOR VALUES IN ((ROW(p1.*)).f1);
-- wrongly accepted
CREATE TABLE t (a int, b int DEFAULT (ROW(t.*)).f1);
ALTER TABLE t ALTER COLUMN b SET DEFAULT (ROW(t.*)).f1;
INSERT INTO t (a) VALUES (1); -- crashes here instead
I moved the check out of transformColumnRef into a helper, and also called it
from expandNSItemVars, which every expansion of a table's columns goes
through. Expanding a composite value that isn't a table, like foo().*, makes
no Vars, so it is still allowed.
Author: Paul A. Jungwirth <[email protected]>
Backpatch-through: 14
---
src/backend/parser/parse_expr.c | 62 +++++++++++++---------
src/backend/parser/parse_relation.c | 5 ++
src/include/parser/parse_expr.h | 2 +
src/test/regress/expected/alter_table.out | 8 +++
src/test/regress/expected/create_table.out | 23 ++++++++
src/test/regress/sql/alter_table.sql | 6 +++
src/test/regress/sql/create_table.sql | 12 +++++
7 files changed, 93 insertions(+), 25 deletions(-)
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 728392f436f..e4c33163588 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -502,39 +502,24 @@ transformIndirection(ParseState *pstate, A_Indirection *ind)
}
/*
- * Transform a ColumnRef.
+ * checkColumnRefAllowed
+ * Complain if a column reference is not allowed in the expression
+ * being transformed.
*
- * If you find yourself changing this code, see also ExpandColumnRefStar.
+ * We allow column references in most places, except in a few kinds of
+ * expression that must produce the same value for every row, namely default
+ * expressions and partition bound expressions. Outside of any expression,
+ * as when expanding "*" in a SELECT list, they are always allowed.
*/
-static Node *
-transformColumnRef(ParseState *pstate, ColumnRef *cref)
+void
+checkColumnRefAllowed(ParseState *pstate, int location)
{
- Node *node = NULL;
- char *nspname = NULL;
- char *relname = NULL;
- char *colname = NULL;
- ParseNamespaceItem *nsitem;
- int levels_up;
- enum
- {
- CRERR_NO_COLUMN,
- CRERR_NO_RTE,
- CRERR_WRONG_DB,
- CRERR_TOO_MANY
- } crerr = CRERR_NO_COLUMN;
const char *err;
- /*
- * Check to see if the column reference is in an invalid place within the
- * query. We allow column references in most places, except in default
- * expressions and partition bound expressions.
- */
err = NULL;
switch (pstate->p_expr_kind)
{
case EXPR_KIND_NONE:
- Assert(false); /* can't happen */
- break;
case EXPR_KIND_OTHER:
case EXPR_KIND_JOIN_ON:
case EXPR_KIND_JOIN_USING:
@@ -599,7 +584,34 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg_internal("%s", err),
- parser_errposition(pstate, cref->location)));
+ parser_errposition(pstate, location)));
+}
+
+/*
+ * Transform a ColumnRef.
+ *
+ * If you find yourself changing this code, see also ExpandColumnRefStar.
+ */
+static Node *
+transformColumnRef(ParseState *pstate, ColumnRef *cref)
+{
+ Node *node = NULL;
+ char *nspname = NULL;
+ char *relname = NULL;
+ char *colname = NULL;
+ ParseNamespaceItem *nsitem;
+ int levels_up;
+ enum
+ {
+ CRERR_NO_COLUMN,
+ CRERR_NO_RTE,
+ CRERR_WRONG_DB,
+ CRERR_TOO_MANY
+ } crerr = CRERR_NO_COLUMN;
+
+ /* Check to see if the column reference is in an invalid place. */
+ Assert(pstate->p_expr_kind != EXPR_KIND_NONE);
+ checkColumnRefAllowed(pstate, cref->location);
/*
* Give the PreParseColumnRefHook, if any, first shot. If it returns
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index 8aeb3e219ac..140e40bf74e 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -25,6 +25,7 @@
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "parser/parse_enr.h"
+#include "parser/parse_expr.h"
#include "parser/parse_relation.h"
#include "parser/parse_type.h"
#include "parser/parsetree.h"
@@ -3238,6 +3239,8 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset,
* for the non-dropped columns of the nsitem.
*
* The emitted Vars are marked with the given sublevels_up and location.
+ * They are column references, so we complain if those are forbidden in the
+ * expression being transformed.
*
* If colnames isn't NULL, a list of String items for the columns is stored
* there; note that it's just a subset of the RTE's eref list, and hence
@@ -3252,6 +3255,8 @@ expandNSItemVars(ParseState *pstate, ParseNamespaceItem *nsitem,
int colindex;
ListCell *lc;
+ checkColumnRefAllowed(pstate, location);
+
if (colnames)
*colnames = NIL;
colindex = 0;
diff --git a/src/include/parser/parse_expr.h b/src/include/parser/parse_expr.h
index e3de50c0d26..bbfe18a70c6 100644
--- a/src/include/parser/parse_expr.h
+++ b/src/include/parser/parse_expr.h
@@ -20,6 +20,8 @@ extern PGDLLIMPORT bool Transform_null_equals;
extern Node *transformExpr(ParseState *pstate, Node *expr, ParseExprKind exprKind);
+extern void checkColumnRefAllowed(ParseState *pstate, int location);
+
extern const char *ParseExprKindName(ParseExprKind exprKind);
#endif /* PARSE_EXPR_H */
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 20707c186b3..6983e60a99e 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -1351,6 +1351,14 @@ alter table def_test alter column c2 set default 20;
-- set defaults on a non-existent column: this should fail
alter table def_test alter column c3 set default 30;
ERROR: column "c3" of relation "def_test" does not exist
+-- a default may not reference a column, not even through a whole-row
+-- reference: these should all fail
+alter table def_test alter column c1 set default def_test.c1;
+ERROR: cannot use column reference in DEFAULT expression
+alter table def_test alter column c1 set default (ROW(def_test.*)).f1;
+ERROR: cannot use column reference in DEFAULT expression
+alter table def_test add column c4 int default (ROW(def_test.*)).f1;
+ERROR: cannot use column reference in DEFAULT expression
-- set defaults on views: we need to create a view, add a rule
-- to allow insertions into it, and then alter the view to add
-- a default
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out
index 2df8761ae8e..e7788b78c16 100644
--- a/src/test/regress/expected/create_table.out
+++ b/src/test/regress/expected/create_table.out
@@ -128,6 +128,24 @@ CREATE TABLE default_expr_agg_column (id int DEFAULT (avg(id)));
ERROR: cannot use column reference in DEFAULT expression
LINE 1: ...TE TABLE default_expr_agg_column (id int DEFAULT (avg(id)));
^
+-- a whole-row reference is a column reference too
+CREATE TABLE default_expr_wholerow (id int DEFAULT (ROW(default_expr_wholerow.*)).f1);
+ERROR: cannot use column reference in DEFAULT expression
+LINE 1: ... TABLE default_expr_wholerow (id int DEFAULT (ROW(default_ex...
+ ^
+-- but a row constructor with no column reference in it is fine, even one
+-- that expands a composite value that is not a table
+CREATE TABLE default_expr_rowexpr (id int DEFAULT (ROW(1)).f1);
+DROP TABLE default_expr_rowexpr;
+CREATE TABLE default_expr_rowexpr (id int DEFAULT (ROW((ROW(1, 2)).*)).f2);
+INSERT INTO default_expr_rowexpr DEFAULT VALUES;
+SELECT * FROM default_expr_rowexpr;
+ id
+----
+ 2
+(1 row)
+
+DROP TABLE default_expr_rowexpr;
-- invalid column definition
CREATE TABLE default_expr_non_column (a int DEFAULT (avg(non_existent)));
ERROR: cannot use column reference in DEFAULT expression
@@ -446,6 +464,11 @@ CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (somena
ERROR: cannot use column reference in partition bound expression
LINE 1: ...expr_fail PARTITION OF list_parted FOR VALUES IN (somename.s...
^
+-- a whole-row reference to the partition being created is a column reference
+CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN ((ROW(part_bogus_expr_fail.*)).f1);
+ERROR: cannot use column reference in partition bound expression
+LINE 1: ...fail PARTITION OF list_parted FOR VALUES IN ((ROW(part_bogus...
+ ^
CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (a);
ERROR: cannot use column reference in partition bound expression
LINE 1: ..._bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (a);
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index 18f3fdac57a..af84b7a02d3 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -987,6 +987,12 @@ alter table def_test alter column c2 set default 20;
-- set defaults on a non-existent column: this should fail
alter table def_test alter column c3 set default 30;
+-- a default may not reference a column, not even through a whole-row
+-- reference: these should all fail
+alter table def_test alter column c1 set default def_test.c1;
+alter table def_test alter column c1 set default (ROW(def_test.*)).f1;
+alter table def_test add column c4 int default (ROW(def_test.*)).f1;
+
-- set defaults on views: we need to create a view, add a rule
-- to allow insertions into it, and then alter the view to add
-- a default
diff --git a/src/test/regress/sql/create_table.sql b/src/test/regress/sql/create_table.sql
index 80e424e6bda..df4ace75d78 100644
--- a/src/test/regress/sql/create_table.sql
+++ b/src/test/regress/sql/create_table.sql
@@ -81,6 +81,16 @@ DROP TABLE relation_filenode_check;
CREATE TABLE default_expr_column (id int DEFAULT (id));
CREATE TABLE default_expr_column (id int DEFAULT (bar.id));
CREATE TABLE default_expr_agg_column (id int DEFAULT (avg(id)));
+-- a whole-row reference is a column reference too
+CREATE TABLE default_expr_wholerow (id int DEFAULT (ROW(default_expr_wholerow.*)).f1);
+-- but a row constructor with no column reference in it is fine, even one
+-- that expands a composite value that is not a table
+CREATE TABLE default_expr_rowexpr (id int DEFAULT (ROW(1)).f1);
+DROP TABLE default_expr_rowexpr;
+CREATE TABLE default_expr_rowexpr (id int DEFAULT (ROW((ROW(1, 2)).*)).f2);
+INSERT INTO default_expr_rowexpr DEFAULT VALUES;
+SELECT * FROM default_expr_rowexpr;
+DROP TABLE default_expr_rowexpr;
-- invalid column definition
CREATE TABLE default_expr_non_column (a int DEFAULT (avg(non_existent)));
-- invalid use of aggregate
@@ -316,6 +326,8 @@ CREATE TABLE part_null PARTITION OF list_parted FOR VALUES IN (null);
-- forbidden expressions for partition bound with list partitioned table
CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (somename);
CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (somename.somename);
+-- a whole-row reference to the partition being created is a column reference
+CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN ((ROW(part_bogus_expr_fail.*)).f1);
CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (a);
CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (sum(a));
CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (sum(somename));
--
2.47.3
From 38d0a90035f7de3f6eac289089b8c20e4e395f76 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email protected]>
Date: Mon, 2 Nov 2026 14:50:56 -0800
Subject: [PATCH v2 1/2] Reject whole-row references where column references
are forbidden
There are a couple expression kinds that forbid column references (partition
bounds and DEFAULTs), but you could still inject one by going through a
whole-row reference: ExpandColumnRefStar wasn't doing the same check as
transformColumnRef. That allowed these errors:
-- crashes
ALTER TABLE t ADD COLUMN b int DEFAULT (ROW(t.*)).f1;
-- crashes
CREATE TABLE p1 PARTITION OF p FOR VALUES IN ((ROW(p1.*)).f1);
-- wrongly accepted
CREATE TABLE t (a int, b int DEFAULT (ROW(t.*)).f1);
ALTER TABLE t ALTER COLUMN b SET DEFAULT (ROW(t.*)).f1;
INSERT INTO t (a) VALUES (1); -- crashes here instead
I moved the check out of transformColumnRef into a helper, and also called it
from each caller of expandNSItemVars that can put the Vars in an expression.
On master the call is inside expandNSItemVars, but here that function has no
ParseState, and adding one would change an exported signature. Expanding a
composite value that isn't a table, like foo().*, makes no Vars, so it is
still allowed.
Author: Paul A. Jungwirth <[email protected]>
Backpatch-through: 14
---
src/backend/parser/parse_coerce.c | 2 +
src/backend/parser/parse_expr.c | 62 +++++++++++++---------
src/backend/parser/parse_relation.c | 4 ++
src/backend/parser/parse_target.c | 1 +
src/include/parser/parse_expr.h | 2 +
src/test/regress/expected/alter_table.out | 8 +++
src/test/regress/expected/create_table.out | 23 ++++++++
src/test/regress/sql/alter_table.sql | 6 +++
src/test/regress/sql/create_table.sql | 12 +++++
9 files changed, 95 insertions(+), 25 deletions(-)
diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c
index cef31b9a5fe..d0521124aa6 100644
--- a/src/backend/parser/parse_coerce.c
+++ b/src/backend/parser/parse_coerce.c
@@ -22,6 +22,7 @@
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "parser/parse_coerce.h"
+#include "parser/parse_expr.h"
#include "parser/parse_relation.h"
#include "parser/parse_type.h"
#include "utils/builtins.h"
@@ -1046,6 +1047,7 @@ coerce_record_to_complex(ParseState *pstate, Node *node,
ParseNamespaceItem *nsitem;
nsitem = GetNSItemByRangeTablePosn(pstate, rtindex, sublevels_up);
+ checkColumnRefAllowed(pstate, vlocation);
args = expandNSItemVars(nsitem, sublevels_up, vlocation, NULL);
}
else
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 2e0a5b797a1..95a57b13776 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -431,39 +431,24 @@ transformIndirection(ParseState *pstate, A_Indirection *ind)
}
/*
- * Transform a ColumnRef.
+ * checkColumnRefAllowed
+ * Complain if a column reference is not allowed in the expression
+ * being transformed.
*
- * If you find yourself changing this code, see also ExpandColumnRefStar.
+ * We allow column references in most places, except in a few kinds of
+ * expression that must produce the same value for every row, namely default
+ * expressions and partition bound expressions. Outside of any expression,
+ * as when expanding "*" in a SELECT list, they are always allowed.
*/
-static Node *
-transformColumnRef(ParseState *pstate, ColumnRef *cref)
+void
+checkColumnRefAllowed(ParseState *pstate, int location)
{
- Node *node = NULL;
- char *nspname = NULL;
- char *relname = NULL;
- char *colname = NULL;
- ParseNamespaceItem *nsitem;
- int levels_up;
- enum
- {
- CRERR_NO_COLUMN,
- CRERR_NO_RTE,
- CRERR_WRONG_DB,
- CRERR_TOO_MANY
- } crerr = CRERR_NO_COLUMN;
const char *err;
- /*
- * Check to see if the column reference is in an invalid place within the
- * query. We allow column references in most places, except in default
- * expressions and partition bound expressions.
- */
err = NULL;
switch (pstate->p_expr_kind)
{
case EXPR_KIND_NONE:
- Assert(false); /* can't happen */
- break;
case EXPR_KIND_OTHER:
case EXPR_KIND_JOIN_ON:
case EXPR_KIND_JOIN_USING:
@@ -527,7 +512,34 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg_internal("%s", err),
- parser_errposition(pstate, cref->location)));
+ parser_errposition(pstate, location)));
+}
+
+/*
+ * Transform a ColumnRef.
+ *
+ * If you find yourself changing this code, see also ExpandColumnRefStar.
+ */
+static Node *
+transformColumnRef(ParseState *pstate, ColumnRef *cref)
+{
+ Node *node = NULL;
+ char *nspname = NULL;
+ char *relname = NULL;
+ char *colname = NULL;
+ ParseNamespaceItem *nsitem;
+ int levels_up;
+ enum
+ {
+ CRERR_NO_COLUMN,
+ CRERR_NO_RTE,
+ CRERR_WRONG_DB,
+ CRERR_TOO_MANY
+ } crerr = CRERR_NO_COLUMN;
+
+ /* Check to see if the column reference is in an invalid place. */
+ Assert(pstate->p_expr_kind != EXPR_KIND_NONE);
+ checkColumnRefAllowed(pstate, cref->location);
/*
* Give the PreParseColumnRefHook, if any, first shot. If it returns
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index e0066830299..860a5bf0e60 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -27,6 +27,7 @@
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "parser/parse_enr.h"
+#include "parser/parse_expr.h"
#include "parser/parse_relation.h"
#include "parser/parse_type.h"
#include "parser/parsetree.h"
@@ -3098,6 +3099,8 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset,
* for the non-dropped columns of the nsitem.
*
* The emitted Vars are marked with the given sublevels_up and location.
+ * They are column references, so a caller putting them in an expression must
+ * first check with checkColumnRefAllowed that those are allowed there.
*
* If colnames isn't NULL, a list of String items for the columns is stored
* there; note that it's just a subset of the RTE's eref list, and hence
@@ -3174,6 +3177,7 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem,
*var;
List *te_list = NIL;
+ checkColumnRefAllowed(pstate, location);
vars = expandNSItemVars(nsitem, sublevels_up, location, &names);
/*
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 4d6d88c33a4..e0dd6705a63 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -1393,6 +1393,7 @@ ExpandSingleTable(ParseState *pstate, ParseNamespaceItem *nsitem,
List *vars;
ListCell *l;
+ checkColumnRefAllowed(pstate, location);
vars = expandNSItemVars(nsitem, sublevels_up, location, NULL);
/*
diff --git a/src/include/parser/parse_expr.h b/src/include/parser/parse_expr.h
index c8e5c57b430..e6527ad07d6 100644
--- a/src/include/parser/parse_expr.h
+++ b/src/include/parser/parse_expr.h
@@ -20,6 +20,8 @@ extern PGDLLIMPORT bool Transform_null_equals;
extern Node *transformExpr(ParseState *pstate, Node *expr, ParseExprKind exprKind);
+extern void checkColumnRefAllowed(ParseState *pstate, int location);
+
extern const char *ParseExprKindName(ParseExprKind exprKind);
#endif /* PARSE_EXPR_H */
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index c0ea4c9ec1e..fb51a75c37e 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -1239,6 +1239,14 @@ alter table def_test alter column c2 set default 20;
-- set defaults on a non-existent column: this should fail
alter table def_test alter column c3 set default 30;
ERROR: column "c3" of relation "def_test" does not exist
+-- a default may not reference a column, not even through a whole-row
+-- reference: these should all fail
+alter table def_test alter column c1 set default def_test.c1;
+ERROR: cannot use column reference in DEFAULT expression
+alter table def_test alter column c1 set default (ROW(def_test.*)).f1;
+ERROR: cannot use column reference in DEFAULT expression
+alter table def_test add column c4 int default (ROW(def_test.*)).f1;
+ERROR: cannot use column reference in DEFAULT expression
-- set defaults on views: we need to create a view, add a rule
-- to allow insertions into it, and then alter the view to add
-- a default
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out
index 4806be67706..c3e40b0a340 100644
--- a/src/test/regress/expected/create_table.out
+++ b/src/test/regress/expected/create_table.out
@@ -118,6 +118,24 @@ CREATE TABLE default_expr_agg_column (id int DEFAULT (avg(id)));
ERROR: cannot use column reference in DEFAULT expression
LINE 1: ...TE TABLE default_expr_agg_column (id int DEFAULT (avg(id)));
^
+-- a whole-row reference is a column reference too
+CREATE TABLE default_expr_wholerow (id int DEFAULT (ROW(default_expr_wholerow.*)).f1);
+ERROR: cannot use column reference in DEFAULT expression
+LINE 1: ... TABLE default_expr_wholerow (id int DEFAULT (ROW(default_ex...
+ ^
+-- but a row constructor with no column reference in it is fine, even one
+-- that expands a composite value that is not a table
+CREATE TABLE default_expr_rowexpr (id int DEFAULT (ROW(1)).f1);
+DROP TABLE default_expr_rowexpr;
+CREATE TABLE default_expr_rowexpr (id int DEFAULT (ROW((ROW(1, 2)).*)).f2);
+INSERT INTO default_expr_rowexpr DEFAULT VALUES;
+SELECT * FROM default_expr_rowexpr;
+ id
+----
+ 2
+(1 row)
+
+DROP TABLE default_expr_rowexpr;
-- invalid column definition
CREATE TABLE default_expr_non_column (a int DEFAULT (avg(non_existent)));
ERROR: cannot use column reference in DEFAULT expression
@@ -435,6 +453,11 @@ CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (somena
ERROR: cannot use column reference in partition bound expression
LINE 1: ...expr_fail PARTITION OF list_parted FOR VALUES IN (somename.s...
^
+-- a whole-row reference to the partition being created is a column reference
+CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN ((ROW(part_bogus_expr_fail.*)).f1);
+ERROR: cannot use column reference in partition bound expression
+LINE 1: ...fail PARTITION OF list_parted FOR VALUES IN ((ROW(part_bogus...
+ ^
CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (a);
ERROR: cannot use column reference in partition bound expression
LINE 1: ..._bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (a);
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index 2bbabd4b528..c7bf5fe130a 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -950,6 +950,12 @@ alter table def_test alter column c2 set default 20;
-- set defaults on a non-existent column: this should fail
alter table def_test alter column c3 set default 30;
+-- a default may not reference a column, not even through a whole-row
+-- reference: these should all fail
+alter table def_test alter column c1 set default def_test.c1;
+alter table def_test alter column c1 set default (ROW(def_test.*)).f1;
+alter table def_test add column c4 int default (ROW(def_test.*)).f1;
+
-- set defaults on views: we need to create a view, add a rule
-- to allow insertions into it, and then alter the view to add
-- a default
diff --git a/src/test/regress/sql/create_table.sql b/src/test/regress/sql/create_table.sql
index b761b63daf1..eb30e2eecd3 100644
--- a/src/test/regress/sql/create_table.sql
+++ b/src/test/regress/sql/create_table.sql
@@ -75,6 +75,16 @@ DROP TABLE relation_filenode_check;
CREATE TABLE default_expr_column (id int DEFAULT (id));
CREATE TABLE default_expr_column (id int DEFAULT (bar.id));
CREATE TABLE default_expr_agg_column (id int DEFAULT (avg(id)));
+-- a whole-row reference is a column reference too
+CREATE TABLE default_expr_wholerow (id int DEFAULT (ROW(default_expr_wholerow.*)).f1);
+-- but a row constructor with no column reference in it is fine, even one
+-- that expands a composite value that is not a table
+CREATE TABLE default_expr_rowexpr (id int DEFAULT (ROW(1)).f1);
+DROP TABLE default_expr_rowexpr;
+CREATE TABLE default_expr_rowexpr (id int DEFAULT (ROW((ROW(1, 2)).*)).f2);
+INSERT INTO default_expr_rowexpr DEFAULT VALUES;
+SELECT * FROM default_expr_rowexpr;
+DROP TABLE default_expr_rowexpr;
-- invalid column definition
CREATE TABLE default_expr_non_column (a int DEFAULT (avg(non_existent)));
-- invalid use of aggregate
@@ -309,6 +319,8 @@ CREATE TABLE part_null PARTITION OF list_parted FOR VALUES IN (null);
-- forbidden expressions for partition bound with list partitioned table
CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (somename);
CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (somename.somename);
+-- a whole-row reference to the partition being created is a column reference
+CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN ((ROW(part_bogus_expr_fail.*)).f1);
CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (a);
CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (sum(a));
CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (sum(somename));
--
2.47.3
From eea7d2d5ee3cbaefd171c42ae922e715091a40f1 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email protected]>
Date: Mon, 2 Nov 2026 13:51:38 -0800
Subject: [PATCH v2 2/2] Improve error message expanding foo.* in invalid
location
When a column reference is not allowed, we should use the same error message for
zz.a and (ROW(zz.*)).a. But currently the first gives "cannot use column
reference" and the latter "missing FROM-clause entry". This commit updates
ExpandColumnRefStar so that we get the first error message in both cases.
Author: Paul A. Jungwirth <[email protected]>
Backpatch-through: 14
---
src/backend/parser/parse_target.c | 7 +++++++
src/test/regress/expected/create_table.out | 8 ++++++++
src/test/regress/sql/create_table.sql | 2 ++
3 files changed, 17 insertions(+)
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index e0dd6705a63..f73e1480160 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -1133,6 +1133,13 @@ ExpandColumnRefStar(ParseState *pstate, ColumnRef *cref,
List *fields = cref->fields;
int numnames = list_length(fields);
+ /*
+ * Check to see if the column reference is in an invalid place, before
+ * looking up the name, as transformColumnRef does. expandNSItemVars
+ * would catch it anyway, but only if the name resolves.
+ */
+ checkColumnRefAllowed(pstate, cref->location);
+
if (numnames == 1)
{
/*
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out
index c3e40b0a340..4aec75f9d44 100644
--- a/src/test/regress/expected/create_table.out
+++ b/src/test/regress/expected/create_table.out
@@ -123,6 +123,10 @@ CREATE TABLE default_expr_wholerow (id int DEFAULT (ROW(default_expr_wholerow.*)
ERROR: cannot use column reference in DEFAULT expression
LINE 1: ... TABLE default_expr_wholerow (id int DEFAULT (ROW(default_ex...
^
+CREATE TABLE default_expr_wholerow (id int DEFAULT (ROW(bar.*)).f1);
+ERROR: cannot use column reference in DEFAULT expression
+LINE 1: ... TABLE default_expr_wholerow (id int DEFAULT (ROW(bar.*)).f1...
+ ^
-- but a row constructor with no column reference in it is fine, even one
-- that expands a composite value that is not a table
CREATE TABLE default_expr_rowexpr (id int DEFAULT (ROW(1)).f1);
@@ -458,6 +462,10 @@ CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN ((ROW(p
ERROR: cannot use column reference in partition bound expression
LINE 1: ...fail PARTITION OF list_parted FOR VALUES IN ((ROW(part_bogus...
^
+CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN ((ROW(somename.*)).f1);
+ERROR: cannot use column reference in partition bound expression
+LINE 1: ...fail PARTITION OF list_parted FOR VALUES IN ((ROW(somename.*...
+ ^
CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (a);
ERROR: cannot use column reference in partition bound expression
LINE 1: ..._bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (a);
diff --git a/src/test/regress/sql/create_table.sql b/src/test/regress/sql/create_table.sql
index eb30e2eecd3..79a86dc9095 100644
--- a/src/test/regress/sql/create_table.sql
+++ b/src/test/regress/sql/create_table.sql
@@ -77,6 +77,7 @@ CREATE TABLE default_expr_column (id int DEFAULT (bar.id));
CREATE TABLE default_expr_agg_column (id int DEFAULT (avg(id)));
-- a whole-row reference is a column reference too
CREATE TABLE default_expr_wholerow (id int DEFAULT (ROW(default_expr_wholerow.*)).f1);
+CREATE TABLE default_expr_wholerow (id int DEFAULT (ROW(bar.*)).f1);
-- but a row constructor with no column reference in it is fine, even one
-- that expands a composite value that is not a table
CREATE TABLE default_expr_rowexpr (id int DEFAULT (ROW(1)).f1);
@@ -321,6 +322,7 @@ CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (somena
CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (somename.somename);
-- a whole-row reference to the partition being created is a column reference
CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN ((ROW(part_bogus_expr_fail.*)).f1);
+CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN ((ROW(somename.*)).f1);
CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (a);
CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (sum(a));
CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (sum(somename));
--
2.47.3