From f1ab7edadac846883b9c12f02ecc6c64dd293060 Mon Sep 17 00:00:00 2001 From: Gareth Palmer Date: Thu, 14 Jul 2022 01:47:17 +0000 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 | 56 +++++++++++++- src/backend/parser/gram.y | 77 ++++++++++++++++++- src/test/regress/expected/identity.out | 42 ++++++---- src/test/regress/expected/insert.out | 13 +++- src/test/regress/expected/insert_conflict.out | 2 + src/test/regress/expected/with.out | 20 +++++ src/test/regress/sql/identity.sql | 9 +++ src/test/regress/sql/insert.sql | 1 + src/test/regress/sql/insert_conflict.sql | 3 + src/test/regress/sql/with.sql | 9 +++ 10 files changed, 210 insertions(+), 22 deletions(-) diff --git a/doc/src/sgml/ref/insert.sgml b/doc/src/sgml/ref/insert.sgml index a9af9959c0..b774b6ce74 100644 --- a/doc/src/sgml/ref/insert.sgml +++ b/doc/src/sgml/ref/insert.sgml @@ -28,6 +28,16 @@ INSERT INTO table_name [ AS conflict_target ] conflict_action ] [ RETURNING * | output_expression [ [ AS ] output_name ] [, ...] ] + +[ WITH [ RECURSIVE ] with_query [, ...] ] +INSERT INTO table_name [ AS alias ] + [ OVERRIDING { SYSTEM | USER} VALUE ] + SET { column_name = { expression | DEFAULT } } [, ...] + [ FROM from_clause ] + [ ON CONFLICT [ conflict_target ] conflict_action ] + [ RETURNING * | output_expression [ [ AS ] output_name ] [, ...] ] + + where conflict_target can be one of: ( { index_column_name | ( index_expression ) } [ COLLATE collation ] [ opclass ] [, ...] ) [ WHERE index_predicate ] @@ -263,6 +273,18 @@ INSERT INTO table_name [ AS + + from_clause + + + A list of table expressions, allowing columns from other tables + to be used as values in the expression. + Refer to the statement for a + description of the syntax. + + + + DEFAULT @@ -650,6 +672,15 @@ INSERT INTO films (code, title, did, date_prod, kind) VALUES + + Insert a row using SET syntax: + + +INSERT INTO films SET code = 'MH832', title = 'Blade Runner', + did = 201, date_prod = DEFAULT, kind = 'SciFi'; + + + This example inserts some rows into table films from a table tmp_films @@ -696,6 +727,16 @@ WITH upd AS ( INSERT INTO employees_log SELECT *, current_timestamp FROM upd; + + Insert multiple rows into employees_log containing +the hours worked by each employee from time_sheets. + +INSERT INTO employees_log SET id = time_sheets.employee, + total_hours = sum(time_sheets.hours) FROM time_sheets + WHERE time_sheets.date ≥ '2019-11-15' GROUP BY time_sheets.employee; + + + Insert or update new distributors as appropriate. Assumes a unique index has been defined that constrains values appearing in the @@ -752,6 +793,18 @@ INSERT INTO distributors (did, dname) VALUES (9, 'Antwerp Design') INSERT INTO distributors (did, dname) VALUES (10, 'Conrad International') ON CONFLICT (did) WHERE is_active DO NOTHING; + + Insert a new film into watched_films or increment the + number of times seen. Returns the new seen count, example assumes a + unique index has been defined that constrains the values appearing in + the title and year columns and + that seen_count defaults to 1. + +INSERT INTO watched_films SET title = 'Akira', year = 1988 + ON CONFLICT (title, year) DO UPDATE SET seen_count = watched_films.seen_count + 1 + RETURNING watched_films.seen_count; + + @@ -762,7 +815,8 @@ INSERT INTO distributors (did, dname) VALUES (10, 'Conrad International') the RETURNING clause is a PostgreSQL extension, as is the ability to use WITH with INSERT, and the ability to - specify an alternative action with ON CONFLICT. + specify an alternative action with ON CONFLICT, and the + ability to specify the inserted columns using SET. Also, the case in which a column name list is omitted, but not all the columns are filled from the VALUES clause or query, diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index c018140afe..a995875449 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -442,7 +442,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); distinct_clause opt_distinct_clause target_list opt_target_list insert_column_list set_target_list merge_values_clause - set_clause_list set_clause + set_clause_list set_clause insert_set_list def_list operator_def_list indirection opt_indirection reloption_list TriggerFuncArgs opclass_item_list opclass_drop_list opclass_purpose opt_opfamily transaction_mode_list_or_empty @@ -512,7 +512,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type OptSeqOptList SeqOptList OptParenthesizedSeqOptList %type SeqOptElem -%type insert_rest +%type insert_rest insert_set_clause %type opt_conf_expr %type opt_on_conflict %type merge_insert merge_update merge_delete @@ -553,7 +553,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type extended_relation_expr %type relation_expr_opt_alias %type tablesample_clause opt_repeatable_clause -%type target_el set_target insert_column_item +%type target_el set_target insert_column_item insert_set_item %type generic_option_name %type generic_option_arg @@ -12063,6 +12063,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); @@ -12094,6 +12103,65 @@ insert_column_item: } ; +/* + * There are two rules here to handle the two different types of INSERT. + * INSERT using VALUES and INSERT using SELECT. They can't be combined + * because only the VALUES syntax allows specifying DEFAULT. + */ +insert_set_clause: + SET insert_set_list + { + SelectStmt *n = makeNode(SelectStmt); + ListCell *col_cell; + List *values = NIL; + + 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 insert_set_list FROM from_list where_clause group_clause + having_clause window_clause opt_sort_clause opt_select_limit + opt_for_locking_clause + { + SelectStmt *n = makeNode(SelectStmt); + + n->targetList = $2; + n->fromClause = $4; + n->whereClause = $5; + n->groupClause = ($6)->list; + n->groupDistinct = ($6)->distinct; + n->havingClause = $7; + n->windowClause = $8; + insertSelectOptions(n, $9, $11, $10, NULL, yyscanner); + $$ = makeNode(InsertStmt); + $$->cols = $2; + $$->selectStmt = (Node *) n; + } + ; + +insert_set_list: + insert_set_item + { $$ = list_make1($1); } + | insert_set_list ',' insert_set_item + { $$ = lappend($1, $3); } + ; + +insert_set_item: + insert_column_item '=' a_expr + { + $$ = $1; + $$->val = $3; + } + ; + opt_on_conflict: ON CONFLICT opt_conf_expr DO UPDATE SET set_clause_list where_clause { @@ -12646,6 +12714,9 @@ select_clause: * * NOTE: only the leftmost component SelectStmt should have INTO. * However, this is not checked by the grammar; parse analysis must check it. + * + * NOTE: insert_set_clause also has SELECT-like syntax so if you add any + * clauses after from_clause here you may need to add them there as well. */ simple_select: SELECT opt_all_clause opt_target_list diff --git a/src/test/regress/expected/identity.out b/src/test/regress/expected/identity.out index 5f03d8e14f..08df7aec3a 100644 --- a/src/test/regress/expected/identity.out +++ b/src/test/regress/expected/identity.out @@ -124,41 +124,55 @@ ERROR: cannot insert a non-DEFAULT value into column "a" DETAIL: Column "a" is an identity column defined as GENERATED ALWAYS. HINT: Use OVERRIDING SYSTEM VALUE to override. INSERT INTO itest5 VALUES (DEFAULT, 'b'), (DEFAULT, 'c'); -- ok +INSERT INTO itest5 SET a = 4, b = 'a'; -- error +ERROR: cannot insert a non-DEFAULT value into column "a" +DETAIL: Column "a" is an identity column defined as GENERATED ALWAYS. +HINT: Use OVERRIDING SYSTEM VALUE to override. +INSERT INTO itest5 SET a = DEFAULT, b = 'd'; -- ok INSERT INTO itest5 OVERRIDING SYSTEM VALUE VALUES (-1, 'aa'); INSERT INTO itest5 OVERRIDING SYSTEM VALUE VALUES (-2, 'bb'), (-3, 'cc'); INSERT INTO itest5 OVERRIDING SYSTEM VALUE VALUES (DEFAULT, 'dd'), (-4, 'ee'); INSERT INTO itest5 OVERRIDING SYSTEM VALUE VALUES (-5, 'ff'), (DEFAULT, 'gg'); INSERT INTO itest5 OVERRIDING SYSTEM VALUE VALUES (DEFAULT, 'hh'), (DEFAULT, 'ii'); +INSERT INTO itest5 OVERRIDING SYSTEM VALUE SET a = -6, b = 'jj'; +INSERT INTO itest5 OVERRIDING SYSTEM VALUE SET a = DEFAULT, b = 'kk'; INSERT INTO itest5 OVERRIDING USER VALUE VALUES (-1, 'aaa'); INSERT INTO itest5 OVERRIDING USER VALUE VALUES (-2, 'bbb'), (-3, 'ccc'); INSERT INTO itest5 OVERRIDING USER VALUE VALUES (DEFAULT, 'ddd'), (-4, 'eee'); INSERT INTO itest5 OVERRIDING USER VALUE VALUES (-5, 'fff'), (DEFAULT, 'ggg'); INSERT INTO itest5 OVERRIDING USER VALUE VALUES (DEFAULT, 'hhh'), (DEFAULT, 'iii'); +INSERT INTO itest5 OVERRIDING USER VALUE SET a = -6, b = 'jjj'; +INSERT INTO itest5 OVERRIDING USER VALUE SET a = DEFAULT, b = 'kkk'; SELECT * FROM itest5; a | b ----+----- 1 | a 2 | b 3 | c + 4 | d -1 | aa -2 | bb -3 | cc - 4 | dd + 5 | dd -4 | ee -5 | ff - 5 | gg - 6 | hh - 7 | ii - 8 | aaa - 9 | bbb - 10 | ccc - 11 | ddd - 12 | eee - 13 | fff - 14 | ggg - 15 | hhh - 16 | iii -(21 rows) + 6 | gg + 7 | hh + 8 | ii + -6 | jj + 9 | kk + 10 | aaa + 11 | bbb + 12 | ccc + 13 | ddd + 14 | eee + 15 | fff + 16 | ggg + 17 | hhh + 18 | iii + 19 | jjj + 20 | kkk +(26 rows) DROP TABLE itest5; INSERT INTO itest3 VALUES (DEFAULT, 'a'); diff --git a/src/test/regress/expected/insert.out b/src/test/regress/expected/insert.out index dd4354fc7d..3dd6ef855c 100644 --- a/src/test/regress/expected/insert.out +++ b/src/test/regress/expected/insert.out @@ -9,6 +9,7 @@ insert into inserttest (col2, col3) values (3, DEFAULT); insert into inserttest (col1, col2, col3) values (DEFAULT, 5, DEFAULT); insert into inserttest values (DEFAULT, 5, 'test'); insert into inserttest values (DEFAULT, 7); +insert into inserttest set col1 = DEFAULT, col2 = 9; select * from inserttest; col1 | col2 | col3 ------+------+--------- @@ -16,7 +17,8 @@ select * from inserttest; | 5 | testing | 5 | test | 7 | testing -(4 rows) + | 9 | testing +(5 rows) -- -- insert with similar expression / target_list values (all fail) @@ -44,7 +46,8 @@ select * from inserttest; | 5 | testing | 5 | test | 7 | testing -(4 rows) + | 9 | testing +(5 rows) -- -- VALUES test @@ -58,10 +61,11 @@ select * from inserttest; | 5 | testing | 5 | test | 7 | testing + | 9 | testing 10 | 20 | 40 -1 | 2 | testing 2 | 3 | values are fun! -(7 rows) +(8 rows) -- -- TOASTed value test @@ -74,11 +78,12 @@ select col1, col2, char_length(col3) from inserttest; | 5 | 7 | 5 | 4 | 7 | 7 + | 9 | 7 10 | 20 | 2 -1 | 2 | 7 2 | 3 | 15 30 | 50 | 10000 -(8 rows) +(9 rows) drop table inserttest; -- diff --git a/src/test/regress/expected/insert_conflict.out b/src/test/regress/expected/insert_conflict.out index 66d8633e3e..0a342ca5e9 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 30dd900e11..9a10453b67 100644 --- a/src/test/regress/expected/with.out +++ b/src/test/regress/expected/with.out @@ -1790,6 +1790,26 @@ SELECT * FROM y; 10 (10 rows) +TRUNCATE TABLE y; +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/identity.sql b/src/test/regress/sql/identity.sql index 9b8db2e4a3..f844d7ebfa 100644 --- a/src/test/regress/sql/identity.sql +++ b/src/test/regress/sql/identity.sql @@ -64,18 +64,27 @@ INSERT INTO itest5 VALUES (DEFAULT, 'b'), (3, 'c'); -- error INSERT INTO itest5 VALUES (2, 'b'), (DEFAULT, 'c'); -- error INSERT INTO itest5 VALUES (DEFAULT, 'b'), (DEFAULT, 'c'); -- ok +INSERT INTO itest5 SET a = 4, b = 'a'; -- error +INSERT INTO itest5 SET a = DEFAULT, b = 'd'; -- ok + INSERT INTO itest5 OVERRIDING SYSTEM VALUE VALUES (-1, 'aa'); INSERT INTO itest5 OVERRIDING SYSTEM VALUE VALUES (-2, 'bb'), (-3, 'cc'); INSERT INTO itest5 OVERRIDING SYSTEM VALUE VALUES (DEFAULT, 'dd'), (-4, 'ee'); INSERT INTO itest5 OVERRIDING SYSTEM VALUE VALUES (-5, 'ff'), (DEFAULT, 'gg'); INSERT INTO itest5 OVERRIDING SYSTEM VALUE VALUES (DEFAULT, 'hh'), (DEFAULT, 'ii'); +INSERT INTO itest5 OVERRIDING SYSTEM VALUE SET a = -6, b = 'jj'; +INSERT INTO itest5 OVERRIDING SYSTEM VALUE SET a = DEFAULT, b = 'kk'; + INSERT INTO itest5 OVERRIDING USER VALUE VALUES (-1, 'aaa'); INSERT INTO itest5 OVERRIDING USER VALUE VALUES (-2, 'bbb'), (-3, 'ccc'); INSERT INTO itest5 OVERRIDING USER VALUE VALUES (DEFAULT, 'ddd'), (-4, 'eee'); INSERT INTO itest5 OVERRIDING USER VALUE VALUES (-5, 'fff'), (DEFAULT, 'ggg'); INSERT INTO itest5 OVERRIDING USER VALUE VALUES (DEFAULT, 'hhh'), (DEFAULT, 'iii'); +INSERT INTO itest5 OVERRIDING USER VALUE SET a = -6, b = 'jjj'; +INSERT INTO itest5 OVERRIDING USER VALUE SET a = DEFAULT, b = 'kkk'; + SELECT * FROM itest5; DROP TABLE itest5; diff --git a/src/test/regress/sql/insert.sql b/src/test/regress/sql/insert.sql index bdcffd0314..8265c8d993 100644 --- a/src/test/regress/sql/insert.sql +++ b/src/test/regress/sql/insert.sql @@ -7,6 +7,7 @@ insert into inserttest (col2, col3) values (3, DEFAULT); insert into inserttest (col1, col2, col3) values (DEFAULT, 5, DEFAULT); insert into inserttest values (DEFAULT, 5, 'test'); insert into inserttest values (DEFAULT, 7); +insert into inserttest set col1 = DEFAULT, col2 = 9; select * from inserttest; diff --git a/src/test/regress/sql/insert_conflict.sql b/src/test/regress/sql/insert_conflict.sql index 23d5778b82..95223bd831 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 5c52561a8a..b8c68a3e29 100644 --- a/src/test/regress/sql/with.sql +++ b/src/test/regress/sql/with.sql @@ -797,6 +797,15 @@ DELETE FROM y USING t WHERE t.a = y.a RETURNING y.a; SELECT * FROM y; +TRUNCATE TABLE y; + +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; -- -- 2.25.1