[PATCH] Add support for INSERT ... SET syntax
Hackorum builds and tests every patch posted to the lists, not only commitfest submissions. This is Hackorum's own CI rather than the PostgreSQL project's, and it is still under testing - please report anything that looks wrong.
You can run a PostgreSQL built from this patch straight from Docker, with no checkout and no build:
docker run --rm -p 5432:5432 ghcr.io/hackorum-dev/postgres-patch:t139169psql -h localhost -U postgresBuilt from patchset v20 (message #20), September 20, 2026 at 12:22 PM.
Every patchset is also pushed to a branch of our PostgreSQL fork, so you can check out the same tree CI built. Without a PostgreSQL checkout:
git clone --branch t139169_20 https://github.com/hackorum-dev/postgres.gitIn a checkout you already have, add the fork once:
git remote add hackorum https://github.com/hackorum-dev/postgres.gitthen, for this patchset and every later one:
git fetch hackorum t139169_20 && git checkout t139169_20Patchset v20 (message #20) is on t139169_20
Hi,
I would like to propose adding support for an alternative INSERT syntax
that uses named column assignments via a SET clause. This provides a more
convenient and readable way to write inserts, particularly when only
specific columns need values.
Currently, PostgreSQL requires INSERT statements to separate the column
list from the values:
INSERT INTO users (name, email, status) VALUES ('Alice', '
alice@example.com', 'active');
For inserts with many columns or where only a subset of columns are
specified, the proposed SET syntax offers better readability by keeping
column names adjacent to their values:
INSERT INTO users SET name='Alice', email='alice@example.com',
status='active';
Proposed Syntax:
INSERT INTO table_name
SET (column1=value1, column2=value2, ...), (, ...)
[ ON CONFLICT ... ]
[ RETURNING ... ];
Below INSERT features are supported:
- DEFAULT keyword: SET col=DEFAULT
- Expressions and functions: SET col=expr, col2=function(...)
- Subqueries: SET col=(SELECT ...)
- RETURNING clause
- ON CONFLICT DO UPDATE/NOTHING
- OVERRIDING SYSTEM VALUE
- Multi-row syntax: SET (col1=val1, col2=val2), (col1=val3, col2=val4)
Columns not mentioned receive their default values or NULL, consistent with
standard INSERT behavior.
I've attached the patch. Looking forward to your feedback.
--
Thanks & Regards,
Suraj kharage,
enterprisedb.com <https://www.enterprisedb.com/>
Attachments:
v1-0001-Add-support-for-INSERT-.-SET-syntax.patchapplication/octet-stream; name=v1-0001-Add-support-for-INSERT-.-SET-syntax.patchDownload+695-14
On Monday, March 30, 2026, Suraj Kharage <suraj.kharage@enterprisedb.com>
wrote:
I would like to propose adding support for an alternative INSERT syntax
that uses named column assignments via a SET clause. This provides a more
convenient and readable way to write inserts, particularly when only
specific columns need values.
-1 for inventing our own full variant of insert command syntax.
David J.
"David G. Johnston" <david.g.johnston@gmail.com> writes:
On Monday, March 30, 2026, Suraj Kharage <suraj.kharage@enterprisedb.com>
wrote:I would like to propose adding support for an alternative INSERT syntax
that uses named column assignments via a SET clause. This provides a more
convenient and readable way to write inserts, particularly when only
specific columns need values.
-1 for inventing our own full variant of insert command syntax.
This has been discussed before, no? I don't recall exactly why
we didn't adopt the earlier proposal(s), but some digging in the
mailing list archives should be fruitful.
regards, tom lane
On 2026-03-30 Mo 10:38 AM, David G. Johnston wrote:
On Monday, March 30, 2026, Suraj Kharage
<suraj.kharage@enterprisedb.com> wrote:I would like to propose adding support for an alternative INSERT
syntax that uses named column assignments via a SET clause. This
provides a more convenient and readable way to write inserts,
particularly when only specific columns need values.-1 for inventing our own full variant of insert command syntax.
Well, Suraj has kinda beaten me to it, but he didn't invent this syntax.
Oracle did
<https://oracle-base.com/articles/23/non-positional-insert-into-set-and-insert-into-by-name-clauses-23>
and I believe there is a proposal to add it to the standard. (Unlike
Suraj's, my WIP patch also supports the INSERT BY NAME variant.)
cheers
andrew
--
Andrew Dunstan
EDB:https://www.enterprisedb.com
On 2026-Mar-30, Andrew Dunstan wrote:
Well, Suraj has kinda beaten me to it, but he didn't invent this syntax.
Oracle did <https://oracle-base.com/articles/23/non-positional-insert-into-set-and-insert-into-by-name-clauses-23>
and I believe there is a proposal to add it to the standard. (Unlike
Suraj's, my WIP patch also supports the INSERT BY NAME variant.)
Hmm, I don't see any WIP patch from you -- are you talking about this
patch from July 2019?
/messages/by-id/CA+A-St+NntBh2EGu3a0xbVxJFzaeEOn=Vn_V84OuhM59_HKarQ@mail.gmail.com
Funnily enough, we have an even older proposal from 2016,
/messages/by-id/709e06c0-59c9-ccec-d216-21e38cb5ed61@joh.to
It seems this is quite a popular missing feature, as Marko's patch was
also asked about in February 2019:
/messages/by-id/e58dd487-7ed4-3f95-c63c-24200ed768be@berkvens.net
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
On 2026-03-30 Mo 11:49 AM, Álvaro Herrera wrote:
On 2026-Mar-30, Andrew Dunstan wrote:
Well, Suraj has kinda beaten me to it, but he didn't invent this syntax.
Oracle did <https://oracle-base.com/articles/23/non-positional-insert-into-set-and-insert-into-by-name-clauses-23>
and I believe there is a proposal to add it to the standard. (Unlike
Suraj's, my WIP patch also supports the INSERT BY NAME variant.)Hmm, I don't see any WIP patch from you
No, I haven't submitted it, still working on it. Given Suraj's work, I
will probably just submit a patch for INSERT BY NAME now.
cheers
andrew
--
Andrew Dunstan
EDB: https://www.enterprisedb.com
Hello all,
I am reporting a server crash encountered while testing the patch
provided by Suraj. The crash is consistently triggered by the query
attached below.
postgres=# INSERT INTO emp_test SET (empno,ename)=(SELECT 1,'aa');
server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
The connection to the server was lost. Attempting reset: Failed.
Thanks
Ajay
Show quoted text
On Mon, Mar 30, 2026 at 9:19 PM Álvaro Herrera <alvherre@kurilemu.de> wrote:
On 2026-Mar-30, Andrew Dunstan wrote:
Well, Suraj has kinda beaten me to it, but he didn't invent this syntax.
Oracle did <https://oracle-base.com/articles/23/non-positional-insert-into-set-and-insert-into-by-name-clauses-23>
and I believe there is a proposal to add it to the standard. (Unlike
Suraj's, my WIP patch also supports the INSERT BY NAME variant.)Hmm, I don't see any WIP patch from you -- are you talking about this
patch from July 2019?
/messages/by-id/CA+A-St+NntBh2EGu3a0xbVxJFzaeEOn=Vn_V84OuhM59_HKarQ@mail.gmail.comFunnily enough, we have an even older proposal from 2016,
/messages/by-id/709e06c0-59c9-ccec-d216-21e38cb5ed61@joh.toIt seems this is quite a popular missing feature, as Marko's patch was
also asked about in February 2019:
/messages/by-id/e58dd487-7ed4-3f95-c63c-24200ed768be@berkvens.net--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
Thanks, Ajay for reporting this.
The issue is that the INSERT...SET grammar was incorrectly using
set_clause_list which includes the UPDATE-style
multi-column assignment syntax (col1, col2) = expr. This creates
MultiAssignRef nodes that are only valid in UPDATE contexts, not INSERT.
Fix this by changing the grammer rule.
Please find attached v2 patch with the above fix. I have also added support
for different column sets in multi-row inserts.
Thanks, Andrew for the offline discussion and help on this.
--
Thanks & Regards,
Suraj kharage,
enterprisedb.com <https://www.enterprisedb.com/>
On Tue, Mar 31, 2026 at 11:44 AM Ajay Pal <ajay.pal.k@gmail.com> wrote:
Show quoted text
Hello all,
I am reporting a server crash encountered while testing the patch
provided by Suraj. The crash is consistently triggered by the query
attached below.postgres=# INSERT INTO emp_test SET (empno,ename)=(SELECT 1,'aa');
server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
The connection to the server was lost. Attempting reset: Failed.Thanks
AjayOn Mon, Mar 30, 2026 at 9:19 PM Álvaro Herrera <alvherre@kurilemu.de>
wrote:On 2026-Mar-30, Andrew Dunstan wrote:
Well, Suraj has kinda beaten me to it, but he didn't invent this
syntax.
Oracle did <
and I believe there is a proposal to add it to the standard. (Unlike
Suraj's, my WIP patch also supports the INSERT BY NAME variant.)Hmm, I don't see any WIP patch from you -- are you talking about this
patch from July 2019?/messages/by-id/CA+A-St+NntBh2EGu3a0xbVxJFzaeEOn=Vn_V84OuhM59_HKarQ@mail.gmail.com
Funnily enough, we have an even older proposal from 2016,
/messages/by-id/709e06c0-59c9-ccec-d216-21e38cb5ed61@joh.toIt seems this is quite a popular missing feature, as Marko's patch was
also asked about in February 2019:
/messages/by-id/e58dd487-7ed4-3f95-c63c-24200ed768be@berkvens.net--
Álvaro Herrera Breisgau, Deutschland —
Attachments:
v2-0001-Add-support-for-INSERT-.-SET-syntax.patchapplication/octet-stream; name=v2-0001-Add-support-for-INSERT-.-SET-syntax.patchDownload+779-15
Hi,
Rebased the patch with some documentation changes.
Also, added commitfest entry - https://commitfest.postgresql.org/patch/6635/
--
Thanks & Regards,
Suraj kharage,
enterprisedb.com <https://www.enterprisedb.com/>
From: Suraj Kharage <suraj.kharage@enterprisedb.com>
Date: Mon, Apr 6, 2026 at 4:13 PM
Subject: Re: [PATCH] Add support for INSERT ... SET syntax
To: Ajay Pal <ajay.pal.k@gmail.com>
Cc: Álvaro Herrera <alvherre@kurilemu.de>, Andrew Dunstan <
andrew@dunslane.net>, David G. Johnston <david.g.johnston@gmail.com>,
PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
Hi,
Rebased the patch with some documentation changes.
Also, added commitfest entry - https://commitfest.postgresql.org/patch/6635/
--
Thanks & Regards,
Suraj kharage,
enterprisedb.com <https://www.enterprisedb.com/>
Hi,
Sharing an update on testing.
Below are the areas that I’ve covered:
**Core syntax**
- Basic SET syntax (single row, with and without parentheses)
- Column order independence
- Multi-row insertion — SET (col=val, ...), (col=val, ...) syntax
**Value types**
- DEFAULT keyword and implicit defaults (column omission)
- Expressions, functions, and subqueries (including deeply nested)
- NULL values, array columns, composite types, and domain types
**Clauses & advanced features**
- RETURNING clause (single and multi-row)
- ON CONFLICT DO NOTHING / DO UPDATE
- OVERRIDING SYSTEM VALUE
- CTE (WITH clause)
- EXPLAIN / EXPLAIN ANALYZE
**Schema & constraints**
- CHECK constraints and column-level privileges
- Foreign key constraints (valid and failing cases)
- Partitioned tables, table inheritance, and schema-qualified table names
- Quoted and reserved keyword column names
**Procedural contexts**
- PL/pgSQL functions and DO blocks (including multi-row)
- Dynamic SQL via EXECUTE with parameters
- Prepared statements (PREPARE + EXECUTE)
- SAVEPOINTs and rollback behavior
**Other**
- Updatable views
- BEFORE and AFTER INSERT triggers
- Negative cases covering syntax errors, constraint violations, and
privilege violations
Note: Issue reported by Ajay Pal has been resolved in v3 patch.
--
Warm regards,
Triveni
Attachments:
v3-0001-Add-support-for-INSERT-.-SET-syntax.patchapplication/x-patch; name=v3-0001-Add-support-for-INSERT-.-SET-syntax.patchDownload+814-15
Import Notes
Reply to msg id not found: CAC6VRoZLEK0Ps=z21_q9zfp=Y+Ej6JR=KcScUyDbWu3m_wi7Hg@mail.gmail.com
Hi Suraj,
I tested v3 of the patch on current master.
The patch applied and built cleanly on my setup. Before applying it,
statements using the proposed INSERT...SET syntax resulted in a syntax
error near SET, which is expected since PostgreSQL does not currently
support this syntax. After applying the patch, the same statements
worked as expected and rows were inserted successfully.
I went through the functionality added by the patch and tested a few
different scenarios, including basic INSERT...SET usage, DEFAULT
values, expressions/functions, RETURNING, ON CONFLICT DO UPDATE,
multi-row inserts, different column sets in multi-row inserts, and
subqueries in assignments.
All of them behaved as expected in my testing.
I also verified the query that Ajay previously reported as causing a
backend crash:
INSERT INTO emp_test SET (empno,ename)=(SELECT 1,'aa');
With v3, I was not able to reproduce the crash. The query returned a
normal syntax error, and the server continued to run normally
afterward.
In addition, I ran the regression test suite and all tests passed successfully.
Overall, the patch worked well in my testing and I did not notice any
regressions.
Regards,
solai
On Wed, 1 Jul 2026 at 15:27, Suraj Kharage <suraj.kharage@enterprisedb.com>
wrote:
Hi,
I would like to propose adding support for an alternative INSERT syntax
that uses named column assignments via a SET clause. This provides a more
convenient and readable way to write inserts, particularly when only
specific columns need values.Currently, PostgreSQL requires INSERT statements to separate the column
list from the values:INSERT INTO users (name, email, status) VALUES ('Alice', '
alice@example.com', 'active');For inserts with many columns or where only a subset of columns are
specified, the proposed SET syntax offers better readability by keeping
column names adjacent to their values:INSERT INTO users SET name='Alice', email='alice@example.com',
status='active';Proposed Syntax:
INSERT INTO table_name
SET (column1=value1, column2=value2, ...), (, ...)
[ ON CONFLICT ... ]
[ RETURNING ... ];
This is some feedback for -v3. I had some problems resending the other
conversations in the thread from the archives[1]/messages/by-id/CAF1DzPXhKc15Toa06MTT7NYOAEq8Ucr=RYe5V7X2FkwGMmjD6w@mail.gmail.com, that's why I'm replying
to this, sorry.
I like the idea. In spite it's not (yet) part of the SQL standard, it
wouldn't be odd if we see that happening in the near future.
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 2a2e00b372e..11cb4fcd2da 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4370,6 +4370,8 @@ raw_expression_tree_walker_impl(Node *node,
return true;
if (WALK(stmt->selectStmt))
return true;
+ if (WALK(stmt->setClauseList))
+ return true;
if (WALK(stmt->onConflictClause))
you used `stmt->setClauseList` however, I read the entire
`raw_expression_tree_walker_impl`
function and it seems we don't mix "clause" with "List" in the variable
names. Reading the whole file, I just found "targetList" and "valuesList".
If you get my point, maybe you could use "setClause" only? I know that
sounds like something that exists in setter/getters stuff. Like we're
setting a clause up but would it be worth looking for a new variable name?
I personally think so. Actually, after reading
`src/include/nodes/parsenodes.h`,
I think we should go for a change.
----
Also, in src/backend/parser/analyze.c we can change a lot of those foreach
by foreach_node, however, I need to ask, did you have a reason to not use
foreach_node() when you first wrote the code? Maybe I'm missing something.
Because this patch is on a commitfest already, I didn't want to send a
patch we might need to squash if I'm right afterwards. That's why I'd like
to show you what I did:
https://github.com/postgres/postgres/commit/7be0538f2a5d916f2fb4a39764985b358cf6d379
If you like I could send a v4- with the squashed version.
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 70c75d0bb20..d2f5b0edcc8 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -679,31 +679,23 @@ transformInsertSetClause(ParseState *pstate, List
*setClauseList,
{
List *all_cols = NIL; /* List of all unique
column names */
List *valuesLists = NIL;
- ListCell *outer_lc;
- ListCell *lc;
/*
* First pass: collect all unique column names from all rows.
* We need to scan all rows first to determine the complete set of
columns.
* Also check for duplicate columns within each row.
*/
- foreach(outer_lc, setClauseList)
+ foreach_node(List, set_clause, setClauseList)
{
- List *set_clause = (List *) lfirst(outer_lc);
List *row_cols = NIL; /* Columns seen in
this row */
- ListCell *set_lc;
[...]
(more of that in the link to the diff in GH)
That's all for now.
Thanks!
[1]: /messages/by-id/CAF1DzPXhKc15Toa06MTT7NYOAEq8Ucr=RYe5V7X2FkwGMmjD6w@mail.gmail.com
/messages/by-id/CAF1DzPXhKc15Toa06MTT7NYOAEq8Ucr=RYe5V7X2FkwGMmjD6w@mail.gmail.com
Below INSERT features are supported:
- DEFAULT keyword: SET col=DEFAULT
- Expressions and functions: SET col=expr, col2=function(...)
- Subqueries: SET col=(SELECT ...)
- RETURNING clause
- ON CONFLICT DO UPDATE/NOTHING
- OVERRIDING SYSTEM VALUE
- Multi-row syntax: SET (col1=val1, col2=val2), (col1=val3, col2=val4)Columns not mentioned receive their default values or NULL, consistent
with standard INSERT behavior.I've attached the patch. Looking forward to your feedback.
--Thanks & Regards,
Suraj kharage,
--
Mario Gonzalez
EDB: https://www.enterprisedb.com
Thanks Mario for the review.
On Mon, Jul 13, 2026 at 12:18 AM Mario González Troncoso <
gonzalemario@gmail.com> wrote:
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 2a2e00b372e..11cb4fcd2da 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4370,6 +4370,8 @@ raw_expression_tree_walker_impl(Node *node, return true; if (WALK(stmt->selectStmt)) return true; + if (WALK(stmt->setClauseList)) + return true; if (WALK(stmt->onConflictClause))you used `stmt->setClauseList` however, I read the entire `raw_expression_tree_walker_impl`
function and it seems we don't mix "clause" with "List" in the variable
names. Reading the whole file, I just found "targetList" and "valuesList".If you get my point, maybe you could use "setClause" only? I know that
sounds like something that exists in setter/getters stuff. Like we're
setting a clause up but would it be worth looking for a new variable name?
I personally think so. Actually, after reading `src/include/nodes/parsenodes.h`,
I think we should go for a change.
Renamed setClauseList as per your suggestion.
----
Also, in src/backend/parser/analyze.c we can change a lot of those foreach
by foreach_node, however, I need to ask, did you have a reason to not use
foreach_node() when you first wrote the code? Maybe I'm missing something.
Because this patch is on a commitfest already, I didn't want to send a
patch we might need to squash if I'm right afterwards. That's why I'd like
to show you what I did:
https://github.com/postgres/postgres/commit/7be0538f2a5d916f2fb4a39764985b358cf6d379
If you like I could send a v4- with the squashed version.diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 70c75d0bb20..d2f5b0edcc8 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -679,31 +679,23 @@ transformInsertSetClause(ParseState *pstate, List *setClauseList, { List *all_cols = NIL; /* List of all unique column names */ List *valuesLists = NIL; - ListCell *outer_lc; - ListCell *lc;/* * First pass: collect all unique column names from all rows. * We need to scan all rows first to determine the complete set of columns. * Also check for duplicate columns within each row. */ - foreach(outer_lc, setClauseList) + foreach_node(List, set_clause, setClauseList) { - List *set_clause = (List *) lfirst(outer_lc); List *row_cols = NIL; /* Columns seen in this row */ - ListCell *set_lc; [...]
Used foreach_node as per your suggestion.
I have addressed your review comments in the attached v4 patch.
Thanks & Regards,
Suraj kharage,
enterprisedb.com <https://www.enterprisedb.com/>
Attachments:
t139169_13v4-0001-Add-support-for-INSERT-.-SET-syntax.patchapplication/octet-stream; name=v4-0001-Add-support-for-INSERT-.-SET-syntax.patchDownload+799-15
On Tue, 14 Jul 2026 at 00:39, Suraj Kharage <suraj.kharage@enterprisedb.com>
wrote:
Thanks Mario for the review.
On Mon, Jul 13, 2026 at 12:18 AM Mario González Troncoso <
gonzalemario@gmail.com> wrote:diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs. c index 2a2e00b372e..11cb4fcd2da 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4370,6 +4370,8 @@ raw_expression_tree_walker_impl(Node *node, return true; if (WALK(stmt->selectStmt)) return true; + if (WALK(stmt->setClauseList)) + return true; if (WALK(stmt->onConflictClause))you used `stmt->setClauseList` however, I read the entire `raw_expression_tree_walker_impl`
function and it seems we don't mix "clause" with "List" in the variable
names. Reading the whole file, I just found "targetList" and "valuesList".If you get my point, maybe you could use "setClause" only? I know that
sounds like something that exists in setter/getters stuff. Like we're
setting a clause up but would it be worth looking for a new variable name?
I personally think so. Actually, after reading `src/include/nodes/parsenodes.h`,
I think we should go for a change.Renamed setClauseList as per your suggestion.
----
Also, in src/backend/parser/analyze.c we can change a lot of those
foreach by foreach_node, however, I need to ask, did you have a reason to
not use foreach_node() when you first wrote the code? Maybe I'm missing
something. Because this patch is on a commitfest already, I didn't want to
send a patch we might need to squash if I'm right afterwards. That's why
I'd like to show you what I did:
https://github.com/postgres/postgres/commit/7be0538f2a5d916f2fb4a39764985b358cf6d379
If you like I could send a v4- with the squashed version.diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 70c75d0bb20..d2f5b0edcc8 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -679,31 +679,23 @@ transformInsertSetClause(ParseState *pstate, List *setClauseList, { List *all_cols = NIL; /* List of all unique column names */ List *valuesLists = NIL; - ListCell *outer_lc; - ListCell *lc;/* * First pass: collect all unique column names from all rows. * We need to scan all rows first to determine the complete set of columns. * Also check for duplicate columns within each row. */ - foreach(outer_lc, setClauseList) + foreach_node(List, set_clause, setClauseList) { - List *set_clause = (List *) lfirst(outer_lc); List *row_cols = NIL; /* Columns seen in this row */ - ListCell *set_lc; [...]Used foreach_node as per your suggestion.
I have addressed your review comments in the attached v4 patch.
lgtm Suraj. I hope you can find a committer that buys you with this idea
--
Mario Gonzalez
Hi Suraj,
I have a few observations regarding the latest v4 patch:
1. Assigning two different subfields or elements of the same column in a
single row is rejected,
even though the equivalent column-list INSERT syntax accepts it:
postgres=# create type comp_t as (x int, y int);
CREATE TYPE
postgres=# create table t2 (id int primary key, c comp_t);
CREATE TABLE
postgres=# insert into t2 (id, c.x, c.y) values (1, 5, 6);
INSERT 0 1
postgres=# insert into t2 set id=2, c.x=7, c.y=8;
ERROR: column "c" specified more than once
LINE 1: insert into t2 set id=2, c.x=7, c.y=8;
^
The same failure occurs with an array column, without requiring a custom
type:
postgres=# create table t3 (id int primary key, arr int[]);
CREATE TABLE
postgres=# insert into t3 (id, arr[1], arr[2]) values (1, 10, 20);
INSERT 0 1
postgres=# insert into t3 set id=2, arr[1]=30, arr[2]=40;
ERROR: column "arr" specified more than once
LINE 1: insert into t3 set id=2, arr[1]=30, arr[2]=40;
^
2. There is a silent misassignment across rows in multi-row SET syntax:
postgres=# create table t7 (id int primary key, arr int[]);
CREATE TABLE
postgres=# insert into t7 set (id=1, arr[1]=111), (id=2, arr[2]=222);
INSERT 0 2
postgres=# select * from t7;
id | arr
----+-------
1 | {111}
2 | {222}
(2 rows)
Although row 2 explicitly specifies arr[2]=222, the code only tracks
columns by name and
not by the specific element or field targeted. It retains the tracking from
row 1 ("arr → index [1]")
and applies it to subsequent rows. As a result, the value for row 2
silently lands in arr[1] instead
of arr[2], leaving arr[2] as NULL without throwing an error or warning.
This differs from the standard VALUES limitation (e.g., INSERT INTO t7 (id,
arr[1]) VALUES (1,111),(2,222)),
where applying arr[1] to both rows is expected because it is defined once
in the shared header.
In this multi-row SET case, the explicit per-row target is ignored and
silently corrupted rather than being rejected as unsupported.
Regards,
Vaibhav
On Tue, Aug 25, 2026 at 8:40 PM Mario González <gonzalemario@gmail.com>
wrote:
Show quoted text
On Tue, 14 Jul 2026 at 00:39, Suraj Kharage <
suraj.kharage@enterprisedb.com> wrote:Thanks Mario for the review.
On Mon, Jul 13, 2026 at 12:18 AM Mario González Troncoso <
gonzalemario@gmail.com> wrote:diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 2a2e00b372e..11cb4fcd2da 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4370,6 +4370,8 @@ raw_expression_tree_walker_impl(Node *node, return true; if (WALK(stmt->selectStmt)) return true; + if (WALK(stmt->setClauseList)) + return true; if (WALK(stmt->onConflictClause))you used `stmt->setClauseList` however, I read the entire `raw_expression_tree_walker_impl`
function and it seems we don't mix "clause" with "List" in the variable
names. Reading the whole file, I just found "targetList" and "valuesList".If you get my point, maybe you could use "setClause" only? I know that
sounds like something that exists in setter/getters stuff. Like we're
setting a clause up but would it be worth looking for a new variable name?
I personally think so. Actually, after reading `src/include/nodes/parsenodes.h`,
I think we should go for a change.Renamed setClauseList as per your suggestion.
----
Also, in src/backend/parser/analyze.c we can change a lot of those
foreach by foreach_node, however, I need to ask, did you have a reason to
not use foreach_node() when you first wrote the code? Maybe I'm missing
something. Because this patch is on a commitfest already, I didn't want to
send a patch we might need to squash if I'm right afterwards. That's why
I'd like to show you what I did:
https://github.com/postgres/postgres/commit/7be0538f2a5d916f2fb4a39764985b358cf6d379
If you like I could send a v4- with the squashed version.diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 70c75d0bb20..d2f5b0edcc8 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -679,31 +679,23 @@ transformInsertSetClause(ParseState *pstate, List *setClauseList, { List *all_cols = NIL; /* List of all unique column names */ List *valuesLists = NIL; - ListCell *outer_lc; - ListCell *lc;/* * First pass: collect all unique column names from all rows. * We need to scan all rows first to determine the complete set of columns. * Also check for duplicate columns within each row. */ - foreach(outer_lc, setClauseList) + foreach_node(List, set_clause, setClauseList) { - List *set_clause = (List *) lfirst(outer_lc); List *row_cols = NIL; /* Columns seen in this row */ - ListCell *set_lc; [...]Used foreach_node as per your suggestion.
I have addressed your review comments in the attached v4 patch.
lgtm Suraj. I hope you can find a committer that buys you with this idea
--
Mario Gonzalez
Thank you Vaibhav for the review.
I have fixed these issues in the attached v4 patch. Please have a look.
--
Thanks & Regards,
Suraj kharage,
enterprisedb.com <https://www.enterprisedb.com/>
On Wed, Aug 26, 2026 at 6:32 PM Vaibhav Dalvi <
vaibhav.dalvi@enterprisedb.com> wrote:
Show quoted text
Hi Suraj,
I have a few observations regarding the latest v4 patch:
1. Assigning two different subfields or elements of the same column in a
single row is rejected,
even though the equivalent column-list INSERT syntax accepts it:postgres=# create type comp_t as (x int, y int);
CREATE TYPE
postgres=# create table t2 (id int primary key, c comp_t);
CREATE TABLE
postgres=# insert into t2 (id, c.x, c.y) values (1, 5, 6);
INSERT 0 1
postgres=# insert into t2 set id=2, c.x=7, c.y=8;
ERROR: column "c" specified more than once
LINE 1: insert into t2 set id=2, c.x=7, c.y=8;
^The same failure occurs with an array column, without requiring a custom
type:postgres=# create table t3 (id int primary key, arr int[]);
CREATE TABLE
postgres=# insert into t3 (id, arr[1], arr[2]) values (1, 10, 20);
INSERT 0 1
postgres=# insert into t3 set id=2, arr[1]=30, arr[2]=40;
ERROR: column "arr" specified more than once
LINE 1: insert into t3 set id=2, arr[1]=30, arr[2]=40;
^2. There is a silent misassignment across rows in multi-row SET syntax:
postgres=# create table t7 (id int primary key, arr int[]);
CREATE TABLE
postgres=# insert into t7 set (id=1, arr[1]=111), (id=2, arr[2]=222);
INSERT 0 2
postgres=# select * from t7;
id | arr
----+-------
1 | {111}
2 | {222}
(2 rows)Although row 2 explicitly specifies arr[2]=222, the code only tracks
columns by name and
not by the specific element or field targeted. It retains the tracking
from row 1 ("arr → index [1]")
and applies it to subsequent rows. As a result, the value for row 2
silently lands in arr[1] instead
of arr[2], leaving arr[2] as NULL without throwing an error or warning.This differs from the standard VALUES limitation (e.g., INSERT INTO t7
(id, arr[1]) VALUES (1,111),(2,222)),
where applying arr[1] to both rows is expected because it is defined once
in the shared header.
In this multi-row SET case, the explicit per-row target is ignored and
silently corrupted rather than being rejected as unsupported.Regards,
VaibhavOn Tue, Aug 25, 2026 at 8:40 PM Mario González <gonzalemario@gmail.com>
wrote:On Tue, 14 Jul 2026 at 00:39, Suraj Kharage <
suraj.kharage@enterprisedb.com> wrote:Thanks Mario for the review.
On Mon, Jul 13, 2026 at 12:18 AM Mario González Troncoso <
gonzalemario@gmail.com> wrote:diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 2a2e00b372e..11cb4fcd2da 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4370,6 +4370,8 @@ raw_expression_tree_walker_impl(Node *node, return true; if (WALK(stmt->selectStmt)) return true; + if (WALK(stmt->setClauseList)) + return true; if (WALK(stmt->onConflictClause))you used `stmt->setClauseList` however, I read the entire `raw_expression_tree_walker_impl`
function and it seems we don't mix "clause" with "List" in the variable
names. Reading the whole file, I just found "targetList" and "valuesList".If you get my point, maybe you could use "setClause" only? I know that
sounds like something that exists in setter/getters stuff. Like we're
setting a clause up but would it be worth looking for a new variable name?
I personally think so. Actually, after reading `src/include/nodes/parsenodes.h`,
I think we should go for a change.Renamed setClauseList as per your suggestion.
----
Also, in src/backend/parser/analyze.c we can change a lot of those
foreach by foreach_node, however, I need to ask, did you have a reason to
not use foreach_node() when you first wrote the code? Maybe I'm missing
something. Because this patch is on a commitfest already, I didn't want to
send a patch we might need to squash if I'm right afterwards. That's why
I'd like to show you what I did:
https://github.com/postgres/postgres/commit/7be0538f2a5d916f2fb4a39764985b358cf6d379
If you like I could send a v4- with the squashed version.diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 70c75d0bb20..d2f5b0edcc8 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -679,31 +679,23 @@ transformInsertSetClause(ParseState *pstate, List *setClauseList, { List *all_cols = NIL; /* List of all unique column names */ List *valuesLists = NIL; - ListCell *outer_lc; - ListCell *lc;/* * First pass: collect all unique column names from all rows. * We need to scan all rows first to determine the complete set of columns. * Also check for duplicate columns within each row. */ - foreach(outer_lc, setClauseList) + foreach_node(List, set_clause, setClauseList) { - List *set_clause = (List *) lfirst(outer_lc); List *row_cols = NIL; /* Columns seen in this row */ - ListCell *set_lc; [...]Used foreach_node as per your suggestion.
I have addressed your review comments in the attached v4 patch.
lgtm Suraj. I hope you can find a committer that buys you with this idea
--
Mario Gonzalez
Attachments:
t139169_16v5-0001-Add-support-for-INSERT-.-SET-syntax.patchapplication/octet-stream; name=v5-0001-Add-support-for-INSERT-.-SET-syntax.patchDownload+930-16
Hi Suraj,
Thanks for working on this.
I think the following bug also needs to be fixed.
1. Example:
create table ta(id int primary key, arr int[] default array[1]);
-- classic VALUES: row 3 gets the table's default {1}
insert into ta values (70, array[1]), (71, array[2]), (72, default);
-- id | arr
-- ----+-----
-- 70 | {1}
-- 71 | {2}
-- 72 | {1}
-- new SET syntax: same intent, row 3 just omits arr
insert into ta set (id=170, arr[1]=1), (id=171, arr[1]=2), (id=172);
-- ERROR: cannot set an array element to DEFAULT
*Classic VALUES: *if a row skips a column (or writes DEFAULT), Postgres
asks, "What's the default for the whole column arr?" —
table says {1}, done.
*New INSERT ... SET:* if a row skips arr, but other rows in the same
statement use arr[1]=..., Postgres instead asks "what's
the default for just slot 1 of arr?" — and that question has always been
illegal in Postgres (you can't default one array
slot). So it errors, even though the row never asked for a slot at all — it
just wanted the whole column left alone.
*Why is this a bug?: *row 172 never used array-indirection syntax at all.
it should get the plain, legal,
whole-column default ({1}), exactly like classic VALUES gives it. Instead,
the code wrongly forces it through the "set array
slot 1 to default" path just because other rows in the same statement
happened to use arr[1]. That path is always illegal
in Postgres, so a perfectly valid statement fails with an error the user's
SQL never requested.
Regards,
Vaibhav
On Thu, Aug 27, 2026 at 4:25 PM Suraj Kharage <
suraj.kharage@enterprisedb.com> wrote:
Show quoted text
Thank you Vaibhav for the review.
I have fixed these issues in the attached v4 patch. Please have a look.
--Thanks & Regards,
Suraj kharage,enterprisedb.com <https://www.enterprisedb.com/>
On Wed, Aug 26, 2026 at 6:32 PM Vaibhav Dalvi <
vaibhav.dalvi@enterprisedb.com> wrote:Hi Suraj,
I have a few observations regarding the latest v4 patch:
1. Assigning two different subfields or elements of the same column in a
single row is rejected,
even though the equivalent column-list INSERT syntax accepts it:postgres=# create type comp_t as (x int, y int);
CREATE TYPE
postgres=# create table t2 (id int primary key, c comp_t);
CREATE TABLE
postgres=# insert into t2 (id, c.x, c.y) values (1, 5, 6);
INSERT 0 1
postgres=# insert into t2 set id=2, c.x=7, c.y=8;
ERROR: column "c" specified more than once
LINE 1: insert into t2 set id=2, c.x=7, c.y=8;
^The same failure occurs with an array column, without requiring a custom
type:postgres=# create table t3 (id int primary key, arr int[]);
CREATE TABLE
postgres=# insert into t3 (id, arr[1], arr[2]) values (1, 10, 20);
INSERT 0 1
postgres=# insert into t3 set id=2, arr[1]=30, arr[2]=40;
ERROR: column "arr" specified more than once
LINE 1: insert into t3 set id=2, arr[1]=30, arr[2]=40;
^2. There is a silent misassignment across rows in multi-row SET syntax:
postgres=# create table t7 (id int primary key, arr int[]);
CREATE TABLE
postgres=# insert into t7 set (id=1, arr[1]=111), (id=2, arr[2]=222);
INSERT 0 2
postgres=# select * from t7;
id | arr
----+-------
1 | {111}
2 | {222}
(2 rows)Although row 2 explicitly specifies arr[2]=222, the code only tracks
columns by name and
not by the specific element or field targeted. It retains the tracking
from row 1 ("arr → index [1]")
and applies it to subsequent rows. As a result, the value for row 2
silently lands in arr[1] instead
of arr[2], leaving arr[2] as NULL without throwing an error or warning.This differs from the standard VALUES limitation (e.g., INSERT INTO t7
(id, arr[1]) VALUES (1,111),(2,222)),
where applying arr[1] to both rows is expected because it is defined once
in the shared header.
In this multi-row SET case, the explicit per-row target is ignored and
silently corrupted rather than being rejected as unsupported.Regards,
VaibhavOn Tue, Aug 25, 2026 at 8:40 PM Mario González <gonzalemario@gmail.com>
wrote:On Tue, 14 Jul 2026 at 00:39, Suraj Kharage <
suraj.kharage@enterprisedb.com> wrote:Thanks Mario for the review.
On Mon, Jul 13, 2026 at 12:18 AM Mario González Troncoso <
gonzalemario@gmail.com> wrote:diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 2a2e00b372e..11cb4fcd2da 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4370,6 +4370,8 @@ raw_expression_tree_walker_impl(Node *node, return true; if (WALK(stmt->selectStmt)) return true; + if (WALK(stmt->setClauseList)) + return true; if (WALK(stmt->onConflictClause))you used `stmt->setClauseList` however, I read the entire `raw_expression_tree_walker_impl`
function and it seems we don't mix "clause" with "List" in the variable
names. Reading the whole file, I just found "targetList" and "valuesList".If you get my point, maybe you could use "setClause" only? I know that
sounds like something that exists in setter/getters stuff. Like we're
setting a clause up but would it be worth looking for a new variable name?
I personally think so. Actually, after reading `src/include/nodes/parsenodes.h`,
I think we should go for a change.Renamed setClauseList as per your suggestion.
----
Also, in src/backend/parser/analyze.c we can change a lot of those
foreach by foreach_node, however, I need to ask, did you have a reason to
not use foreach_node() when you first wrote the code? Maybe I'm missing
something. Because this patch is on a commitfest already, I didn't want to
send a patch we might need to squash if I'm right afterwards. That's why
I'd like to show you what I did:
https://github.com/postgres/postgres/commit/7be0538f2a5d916f2fb4a39764985b358cf6d379
If you like I could send a v4- with the squashed version.diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 70c75d0bb20..d2f5b0edcc8 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -679,31 +679,23 @@ transformInsertSetClause(ParseState *pstate, List *setClauseList, { List *all_cols = NIL; /* List of all unique column names */ List *valuesLists = NIL; - ListCell *outer_lc; - ListCell *lc;/* * First pass: collect all unique column names from all rows. * We need to scan all rows first to determine the complete set of columns. * Also check for duplicate columns within each row. */ - foreach(outer_lc, setClauseList) + foreach_node(List, set_clause, setClauseList) { - List *set_clause = (List *) lfirst(outer_lc); List *row_cols = NIL; /* Columns seen in this row */ - ListCell *set_lc; [...]Used foreach_node as per your suggestion.
I have addressed your review comments in the attached v4 patch.
lgtm Suraj. I hope you can find a committer that buys you with this idea
--
Mario Gonzalez
On Fri, Aug 28, 2026 at 11:39 AM Vaibhav Dalvi <
vaibhav.dalvi@enterprisedb.com> wrote:
create table ta(id int primary key, arr int[] default array[1]);
-- classic VALUES: row 3 gets the table's default {1}
insert into ta values (70, array[1]), (71, array[2]), (72, default);
-- id | arr
-- ----+-----
-- 70 | {1}
-- 71 | {2}
-- 72 | {1}-- new SET syntax: same intent, row 3 just omits arr
insert into ta set (id=170, arr[1]=1), (id=171, arr[1]=2), (id=172);
-- ERROR: cannot set an array element to DEFAULT
Thanks for reporting this. I have looked into this and feel that this is an
existing behavior for INSERT command.
transformAssignedExpr() unconditionally rejects DEFAULT for any indirection
target (array element or subfield).
For e.g. for plain insert as well it will fail -
postgres@2495950=#INSERT INTO ta(id, arr[1]) VALUES (70,1),(71,2),(72,
default);
ERROR: cannot set an array element to DEFAULT
LINE 1: INSERT INTO ta(id, arr[1]) VALUES (70,1),(71,2),(72, default...
It will work in both cases, if we try to insert in arr column.
postgres@2495950=#INSERT INTO ta VALUES (70,array[1]),(71,array[2]),(72,
default);
INSERT 0 3
postgres@2495950=#select * from ta;
id | arr
----+-----
70 | {1}
71 | {2}
72 | {1}
(3 rows)
postgres@2495950=#insert into ta set (id=170, arr=array[1]), (id=171,
arr=array[2]), (id=172, arr=default);
INSERT 0 3
postgres@2495950=#select * from ta;
id | arr
-----+-----
170 | {1}
171 | {2}
172 | {1}
(3 rows)
I hope this helps.
Regards,
Suraj
Show quoted text
*Classic VALUES: *if a row skips a column (or writes DEFAULT), Postgres
asks, "What's the default for the whole column arr?" —
table says {1}, done.*New INSERT ... SET:* if a row skips arr, but other rows in the same
statement use arr[1]=..., Postgres instead asks "what's
the default for just slot 1 of arr?" — and that question has always been
illegal in Postgres (you can't default one array
slot). So it errors, even though the row never asked for a slot at all —
it just wanted the whole column left alone.*Why is this a bug?: *row 172 never used array-indirection syntax at all.
it should get the plain, legal,
whole-column default ({1}), exactly like classic VALUES gives it. Instead,
the code wrongly forces it through the "set array
slot 1 to default" path just because other rows in the same statement
happened to use arr[1]. That path is always illegal
in Postgres, so a perfectly valid statement fails with an error the user's
SQL never requested.Regards,
VaibhavOn Thu, Aug 27, 2026 at 4:25 PM Suraj Kharage <
suraj.kharage@enterprisedb.com> wrote:Thank you Vaibhav for the review.
I have fixed these issues in the attached v4 patch. Please have a look.
--Thanks & Regards,
Suraj kharage,enterprisedb.com <https://www.enterprisedb.com/>
On Wed, Aug 26, 2026 at 6:32 PM Vaibhav Dalvi <
vaibhav.dalvi@enterprisedb.com> wrote:Hi Suraj,
I have a few observations regarding the latest v4 patch:
1. Assigning two different subfields or elements of the same column in
a single row is rejected,
even though the equivalent column-list INSERT syntax accepts it:postgres=# create type comp_t as (x int, y int);
CREATE TYPE
postgres=# create table t2 (id int primary key, c comp_t);
CREATE TABLE
postgres=# insert into t2 (id, c.x, c.y) values (1, 5, 6);
INSERT 0 1
postgres=# insert into t2 set id=2, c.x=7, c.y=8;
ERROR: column "c" specified more than once
LINE 1: insert into t2 set id=2, c.x=7, c.y=8;
^The same failure occurs with an array column, without requiring a custom
type:postgres=# create table t3 (id int primary key, arr int[]);
CREATE TABLE
postgres=# insert into t3 (id, arr[1], arr[2]) values (1, 10, 20);
INSERT 0 1
postgres=# insert into t3 set id=2, arr[1]=30, arr[2]=40;
ERROR: column "arr" specified more than once
LINE 1: insert into t3 set id=2, arr[1]=30, arr[2]=40;
^2. There is a silent misassignment across rows in multi-row SET syntax:
postgres=# create table t7 (id int primary key, arr int[]);
CREATE TABLE
postgres=# insert into t7 set (id=1, arr[1]=111), (id=2, arr[2]=222);
INSERT 0 2
postgres=# select * from t7;
id | arr
----+-------
1 | {111}
2 | {222}
(2 rows)Although row 2 explicitly specifies arr[2]=222, the code only tracks
columns by name and
not by the specific element or field targeted. It retains the tracking
from row 1 ("arr → index [1]")
and applies it to subsequent rows. As a result, the value for row 2
silently lands in arr[1] instead
of arr[2], leaving arr[2] as NULL without throwing an error or warning.This differs from the standard VALUES limitation (e.g., INSERT INTO t7
(id, arr[1]) VALUES (1,111),(2,222)),
where applying arr[1] to both rows is expected because it is defined
once in the shared header.
In this multi-row SET case, the explicit per-row target is ignored and
silently corrupted rather than being rejected as unsupported.Regards,
VaibhavOn Tue, Aug 25, 2026 at 8:40 PM Mario González <gonzalemario@gmail.com>
wrote:On Tue, 14 Jul 2026 at 00:39, Suraj Kharage <
suraj.kharage@enterprisedb.com> wrote:Thanks Mario for the review.
On Mon, Jul 13, 2026 at 12:18 AM Mario González Troncoso <
gonzalemario@gmail.com> wrote:diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 2a2e00b372e..11cb4fcd2da 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4370,6 +4370,8 @@ raw_expression_tree_walker_impl(Node *node, return true; if (WALK(stmt->selectStmt)) return true; + if (WALK(stmt->setClauseList)) + return true; if (WALK(stmt->onConflictClause))you used `stmt->setClauseList` however, I read the entire `raw_expression_tree_walker_impl`
function and it seems we don't mix "clause" with "List" in the variable
names. Reading the whole file, I just found "targetList" and "valuesList".If you get my point, maybe you could use "setClause" only? I know
that sounds like something that exists in setter/getters stuff. Like we're
setting a clause up but would it be worth looking for a new variable name?
I personally think so. Actually, after reading `src/include/nodes/parsenodes.h`,
I think we should go for a change.Renamed setClauseList as per your suggestion.
----
Also, in src/backend/parser/analyze.c we can change a lot of those
foreach by foreach_node, however, I need to ask, did you have a reason to
not use foreach_node() when you first wrote the code? Maybe I'm missing
something. Because this patch is on a commitfest already, I didn't want to
send a patch we might need to squash if I'm right afterwards. That's why
I'd like to show you what I did:
https://github.com/postgres/postgres/commit/7be0538f2a5d916f2fb4a39764985b358cf6d379
If you like I could send a v4- with the squashed version.diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 70c75d0bb20..d2f5b0edcc8 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -679,31 +679,23 @@ transformInsertSetClause(ParseState *pstate, List *setClauseList, { List *all_cols = NIL; /* List of all unique column names */ List *valuesLists = NIL; - ListCell *outer_lc; - ListCell *lc;/* * First pass: collect all unique column names from all rows. * We need to scan all rows first to determine the complete set of columns. * Also check for duplicate columns within each row. */ - foreach(outer_lc, setClauseList) + foreach_node(List, set_clause, setClauseList) { - List *set_clause = (List *) lfirst(outer_lc); List *row_cols = NIL; /* Columns seen in this row */ - ListCell *set_lc; [...]Used foreach_node as per your suggestion.
I have addressed your review comments in the attached v4 patch.
lgtm Suraj. I hope you can find a committer that buys you with this
idea--
Mario Gonzalez
Hi,
Thanks Suraj for the clarification. I see two issues here, both stemming
from the same root cause:
*Issue #1: A row that skips an array column entirely gets wrongly rejected*
Setup:
create table ta(id int primary key, arr int[] default array[1]);
Command:
insert into ta set (id=70, arr[1]=1), (id=71, arr[1]=2), (id=72);
Actual result:
ERROR: cannot set an array element to DEFAULT
What's wrong: row id=72 doesn't mention arr in any way, no slot, no whole
column, nothing.
In plain English, it's saying "I don't care about arr, just use whatever it
normally is."
That should give it the column's default ({1} here, since one was declared,
or NULL if not)
exactly like skipping any other ordinary column works fine. Instead,
because other rows in the
same statement touch arr[1], the code silently treats row 72's silence as
if it had explicitly written
arr[1] = DEFAULT, and setting a single array slot to DEFAULT is always that
asked for nothing
gets blamed for a request it never made, and the whole statement fails.
Expected result:
id | arr
----+-----
70 | {1}
71 | {2}
72 | {1} -- the column's default, same as ny other statement
*Issue #2: Mixing "whole column" and "one slot" across different rows gets
wrongly rejected*
Setup:
create table t1(id int primary key, arr int[] default array[1]);
Command:
insert into t1 set (id=1, arr[1]=1), (id=2, arr=default);
Actual result:
ERROR: column "arr" specified more than once
What's wrong: Row 1 wants to set slot 1 of its own array. Row 2 wants its
own array to just be the default.
These are two completely separate rows with two completely separate array
values, nothing about them
actually conflicts. However, the code doesn't check rows independently; it
first lumps every row's arr-related
target into one shared bucket, and only then checks for conflicts. In that
shared bucket, "the whole column"
and "just slot 1" look like a contradiction, even though they aren't, since
they belong to two different rows, not
one row saying two contradictory things about the same value.
Expected result:
id | arr
----+-----
1 | {1}
2 | {1} -- the column's default
Another thought here is: should we restrict array/field indirection targets
instead like below?
insert into t1 set (id=1, arr[1]=1), (id=2, arr=default);
ERROR: array/field indirection targets are not supported in multi-row
INSERT ... SET
Per my knowledge, MySQL and MariaDB support this INSERT ... SET syntax:
- Standard MySQL does not support multi-row INSERT ... SET directly using SET
... SET.
In MySQL, INSERT ... SET is strictly single-row:
INSERT INTO employees SET id = 1, name = 'Alice', salary = 70000;
- MariaDB extends the standard INSERT ... SET syntax to support multiple
rows by repeating the SET clause for each row:
INSERT INTO employees
SET id = 1, name = 'Alice', salary = 70000,
SET id = 2, name = 'Bob', salary = 80000,
SET id = 3, name = 'Charlie', salary = 75000;
The point is to check if we can think in that direction?
Regards,
Vaibhav
On Mon, Aug 31, 2026 at 10:20 AM Suraj Kharage <
suraj.kharage@enterprisedb.com> wrote:
Show quoted text
On Fri, Aug 28, 2026 at 11:39 AM Vaibhav Dalvi <
vaibhav.dalvi@enterprisedb.com> wrote:create table ta(id int primary key, arr int[] default array[1]);
-- classic VALUES: row 3 gets the table's default {1}
insert into ta values (70, array[1]), (71, array[2]), (72, default);
-- id | arr
-- ----+-----
-- 70 | {1}
-- 71 | {2}
-- 72 | {1}-- new SET syntax: same intent, row 3 just omits arr
insert into ta set (id=170, arr[1]=1), (id=171, arr[1]=2), (id=172);
-- ERROR: cannot set an array element to DEFAULTThanks for reporting this. I have looked into this and feel that this is
an existing behavior for INSERT command.
transformAssignedExpr() unconditionally rejects DEFAULT for any
indirection target (array element or subfield).For e.g. for plain insert as well it will fail -
postgres@2495950=#INSERT INTO ta(id, arr[1]) VALUES (70,1),(71,2),(72,
default);
ERROR: cannot set an array element to DEFAULT
LINE 1: INSERT INTO ta(id, arr[1]) VALUES (70,1),(71,2),(72, default...It will work in both cases, if we try to insert in arr column.
postgres@2495950=#INSERT INTO ta VALUES (70,array[1]),(71,array[2]),(72,
default);
INSERT 0 3
postgres@2495950=#select * from ta;
id | arr
----+-----
70 | {1}
71 | {2}
72 | {1}
(3 rows)postgres@2495950=#insert into ta set (id=170, arr=array[1]), (id=171,
arr=array[2]), (id=172, arr=default);
INSERT 0 3
postgres@2495950=#select * from ta;
id | arr
-----+-----
170 | {1}
171 | {2}
172 | {1}
(3 rows)I hope this helps.
Regards,
Suraj*Classic VALUES: *if a row skips a column (or writes DEFAULT), Postgres
asks, "What's the default for the whole column arr?" —
table says {1}, done.*New INSERT ... SET:* if a row skips arr, but other rows in the same
statement use arr[1]=..., Postgres instead asks "what's
the default for just slot 1 of arr?" — and that question has always been
illegal in Postgres (you can't default one array
slot). So it errors, even though the row never asked for a slot at all —
it just wanted the whole column left alone.*Why is this a bug?: *row 172 never used array-indirection syntax at
all. it should get the plain, legal,
whole-column default ({1}), exactly like classic VALUES gives it.
Instead, the code wrongly forces it through the "set array
slot 1 to default" path just because other rows in the same statement
happened to use arr[1]. That path is always illegal
in Postgres, so a perfectly valid statement fails with an error the
user's SQL never requested.Regards,
VaibhavOn Thu, Aug 27, 2026 at 4:25 PM Suraj Kharage <
suraj.kharage@enterprisedb.com> wrote:Thank you Vaibhav for the review.
I have fixed these issues in the attached v4 patch. Please have a look.
--Thanks & Regards,
Suraj kharage,enterprisedb.com <https://www.enterprisedb.com/>
On Wed, Aug 26, 2026 at 6:32 PM Vaibhav Dalvi <
vaibhav.dalvi@enterprisedb.com> wrote:Hi Suraj,
I have a few observations regarding the latest v4 patch:
1. Assigning two different subfields or elements of the same column in
a single row is rejected,
even though the equivalent column-list INSERT syntax accepts it:postgres=# create type comp_t as (x int, y int);
CREATE TYPE
postgres=# create table t2 (id int primary key, c comp_t);
CREATE TABLE
postgres=# insert into t2 (id, c.x, c.y) values (1, 5, 6);
INSERT 0 1
postgres=# insert into t2 set id=2, c.x=7, c.y=8;
ERROR: column "c" specified more than once
LINE 1: insert into t2 set id=2, c.x=7, c.y=8;
^The same failure occurs with an array column, without requiring a
custom type:postgres=# create table t3 (id int primary key, arr int[]);
CREATE TABLE
postgres=# insert into t3 (id, arr[1], arr[2]) values (1, 10, 20);
INSERT 0 1
postgres=# insert into t3 set id=2, arr[1]=30, arr[2]=40;
ERROR: column "arr" specified more than once
LINE 1: insert into t3 set id=2, arr[1]=30, arr[2]=40;
^2. There is a silent misassignment across rows in multi-row SET syntax:
postgres=# create table t7 (id int primary key, arr int[]);
CREATE TABLE
postgres=# insert into t7 set (id=1, arr[1]=111), (id=2, arr[2]=222);
INSERT 0 2
postgres=# select * from t7;
id | arr
----+-------
1 | {111}
2 | {222}
(2 rows)Although row 2 explicitly specifies arr[2]=222, the code only tracks
columns by name and
not by the specific element or field targeted. It retains the tracking
from row 1 ("arr → index [1]")
and applies it to subsequent rows. As a result, the value for row 2
silently lands in arr[1] instead
of arr[2], leaving arr[2] as NULL without throwing an error or warning.This differs from the standard VALUES limitation (e.g., INSERT INTO t7
(id, arr[1]) VALUES (1,111),(2,222)),
where applying arr[1] to both rows is expected because it is defined
once in the shared header.
In this multi-row SET case, the explicit per-row target is ignored and
silently corrupted rather than being rejected as unsupported.Regards,
VaibhavOn Tue, Aug 25, 2026 at 8:40 PM Mario González <gonzalemario@gmail.com>
wrote:On Tue, 14 Jul 2026 at 00:39, Suraj Kharage <
suraj.kharage@enterprisedb.com> wrote:Thanks Mario for the review.
On Mon, Jul 13, 2026 at 12:18 AM Mario González Troncoso <
gonzalemario@gmail.com> wrote:diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 2a2e00b372e..11cb4fcd2da 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4370,6 +4370,8 @@ raw_expression_tree_walker_impl(Node *node, return true; if (WALK(stmt->selectStmt)) return true; + if (WALK(stmt->setClauseList)) + return true; if (WALK(stmt->onConflictClause))you used `stmt->setClauseList` however, I read the entire `raw_expression_tree_walker_impl`
function and it seems we don't mix "clause" with "List" in the variable
names. Reading the whole file, I just found "targetList" and "valuesList".If you get my point, maybe you could use "setClause" only? I know
that sounds like something that exists in setter/getters stuff. Like we're
setting a clause up but would it be worth looking for a new variable name?
I personally think so. Actually, after reading `src/include/nodes/parsenodes.h`,
I think we should go for a change.Renamed setClauseList as per your suggestion.
----
Also, in src/backend/parser/analyze.c we can change a lot of those
foreach by foreach_node, however, I need to ask, did you have a reason to
not use foreach_node() when you first wrote the code? Maybe I'm missing
something. Because this patch is on a commitfest already, I didn't want to
send a patch we might need to squash if I'm right afterwards. That's why
I'd like to show you what I did:
https://github.com/postgres/postgres/commit/7be0538f2a5d916f2fb4a39764985b358cf6d379
If you like I could send a v4- with the squashed version.diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 70c75d0bb20..d2f5b0edcc8 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -679,31 +679,23 @@ transformInsertSetClause(ParseState *pstate, List *setClauseList, { List *all_cols = NIL; /* List of all unique column names */ List *valuesLists = NIL; - ListCell *outer_lc; - ListCell *lc;/* * First pass: collect all unique column names from all rows. * We need to scan all rows first to determine the complete set of columns. * Also check for duplicate columns within each row. */ - foreach(outer_lc, setClauseList) + foreach_node(List, set_clause, setClauseList) { - List *set_clause = (List *) lfirst(outer_lc); List *row_cols = NIL; /* Columns seen in this row */ - ListCell *set_lc; [...]Used foreach_node as per your suggestion.
I have addressed your review comments in the attached v4 patch.
lgtm Suraj. I hope you can find a committer that buys you with this
idea--
Mario Gonzalez
On Wed, Sep 2, 2026 at 6:10 PM Vaibhav Dalvi <vaibhav.dalvi@enterprisedb.com>
wrote:
Hi,
Thanks Suraj for the clarification. I see two issues here, both stemming
from the same root cause:*Issue #1: A row that skips an array column entirely gets wrongly rejected*
Setup:
create table ta(id int primary key, arr int[] default array[1]);Command:
insert into ta set (id=70, arr[1]=1), (id=71, arr[1]=2), (id=72);Actual result:
ERROR: cannot set an array element to DEFAULTWhat's wrong: row id=72 doesn't mention arr in any way, no slot, no whole
column, nothing.
In plain English, it's saying "I don't care about arr, just use whatever
it normally is."
That should give it the column's default ({1} here, since one was
declared, or NULL if not)
exactly like skipping any other ordinary column works fine. Instead,
because other rows in the
same statement touch arr[1], the code silently treats row 72's silence as
if it had explicitly written
arr[1] = DEFAULT, and setting a single array slot to DEFAULT is always
that asked for nothing
gets blamed for a request it never made, and the whole statement fails.Expected result:
id | arr
----+-----
70 | {1}
71 | {2}
72 | {1} -- the column's default, same as ny other statement*Issue #2: Mixing "whole column" and "one slot" across different rows gets
wrongly rejected*
Setup:
create table t1(id int primary key, arr int[] default array[1]);Command:
insert into t1 set (id=1, arr[1]=1), (id=2, arr=default);Actual result:
ERROR: column "arr" specified more than onceWhat's wrong: Row 1 wants to set slot 1 of its own array. Row 2 wants its
own array to just be the default.
These are two completely separate rows with two completely separate array
values, nothing about them
actually conflicts. However, the code doesn't check rows independently; it
first lumps every row's arr-related
target into one shared bucket, and only then checks for conflicts. In
that shared bucket, "the whole column"
and "just slot 1" look like a contradiction, even though they aren't,
since they belong to two different rows, not
one row saying two contradictory things about the same value.Expected result:
id | arr
----+-----
1 | {1}
2 | {1} -- the column's defaultAnother thought here is: should we restrict array/field indirection
targets instead like below?
Thanks for looking into this.
Both issues share the same root cause: multi-row SET merges all rows into
one shared column list,
so once a column uses arr[1]-style indirection, every row must supply that
exact same target.
I think if we make this work, then need bypassing core Postgres
INSERT/rewrite internals logic and it becomes complex.
Postgres builds one assignment expression per column and reuses it for
every row (via Vars into the VALUES RTE),
and the rewriter refuses to merge a whole-column value with an
element/field assignment on the same column
("multiple assignments to same column" in process_matched_tle()).
So I went with your suggested restriction, but narrower: if all rows set
the same array/field target (e.g. all rows set arr[1]),
it keeps working as before. But if rows disagree — one row skips it, or
sets the whole column, or targets a different element/field —
we now raise one clear error.
Updated docs and tests accordingly.
Please let me know your thoughts on the same.
Regards,
Suraj
Show quoted text
insert into t1 set (id=1, arr[1]=1), (id=2, arr=default);
ERROR: array/field indirection targets are not supported in multi-row
INSERT ... SETPer my knowledge, MySQL and MariaDB support this INSERT ... SET syntax:
- Standard MySQL does not support multi-row INSERT ... SET directly using SET
... SET.
In MySQL, INSERT ... SET is strictly single-row:INSERT INTO employees SET id = 1, name = 'Alice', salary = 70000;
- MariaDB extends the standard INSERT ... SET syntax to support multiple
rows by repeating the SET clause for each row:INSERT INTO employees
SET id = 1, name = 'Alice', salary = 70000,
SET id = 2, name = 'Bob', salary = 80000,
SET id = 3, name = 'Charlie', salary = 75000;The point is to check if we can think in that direction?
Regards,
VaibhavOn Mon, Aug 31, 2026 at 10:20 AM Suraj Kharage <
suraj.kharage@enterprisedb.com> wrote:On Fri, Aug 28, 2026 at 11:39 AM Vaibhav Dalvi <
vaibhav.dalvi@enterprisedb.com> wrote:create table ta(id int primary key, arr int[] default array[1]);
-- classic VALUES: row 3 gets the table's default {1}
insert into ta values (70, array[1]), (71, array[2]), (72, default);
-- id | arr
-- ----+-----
-- 70 | {1}
-- 71 | {2}
-- 72 | {1}-- new SET syntax: same intent, row 3 just omits arr
insert into ta set (id=170, arr[1]=1), (id=171, arr[1]=2), (id=172);
-- ERROR: cannot set an array element to DEFAULTThanks for reporting this. I have looked into this and feel that this is
an existing behavior for INSERT command.
transformAssignedExpr() unconditionally rejects DEFAULT for any
indirection target (array element or subfield).For e.g. for plain insert as well it will fail -
postgres@2495950=#INSERT INTO ta(id, arr[1]) VALUES (70,1),(71,2),(72,
default);
ERROR: cannot set an array element to DEFAULT
LINE 1: INSERT INTO ta(id, arr[1]) VALUES (70,1),(71,2),(72, default...It will work in both cases, if we try to insert in arr column.
postgres@2495950=#INSERT INTO ta VALUES (70,array[1]),(71,array[2]),(72,
default);
INSERT 0 3
postgres@2495950=#select * from ta;
id | arr
----+-----
70 | {1}
71 | {2}
72 | {1}
(3 rows)postgres@2495950=#insert into ta set (id=170, arr=array[1]), (id=171,
arr=array[2]), (id=172, arr=default);
INSERT 0 3
postgres@2495950=#select * from ta;
id | arr
-----+-----
170 | {1}
171 | {2}
172 | {1}
(3 rows)I hope this helps.
Regards,
Suraj*Classic VALUES: *if a row skips a column (or writes DEFAULT), Postgres
asks, "What's the default for the whole column arr?" —
table says {1}, done.*New INSERT ... SET:* if a row skips arr, but other rows in the same
statement use arr[1]=..., Postgres instead asks "what's
the default for just slot 1 of arr?" — and that question has always been
illegal in Postgres (you can't default one array
slot). So it errors, even though the row never asked for a slot at all —
it just wanted the whole column left alone.*Why is this a bug?: *row 172 never used array-indirection syntax at
all. it should get the plain, legal,
whole-column default ({1}), exactly like classic VALUES gives it.
Instead, the code wrongly forces it through the "set array
slot 1 to default" path just because other rows in the same statement
happened to use arr[1]. That path is always illegal
in Postgres, so a perfectly valid statement fails with an error the
user's SQL never requested.Regards,
VaibhavOn Thu, Aug 27, 2026 at 4:25 PM Suraj Kharage <
suraj.kharage@enterprisedb.com> wrote:Thank you Vaibhav for the review.
I have fixed these issues in the attached v4 patch. Please have a look.
--Thanks & Regards,
Suraj kharage,enterprisedb.com <https://www.enterprisedb.com/>
On Wed, Aug 26, 2026 at 6:32 PM Vaibhav Dalvi <
vaibhav.dalvi@enterprisedb.com> wrote:Hi Suraj,
I have a few observations regarding the latest v4 patch:
1. Assigning two different subfields or elements of the same column
in a single row is rejected,
even though the equivalent column-list INSERT syntax accepts it:postgres=# create type comp_t as (x int, y int);
CREATE TYPE
postgres=# create table t2 (id int primary key, c comp_t);
CREATE TABLE
postgres=# insert into t2 (id, c.x, c.y) values (1, 5, 6);
INSERT 0 1
postgres=# insert into t2 set id=2, c.x=7, c.y=8;
ERROR: column "c" specified more than once
LINE 1: insert into t2 set id=2, c.x=7, c.y=8;
^The same failure occurs with an array column, without requiring a
custom type:postgres=# create table t3 (id int primary key, arr int[]);
CREATE TABLE
postgres=# insert into t3 (id, arr[1], arr[2]) values (1, 10, 20);
INSERT 0 1
postgres=# insert into t3 set id=2, arr[1]=30, arr[2]=40;
ERROR: column "arr" specified more than once
LINE 1: insert into t3 set id=2, arr[1]=30, arr[2]=40;
^2. There is a silent misassignment across rows in multi-row SET
syntax:postgres=# create table t7 (id int primary key, arr int[]);
CREATE TABLE
postgres=# insert into t7 set (id=1, arr[1]=111), (id=2, arr[2]=222);
INSERT 0 2
postgres=# select * from t7;
id | arr
----+-------
1 | {111}
2 | {222}
(2 rows)Although row 2 explicitly specifies arr[2]=222, the code only tracks
columns by name and
not by the specific element or field targeted. It retains the tracking
from row 1 ("arr → index [1]")
and applies it to subsequent rows. As a result, the value for row 2
silently lands in arr[1] instead
of arr[2], leaving arr[2] as NULL without throwing an error or warning.This differs from the standard VALUES limitation (e.g., INSERT INTO t7
(id, arr[1]) VALUES (1,111),(2,222)),
where applying arr[1] to both rows is expected because it is defined
once in the shared header.
In this multi-row SET case, the explicit per-row target is ignored and
silently corrupted rather than being rejected as unsupported.Regards,
VaibhavOn Tue, Aug 25, 2026 at 8:40 PM Mario González <gonzalemario@gmail.com>
wrote:On Tue, 14 Jul 2026 at 00:39, Suraj Kharage <
suraj.kharage@enterprisedb.com> wrote:Thanks Mario for the review.
On Mon, Jul 13, 2026 at 12:18 AM Mario González Troncoso <
gonzalemario@gmail.com> wrote:diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 2a2e00b372e..11cb4fcd2da 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4370,6 +4370,8 @@ raw_expression_tree_walker_impl(Node *node, return true; if (WALK(stmt->selectStmt)) return true; + if (WALK(stmt->setClauseList)) + return true; if (WALK(stmt->onConflictClause))you used `stmt->setClauseList` however, I read the entire `raw_expression_tree_walker_impl`
function and it seems we don't mix "clause" with "List" in the variable
names. Reading the whole file, I just found "targetList" and "valuesList".If you get my point, maybe you could use "setClause" only? I know
that sounds like something that exists in setter/getters stuff. Like we're
setting a clause up but would it be worth looking for a new variable name?
I personally think so. Actually, after reading `src/include/nodes/parsenodes.h`,
I think we should go for a change.Renamed setClauseList as per your suggestion.
----
Also, in src/backend/parser/analyze.c we can change a lot of those
foreach by foreach_node, however, I need to ask, did you have a reason to
not use foreach_node() when you first wrote the code? Maybe I'm missing
something. Because this patch is on a commitfest already, I didn't want to
send a patch we might need to squash if I'm right afterwards. That's why
I'd like to show you what I did:
https://github.com/postgres/postgres/commit/7be0538f2a5d916f2fb4a39764985b358cf6d379
If you like I could send a v4- with the squashed version.diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 70c75d0bb20..d2f5b0edcc8 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -679,31 +679,23 @@ transformInsertSetClause(ParseState *pstate, List *setClauseList, { List *all_cols = NIL; /* List of all unique column names */ List *valuesLists = NIL; - ListCell *outer_lc; - ListCell *lc;/* * First pass: collect all unique column names from all rows. * We need to scan all rows first to determine the complete set of columns. * Also check for duplicate columns within each row. */ - foreach(outer_lc, setClauseList) + foreach_node(List, set_clause, setClauseList) { - List *set_clause = (List *) lfirst(outer_lc); List *row_cols = NIL; /* Columns seen in this row */ - ListCell *set_lc; [...]Used foreach_node as per your suggestion.
I have addressed your review comments in the attached v4 patch.
lgtm Suraj. I hope you can find a committer that buys you with this
idea--
Mario Gonzalez