From 44ff9500dfa14994047d2d64279259c39360b6b9 Mon Sep 17 00:00:00 2001
From: Gareth Palmer <gareth@internetnz.net.nz>
Date: Mon, 26 Aug 2019 14:39:16 +1200
Subject: [PATCH] Implement INSERT SET syntax

Allow the target column and values of an INSERT statement to be specified
using a SET clause in the same manner as that of an UPDATE statement.

The advantage of using the INSERT SET style is that the columns and values
are kept together, which can make changing or removing a column or value
from a large list easier.

A simple example that uses SET instead of a VALUES() clause:

INSERT INTO t SET c1 = 'foo', c2 = 'bar', c3 = 'baz';

Values can also be sourced from other tables similar to the INSERT INTO
SELECT FROM syntax:

INSERT INTO t SET c1 = x.c1, c2 = x.c2 FROM x WHERE x.c2 > 10 LIMIT 10;

INSERT SET is not part of any SQL standard, however this syntax is also
implemented by MySQL. Their implementation does not support specifying a
FROM clause.
---
 doc/src/sgml/ref/insert.sgml                  | 52 +++++++++++++++++++
 src/backend/parser/gram.y                     | 50 +++++++++++++++++-
 src/backend/parser/parse_expr.c               |  9 +++-
 src/test/regress/expected/insert.out          | 38 ++++++++++++++
 src/test/regress/expected/insert_conflict.out |  2 +
 src/test/regress/expected/with.out            | 21 ++++++++
 src/test/regress/sql/insert.sql               | 30 +++++++++++
 src/test/regress/sql/insert_conflict.sql      |  3 ++
 src/test/regress/sql/with.sql                 | 10 ++++
 9 files changed, 212 insertions(+), 3 deletions(-)

diff --git a/doc/src/sgml/ref/insert.sgml b/doc/src/sgml/ref/insert.sgml
index f995a7637f..71fab5126d 100644
--- a/doc/src/sgml/ref/insert.sgml
+++ b/doc/src/sgml/ref/insert.sgml
@@ -28,6 +28,25 @@ INSERT INTO <replaceable class="parameter">table_name</replaceable> [ AS <replac
     [ ON CONFLICT [ <replaceable class="parameter">conflict_target</replaceable> ] <replaceable class="parameter">conflict_action</replaceable> ]
     [ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
 
+[ WITH [ RECURSIVE ] <replaceable class="parameter">with_query</replaceable> [, ...] ]
+INSERT INTO <replaceable class="parameter">table_name</replaceable> [ AS <replaceable class="parameter">alias</replaceable> ]
+    [ OVERRIDING { SYSTEM | USER} VALUE ]
+    SET { <replaceable class="parameter">column_name</replaceable> = { <replaceable class="parameter">expression</replaceable> | DEFAULT } |
+          ( <replaceable class="parameter">column_name</replaceable> [, ...] ) = [ ROW ] ( { <replaceable class="parameter">expression</replaceable> | DEFAULT } [, ...] ) |
+          ( <replaceable class="parameter">column_name</replaceable> [, ...] ) = ( <replaceable class="parameter">sub-SELECT</replaceable> )
+        } [, ...]
+    [ FROM <replaceable class="parameter">from_list</replaceable>
+           [ WHERE <replaceable class="parameter">condition</replaceable> ] 
+           [ GROUP BY <replaceable class="parameter">grouping_element</replaceable> [, ...] ]
+           [ HAVING <replaceable class="parameter">condition</replaceable> [, ...] ]
+           [ WINDOW <replaceable class="parameter">window_name</replaceable> AS ( <replaceable class="parameter">window_definition</replaceable> ) [, ...] ]
+           [ ORDER BY <replaceable class="parameter">expression</replaceable> [ ASC | DESC | USING <replaceable class="parameter">operator</replaceable> ] [ NULLS { FIRST | LAST } ] [, ...] ]
+           [ LIMIT { <replaceable class="parameter">count</replaceable> | ALL } ]
+           [ OFFSET <replaceable class="parameter">start</replaceable> [ ROW | ROWS ] ] ]
+           [ FETCH { FIRST | NEXT } [ <replaceable class="parameter">count</replaceable> ] { ROW | ROWS } ONLY ]
+    [ ON CONFLICT [ <replaceable class="parameter">conflict_target</replaceable> ] <replaceable class="parameter">conflict_action</replaceable> ]
+    [ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
+
 <phrase>where <replaceable class="parameter">conflict_target</replaceable> can be one of:</phrase>
 
     ( { <replaceable class="parameter">index_column_name</replaceable> | ( <replaceable class="parameter">index_expression</replaceable> ) } [ COLLATE <replaceable class="parameter">collation</replaceable> ] [ <replaceable class="parameter">opclass</replaceable> ] [, ...] ) [ WHERE <replaceable class="parameter">index_predicate</replaceable> ]
@@ -254,6 +273,32 @@ INSERT INTO <replaceable class="parameter">table_name</replaceable> [ AS <replac
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><replaceable class="parameter">from_list</replaceable></term>
+      <listitem>
+       <para>
+        A list of table expressions, allowing columns from other tables
+        to be used as values in the <literal>expression</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><replaceable class="parameter">condition</replaceable></term>
+      <term><replaceable class="parameter">grouping_element</replaceable></term>
+      <term><replaceable class="parameter">window_name</replaceable></term>
+      <term><replaceable class="parameter">count</replaceable></term>
+      <term><replaceable class="parameter">start</replaceable></term>
+      <listitem>
+       <para>
+        These have the same meaning as when used in a query
+        (<command>SELECT</command> statement).  Refer to the
+        <xref linkend="sql-select"/> statement for a description of the
+        syntax.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><literal>DEFAULT</literal></term>
       <listitem>
@@ -675,6 +720,13 @@ WITH upd AS (
     RETURNING *
 )
 INSERT INTO employees_log SELECT *, current_timestamp FROM upd;
+</programlisting>
+  </para>
+  <para>
+   Insert a single row into table <literal>distributors</literal> using a
+<literal>SET</literal> clause to specify the columns and values:
+<programlisting>
+INSERT INTO distributors SET did = 4, dname = 'Hammers Unlimited, LLC';
 </programlisting>
   </para>
   <para>
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c97bb367f8..9c907dfd1b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -463,7 +463,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <list>	OptSeqOptList SeqOptList OptParenthesizedSeqOptList
 %type <defelt>	SeqOptElem
 
-%type <istmt>	insert_rest
+%type <istmt>	insert_rest insert_set_clause
 %type <infer>	opt_conf_expr
 %type <onconflict> opt_on_conflict
 
@@ -10892,6 +10892,15 @@ insert_rest:
 					$$->override = $5;
 					$$->selectStmt = $7;
 				}
+			| insert_set_clause
+				{
+					$$ = $1;
+				}
+			| OVERRIDING override_kind VALUE_P insert_set_clause
+				{
+					$$ = $4;
+					$$->override = $2;
+				}
 			| DEFAULT VALUES
 				{
 					$$ = makeNode(InsertStmt);
@@ -10923,6 +10932,45 @@ insert_column_item:
 				}
 		;
 
+insert_set_clause:
+		SET set_clause_list
+			{
+				SelectStmt *n = makeNode(SelectStmt);
+				List *values = NIL;
+				ListCell *col_cell;
+
+				foreach(col_cell, $2)
+				{
+					ResTarget *res_col = (ResTarget *) lfirst(col_cell);
+
+					values = lappend(values, res_col->val);
+				}
+				n->valuesLists = list_make1(values);
+				$$ = makeNode(InsertStmt);
+				$$->cols = $2;
+				$$->selectStmt = (Node *) n;
+			}
+		| SET set_clause_list FROM from_list where_clause
+		  group_clause having_clause window_clause opt_sort_clause
+		  opt_select_limit
+			{
+				SelectStmt *n = makeNode(SelectStmt);
+				n->targetList = $2;
+				n->fromClause = $4;
+				n->whereClause = $5;
+				n->groupClause = $6;
+				n->havingClause = $7;
+				n->windowClause = $8;
+				insertSelectOptions(n, $9, NULL,
+									list_nth($10, 0), list_nth($10, 1),
+									NULL,
+									yyscanner);
+				$$ = makeNode(InsertStmt);
+				$$->cols = $2;
+				$$->selectStmt = (Node *) n;
+			}
+		;
+
 opt_on_conflict:
 			ON CONFLICT opt_conf_expr DO UPDATE SET set_clause_list	where_clause
 				{
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 76f3dd7076..91bf1530b2 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1566,8 +1566,13 @@ transformMultiAssignRef(ParseState *pstate, MultiAssignRef *maref)
 	Query	   *qtree;
 	TargetEntry *tle;
 
-	/* We should only see this in first-stage processing of UPDATE tlists */
-	Assert(pstate->p_expr_kind == EXPR_KIND_UPDATE_SOURCE);
+	/*
+	 * We should only see this in first-stage processing of UPDATE tlists
+	 * or of an INSERT SET tlist.
+	 */
+	Assert(pstate->p_expr_kind == EXPR_KIND_VALUES_SINGLE ||
+		   pstate->p_expr_kind == EXPR_KIND_INSERT_TARGET ||
+		   pstate->p_expr_kind == EXPR_KIND_UPDATE_SOURCE);
 
 	/* We only need to transform the source if this is the first column */
 	if (maref->colno == 1)
diff --git a/src/test/regress/expected/insert.out b/src/test/regress/expected/insert.out
index 75e25cdf48..6a934d1873 100644
--- a/src/test/regress/expected/insert.out
+++ b/src/test/regress/expected/insert.out
@@ -82,6 +82,44 @@ select col1, col2, char_length(col3) from inserttest;
 
 drop table inserttest;
 --
+-- SET test
+--
+create table inserttest (col1 int4, col2 text default 'bar');
+insert into inserttest set col1 = 1, col2 = 'foo';
+insert into inserttest set col1 = 2, col2 = DEFAULT;
+insert into inserttest set (col1, col2) = (3, 'baz');
+select * from inserttest;
+ col1 | col2 
+------+------
+    1 | foo
+    2 | bar
+    3 | baz
+(3 rows)
+
+truncate inserttest;
+create table inserttest2 (col1 int4, col2 text default 'f');
+insert into inserttest2 values (10, 't'), (20, 'f'), (50, 't');
+insert into inserttest set col1 = inserttest2.col1 from inserttest2
+  where inserttest2.col1 > 10 order by inserttest2.col1 desc limit 1;
+select * from inserttest;
+ col1 | col2 
+------+------
+   50 | bar
+(1 row)
+
+truncate inserttest;
+insert into inserttest set col1 = sum(inserttest2.col1) from inserttest2
+  group by inserttest2.col2;
+select * from inserttest;
+ col1 | col2 
+------+------
+   60 | bar
+   20 | bar
+(2 rows)
+
+drop table inserttest;
+drop table inserttest2;
+--
 -- check indirection (field/array assignment), cf bug #14265
 --
 -- these tests are aware that transformInsertStmt has 3 separate code paths
diff --git a/src/test/regress/expected/insert_conflict.out b/src/test/regress/expected/insert_conflict.out
index 1338b2b23e..4d6538b1fb 100644
--- a/src/test/regress/expected/insert_conflict.out
+++ b/src/test/regress/expected/insert_conflict.out
@@ -236,6 +236,8 @@ insert into insertconflicttest values (2, 'Orange') on conflict (key, key, key)
 insert into insertconflicttest
 values (1, 'Apple'), (2, 'Orange')
 on conflict (key) do update set (fruit, key) = (excluded.fruit, excluded.key);
+-- Using insert set syntax
+insert into insertconflicttest set key = 1, fruit = 'Banana' on conflict (key) do update set fruit = excluded.fruit;
 -- Give good diagnostic message when EXCLUDED.* spuriously referenced from
 -- RETURNING:
 insert into insertconflicttest values (1, 'Apple') on conflict (key) do update set fruit = excluded.fruit RETURNING excluded.fruit;
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 2a2085556b..b92f9f8a13 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -952,6 +952,27 @@ SELECT * FROM y;
  10
 (10 rows)
 
+DROP TABLE y;
+CREATE TEMPORARY TABLE y (a INTEGER);
+WITH t AS (
+    SELECT generate_series(1, 10) AS a
+)
+INSERT INTO y SET a = t.a+20 FROM t;
+SELECT * FROM y;
+ a  
+----
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+(10 rows)
+
 DROP TABLE y;
 --
 -- error cases
diff --git a/src/test/regress/sql/insert.sql b/src/test/regress/sql/insert.sql
index 23885f638c..c00a23322b 100644
--- a/src/test/regress/sql/insert.sql
+++ b/src/test/regress/sql/insert.sql
@@ -37,6 +37,36 @@ select col1, col2, char_length(col3) from inserttest;
 
 drop table inserttest;
 
+--
+-- SET test
+--
+create table inserttest (col1 int4, col2 text default 'bar');
+insert into inserttest set col1 = 1, col2 = 'foo';
+insert into inserttest set col1 = 2, col2 = DEFAULT;
+insert into inserttest set (col1, col2) = (3, 'baz');
+
+select * from inserttest;
+
+truncate inserttest;
+
+create table inserttest2 (col1 int4, col2 text default 'f');
+insert into inserttest2 values (10, 't'), (20, 'f'), (50, 't');
+
+insert into inserttest set col1 = inserttest2.col1 from inserttest2
+  where inserttest2.col1 > 10 order by inserttest2.col1 desc limit 1;
+
+select * from inserttest;
+
+truncate inserttest;
+
+insert into inserttest set col1 = sum(inserttest2.col1) from inserttest2
+  group by inserttest2.col2;
+
+select * from inserttest;
+
+drop table inserttest;
+drop table inserttest2;
+
 --
 -- check indirection (field/array assignment), cf bug #14265
 --
diff --git a/src/test/regress/sql/insert_conflict.sql b/src/test/regress/sql/insert_conflict.sql
index 43691cd335..e0eb2df0c8 100644
--- a/src/test/regress/sql/insert_conflict.sql
+++ b/src/test/regress/sql/insert_conflict.sql
@@ -97,6 +97,9 @@ insert into insertconflicttest
 values (1, 'Apple'), (2, 'Orange')
 on conflict (key) do update set (fruit, key) = (excluded.fruit, excluded.key);
 
+-- Using insert set syntax
+insert into insertconflicttest set key = 1, fruit = 'Banana' on conflict (key) do update set fruit = excluded.fruit;
+
 -- Give good diagnostic message when EXCLUDED.* spuriously referenced from
 -- RETURNING:
 insert into insertconflicttest values (1, 'Apple') on conflict (key) do update set fruit = excluded.fruit RETURNING excluded.fruit;
diff --git a/src/test/regress/sql/with.sql b/src/test/regress/sql/with.sql
index f85645efde..e6d2e13714 100644
--- a/src/test/regress/sql/with.sql
+++ b/src/test/regress/sql/with.sql
@@ -406,6 +406,16 @@ SELECT * FROM y;
 
 DROP TABLE y;
 
+CREATE TEMPORARY TABLE y (a INTEGER);
+WITH t AS (
+    SELECT generate_series(1, 10) AS a
+)
+INSERT INTO y SET a = t.a+20 FROM t;
+
+SELECT * FROM y;
+
+DROP TABLE y;
+
 --
 -- error cases
 --
-- 
2.17.1

