From 76485ede99a4dd4c1b2817201ac9d0da1c87a836 Mon Sep 17 00:00:00 2001 From: Suraj Kharage Date: Mon, 6 Apr 2026 15:29:28 +0530 Subject: [PATCH v6] Add support for INSERT ... SET syntax This commit adds support for INSERT ... SET syntax, which allows specifying column values using named assignments instead of requiring a separate column list and VALUES clause. Syntax: INSERT INTO table_name SET column1=value1, column2=value2, ...; This syntax provides a more convenient and readable alternative for single-row inserts, particularly when only specific columns need values. Columns not mentioned in the SET clause receive their default values or NULL, consistent with standard INSERT behavior. Features supported: - Basic syntax: INSERT INTO t SET col=val, ... - DEFAULT keyword: SET col=DEFAULT - NULL values: SET col=NULL - Expressions and functions: SET col=expr - Subqueries: SET col=(SELECT ...) - RETURNING clause: SET ... RETURNING * - ON CONFLICT: SET ... ON CONFLICT DO UPDATE/NOTHING - OVERRIDING SYSTEM VALUE: OVERRIDING SYSTEM VALUE SET ... - Multi-row syntax: SET (col1=val1, col2=val2), (col1=val3, col2=val4) - Support for different column sets in multi-row inserts - Subfield/array-element targets: SET col.field=val, SET col[n]=val, including distinct targets of the same column in one row (e.g. c.x=1, c.y=2 or arr[1]=1, arr[2]=2) For multi-row SET, an array/field indirection target (e.g. arr[1], c.x) is only supported when every row uses that exact same target. Rows that omit the target, assign the whole column instead, or use a different element/field, are rejected with a clear error, since the underlying column list is shared across all rows and can't represent different per-row shapes for the same column. --- doc/src/sgml/ref/insert.sgml | 86 +++++- src/backend/nodes/nodeFuncs.c | 2 + src/backend/parser/analyze.c | 232 ++++++++++++++++ src/backend/parser/gram.y | 64 +++++ src/bin/psql/tab-complete.in.c | 6 +- src/include/nodes/parsenodes.h | 1 + src/test/regress/expected/insert.out | 384 +++++++++++++++++++++++++++ src/test/regress/sql/insert.sql | 257 ++++++++++++++++++ 8 files changed, 1028 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/ref/insert.sgml b/doc/src/sgml/ref/insert.sgml index 121a9edcb99..c26314e6ad1 100644 --- a/doc/src/sgml/ref/insert.sgml +++ b/doc/src/sgml/ref/insert.sgml @@ -24,7 +24,9 @@ PostgreSQL documentation [ WITH [ RECURSIVE ] with_query [, ...] ] INSERT INTO table_name [ AS alias ] [ ( column_name [, ...] ) ] [ OVERRIDING { SYSTEM | USER } VALUE ] - { DEFAULT VALUES | VALUES ( { expression | DEFAULT } [, ...] ) [, ...] | query } + { DEFAULT VALUES | VALUES ( { expression | DEFAULT } [, ...] ) [, ...] | query | + SET column_name = { expression | DEFAULT } [, ...] | + SET ( column_name = { expression | DEFAULT } [, ...] ) [, ...] } [ ON CONFLICT [ conflict_target ] conflict_action ] [ RETURNING [ WITH ( { OLD | NEW } AS output_alias [, ...] ) ] { * | output_expression [ [ AS ] output_name ] } [, ...] ] @@ -65,6 +67,35 @@ INSERT INTO table_name [ AS + + As an alternative to the VALUES clause, you can use the + SET clause to specify column values using named + assignments. The SET clause has the form + SET column_name = expression, + with multiple column assignments separated by commas. This syntax is + particularly convenient when inserting a single row with values for + specific columns, as it eliminates the need to specify a separate + column list. When using SET, columns not mentioned + will receive their default values or NULL. The SET + syntax cannot be combined with an explicit column list. + + + + For multi-row inserts, the SET clause uses parentheses + to group each row's assignments: + SET (col1=val1, col2=val2), (col1=val3, col2=val4). + Each row can specify a different set of columns; columns omitted from + a particular row will receive their default value or NULL. Column names + are matched across rows by name rather than position, so the order of + assignments within each row does not need to be consistent. If a target + is qualified with a subfield name or array subscript (see + column_name below), every + row that assigns to that column must use that exact same subfield or + subscript: assigning a different subfield or element, assigning the + whole column instead, or omitting the column entirely, in some other + row of the same statement, is not supported and results in an error. + + Each column not present in the explicit or implicit column list will be filled with a default value, either its declared default value @@ -736,6 +767,59 @@ INSERT INTO films (code, title, did, date_prod, kind) VALUES + + To insert a single row using the SET syntax: + + +INSERT INTO films SET code='UA502', title='Bananas', did=105, + date_prod='1971-07-13', kind='Comedy', len='82 minutes'; + + + + + This example uses the SET syntax with + DEFAULT for some columns: + + +INSERT INTO films SET code='T_601', title='Yojimbo', did=106, + date_prod=DEFAULT, kind='Drama'; + + + + + The SET syntax can be used with expressions + and functions: + + +INSERT INTO films SET code='HG120', title=upper('the dinner game'), + did=140, date_prod=current_date, kind='Comedy'; + + + + + To insert multiple rows using the SET syntax, + enclose each row's assignments in parentheses: + + +INSERT INTO films SET + (code='B6717', title='Tampopo', did=110, date_prod='1985-02-10', kind='Comedy'), + (code='HG120', title='The Dinner Game', did=140, date_prod=DEFAULT, kind='Comedy'); + + + + + In multi-row SET syntax, each row can specify + different columns. Columns not specified in a row will use their + default values: + + +INSERT INTO films SET + (code='UA502', title='Bananas', did=105, kind='Comedy', len='82 minutes'), + (code='T_601', title='Yojimbo', did=106, kind='Drama'); +-- The second row's len and date_prod columns will receive DEFAULT values + + + This example inserts some rows into table films from a table tmp_films diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 9eb905fd025..281e1180f7f 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4302,6 +4302,8 @@ raw_expression_tree_walker_impl(Node *node, return true; if (WALK(stmt->selectStmt)) return true; + if (WALK(stmt->setClause)) + return true; if (WALK(stmt->onConflictClause)) return true; if (WALK(stmt->returningClause)) diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 14202e5cae6..d399eebfd27 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -76,6 +76,8 @@ post_parse_analyze_hook_type post_parse_analyze_hook = NULL; static Query *transformOptionalSelectInto(ParseState *pstate, Node *parseTree); static Query *transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt); static Query *transformInsertStmt(ParseState *pstate, InsertStmt *stmt); +static void transformInsertSetClause(ParseState *pstate, List *setClause, + List **cols_p, List **valuesLists_p); static OnConflictExpr *transformOnConflictClause(ParseState *pstate, OnConflictClause *onConflictClause); static ForPortionOfExpr *transformForPortionOfClause(ParseState *pstate, @@ -658,6 +660,216 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) return qry; } +/* + * transformInsertSetClause - + * Transform INSERT ... SET clause into column list and VALUES lists. + * + * This function handles both single-row and multi-row SET syntax: + * Single row: INSERT INTO t SET c1=1, c2=2 + * Multi-row: INSERT INTO t SET (c1=1, c2=2), (c1=3, c2=4) + * + * The function supports different column sets across rows. For example: + * INSERT INTO t SET (c1=1, c2=2, c3=3), (c1=4, c2=5) + * This will generate: + * - Column list: c1, c2, c3 + * - Values: (1, 2, 3), (4, 5, DEFAULT) + * + * Missing columns in any row are filled with DEFAULT. + * + * A SET target's identity is its column name *and* its indirection (the + * ".field" / "[subscript]" part, if any), matching the whole-column-vs- + * partial-column rules that checkInsertTargets() enforces later for + * ordinary column-list INSERTs: a whole-column assignment (no indirection) + * conflicts with any other assignment to the same column, but assignments + * to different subfields/elements of the same column (e.g. c.x and c.y, or + * arr[1] and arr[2]) target different things and are not duplicates. + * + * Because all rows are flattened into one shared column list (as with an + * ordinary multi-row VALUES INSERT with an explicit column list), the + * indirection expression built for a partial (subfield/subscript) target is + * applied uniformly to every row. There is therefore no way for one row's + * slot to mean "update this element" while another row's slot for the same + * shared target means "leave the column alone" (target omitted) or "replace + * the whole column" (whole-column target): both would have to silently + * become an illegal "set an element/subfield to DEFAULT", or would only be + * caught downstream as a confusing "column specified more than once". To + * avoid that, if a partial target is used at all, every row must supply + * that exact same target (same column and indirection); otherwise we raise + * a clear error here. Rows that all agree on the same indirection target + * are unaffected and continue to work, just like the equivalent multi-row + * "INSERT INTO t (id, arr[1]) VALUES ..." form. + */ +static void +transformInsertSetClause(ParseState *pstate, List *setClause, + List **cols_p, List **valuesLists_p) +{ + List *all_cols = NIL; /* List of all unique SET targets */ + List *valuesLists = NIL; + + /* + * First pass: collect all unique SET targets from all rows. We need + * to scan all rows first to determine the complete set of targets. + * Also check for conflicting targets within each row. + */ + foreach_node(List, set_clause, setClause) + { + List *row_cols = NIL; /* Targets seen in this row */ + + foreach_node(ResTarget, res, set_clause) + { + bool found = false; + + /* + * Check for a conflicting target in the same row. A + * whole-column assignment can't coexist with any other + * assignment to the same column name, but multiple partial + * (subfield/subscript) assignments to different targets of the + * same column are fine. + */ + foreach_node(ResTarget, row_col, row_cols) + { + if (strcmp(row_col->name, res->name) == 0 && + (res->indirection == NIL || row_col->indirection == NIL)) + { + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_COLUMN), + errmsg("column \"%s\" specified more than once", + res->name), + parser_errposition(pstate, res->location))); + } + } + + /* Add to this row's target list */ + row_cols = lappend(row_cols, res); + + /* + * Check if we've already seen this exact target (same column + * name and same indirection) across all rows processed so + * far. + */ + foreach_node(ResTarget, existing, all_cols) + { + if (strcmp(existing->name, res->name) == 0 && + equal(existing->indirection, res->indirection)) + { + found = true; + break; + } + } + + /* If this is a new target across all rows, add it to our list */ + if (!found) + { + ResTarget *col = makeNode(ResTarget); + + col->name = res->name; + col->indirection = res->indirection; + col->val = NULL; + col->location = res->location; + all_cols = lappend(all_cols, col); + } + } + } + + /* + * Second pass (validation): if a partial (subfield/subscript) target is + * used anywhere and there's more than one row, require every row to + * supply that exact same target. See the function comment above for + * why this can't be relaxed while still flattening all rows into one + * shared column list. + */ + if (list_length(setClause) > 1) + { + foreach_node(ResTarget, col, all_cols) + { + if (col->indirection == NIL) + continue; + + foreach_node(List, set_clause, setClause) + { + bool row_has_target = false; + + foreach_node(ResTarget, res, set_clause) + { + if (strcmp(col->name, res->name) == 0 && + equal(col->indirection, res->indirection)) + { + row_has_target = true; + break; + } + } + + if (!row_has_target) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("array/field indirection target \"%s\" must be specified identically in every row of a multi-row INSERT ... SET", + col->name), + errdetail("One or more rows omit this target, assign to the whole column instead, or use different indirection."), + parser_errposition(pstate, col->location))); + } + } + } + + /* + * Third pass: for each row, create a values list matching the target + * order from all_cols. Use DEFAULT for any target not present in this + * row. + */ + foreach_node(List, set_clause, setClause) + { + List *vals = NIL; + + /* For each target in the complete target list */ + foreach_node(ResTarget, col, all_cols) + { + Node *val = NULL; + bool found = false; + + /* + * Search for this exact target in the current row. Scan the + * whole row (rather than stopping at the first match) so that, + * if the same target is assigned more than once in a row, the + * last assignment wins, consistent with ordinary column-list + * INSERT/UPDATE SET behavior for repeated partial targets. + */ + foreach_node(ResTarget, res, set_clause) + { + if (strcmp(col->name, res->name) == 0 && + equal(col->indirection, res->indirection)) + { + val = res->val; + found = true; + } + } + + if (found) + vals = lappend(vals, val); + else + { + /* + * The target is not present in this row. Fill with + * DEFAULT. This can only happen for a whole-column target + * here: the validation pass above already guarantees that + * any partial (indirection) target is present in every + * row, so we'll never generate an illegal "set an + * element/subfield to DEFAULT" from this fallback. + */ + SetToDefault *def = makeNode(SetToDefault); + + def->location = -1; + vals = lappend(vals, def); + } + } + + /* Add this row's values to the valuesLists */ + valuesLists = lappend(valuesLists, vals); + } + + /* Return the results */ + *cols_p = all_cols; + *valuesLists_p = valuesLists; +} + /* * transformInsertStmt - * transform an Insert Statement @@ -697,6 +909,26 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt) qry->override = stmt->override; + /* + * If we have SET clause (INSERT ... SET col=val, ...), transform it + * into column list and VALUES list before further processing. + */ + if (stmt->setClause != NIL) + { + List *cols = NIL; + List *valuesLists = NIL; + + /* Transform SET clause into columns and values */ + transformInsertSetClause(pstate, stmt->setClause, &cols, &valuesLists); + + /* Create a SelectStmt with multiple VALUES rows */ + selectStmt = makeNode(SelectStmt); + selectStmt->valuesLists = valuesLists; + stmt->selectStmt = (Node *) selectStmt; + stmt->cols = cols; + stmt->setClause = NIL; /* clear it so we don't process again */ + } + /* * ON CONFLICT DO UPDATE and ON CONFLICT DO SELECT FOR UPDATE/SHARE * require UPDATE permission on the target relation. diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 1015d303fbf..edcfdfdc340 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -560,6 +560,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type for_portion_of_clause %type tablesample_clause opt_repeatable_clause %type target_el set_target insert_column_item +%type insert_set_clause insert_set_clause_list +%type insert_set_clause_group insert_set_clause_group_list %type generic_option_name %type generic_option_arg @@ -12570,6 +12572,36 @@ insert_rest: $$->cols = NIL; $$->selectStmt = NULL; } + | OVERRIDING override_kind VALUE_P SET insert_set_clause_list + { + $$ = makeNode(InsertStmt); + $$->cols = NIL; + $$->selectStmt = NULL; + $$->override = $2; + $$->setClause = list_make1($5); + } + | OVERRIDING override_kind VALUE_P SET insert_set_clause_group_list + { + $$ = makeNode(InsertStmt); + $$->cols = NIL; + $$->selectStmt = NULL; + $$->override = $2; + $$->setClause = $5; + } + | SET insert_set_clause_group_list + { + $$ = makeNode(InsertStmt); + $$->cols = NIL; + $$->selectStmt = NULL; + $$->setClause = $2; + } + | SET insert_set_clause_list + { + $$ = makeNode(InsertStmt); + $$->cols = NIL; + $$->selectStmt = NULL; + $$->setClause = list_make1($2); + } ; override_kind: @@ -12880,6 +12912,38 @@ set_target_list: | set_target_list ',' set_target { $$ = lappend($1,$3); } ; +/* + * Grammar rules for INSERT ... SET syntax + * Supports both single-row and multi-row syntax: + * Single row: INSERT INTO table SET col1=val1, col2=val2 + * Multi-row: INSERT INTO table SET (col1=val1, col2=val2), (col1=val3, col2=val4) + * + * These rules are INSERT-specific and only allow simple column=value assignments. + */ +insert_set_clause: + set_target '=' a_expr + { + $1->val = (Node *) $3; + $$ = list_make1($1); + } + ; + +insert_set_clause_list: + insert_set_clause { $$ = $1; } + | insert_set_clause_list ',' insert_set_clause + { $$ = list_concat($1, $3); } + ; + +insert_set_clause_group: + '(' insert_set_clause_list ')' { $$ = $2; } + ; + +insert_set_clause_group_list: + insert_set_clause_group { $$ = list_make1($1); } + | insert_set_clause_group_list ',' insert_set_clause_group + { $$ = lappend($1, $3); } + ; + /***************************************************************************** * diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 8e1a1d69740..2145152a003 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -4858,10 +4858,10 @@ match_previous_words(int pattern_id, /* * Complete INSERT INTO with "(" or "VALUES" or "SELECT" or - * "TABLE" or "DEFAULT VALUES" or "OVERRIDING" + * "TABLE" or "DEFAULT VALUES" or "OVERRIDING" or "SET" */ else if (TailMatches("INSERT", "INTO", MatchAny)) - COMPLETE_WITH("(", "DEFAULT VALUES", "SELECT", "TABLE", "VALUES", "OVERRIDING"); + COMPLETE_WITH("(", "DEFAULT VALUES", "SELECT", "SET", "TABLE", "VALUES", "OVERRIDING"); /* * Complete INSERT INTO
(attribs) with "VALUES" or "SELECT" or @@ -4877,7 +4877,7 @@ match_previous_words(int pattern_id, /* Complete after OVERRIDING clause */ else if (TailMatches("OVERRIDING", MatchAny, "VALUE")) - COMPLETE_WITH("SELECT", "TABLE", "VALUES"); + COMPLETE_WITH("SELECT", "SET", "TABLE", "VALUES"); /* Insert an open parenthesis after "VALUES" */ else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES")) diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 910eef936f1..7a870f85dd3 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2174,6 +2174,7 @@ typedef struct InsertStmt ReturningClause *returningClause; /* RETURNING clause */ WithClause *withClause; /* WITH clause */ OverridingKind override; /* OVERRIDING clause */ + List *setClause; /* SET clause (for INSERT ... SET syntax) */ } InsertStmt; /* ---------------------- diff --git a/src/test/regress/expected/insert.out b/src/test/regress/expected/insert.out index 75b8de79fce..b8f200c1c5a 100644 --- a/src/test/regress/expected/insert.out +++ b/src/test/regress/expected/insert.out @@ -1096,3 +1096,387 @@ insert into returningwrtest values (2, 'foo') returning returningwrtest; (1 row) drop table returningwrtest; +-- +-- INSERT ... SET syntax tests +-- +create table insertsettest ( + id int, + name text, + salary int default 50000, + dept text, + created_at timestamp default now() +); +-- Basic INSERT SET syntax +insert into insertsettest set id=1, name='Alice', salary=60000, dept='Engineering'; +insert into insertsettest set name='Bob', id=2, dept='Sales', salary=55000; +-- INSERT SET with DEFAULT keyword +insert into insertsettest set id=3, name='Charlie', salary=DEFAULT, dept='HR'; +insert into insertsettest set id=4, name='David', dept='Marketing'; -- salary should use default +-- INSERT SET with NULL values +insert into insertsettest set id=5, name='Eve', salary=NULL, dept='Finance'; +insert into insertsettest set id=6, name=NULL, salary=70000, dept='IT'; +-- INSERT SET with expressions +insert into insertsettest set id=7, name='Frank', salary=50000+10000, dept='Engineering'; +insert into insertsettest set id=8, name=upper('grace'), salary=45000, dept=lower('SALES'); +-- INSERT SET with functions +insert into insertsettest set id=9, name=concat('John', ' ', 'Doe'), salary=80000, dept='Executive'; +-- INSERT SET with subqueries +insert into insertsettest set id=10, name='Kate', salary=(select max(salary) + 5000 from insertsettest), dept='Engineering'; +-- INSERT SET with column subset (others should be NULL or DEFAULT) +insert into insertsettest set id=11, name='Laura'; +insert into insertsettest set id=12, dept='Support'; +-- Verify all inserts +select id, name, salary, dept from insertsettest order by id; + id | name | salary | dept +----+----------+--------+------------- + 1 | Alice | 60000 | Engineering + 2 | Bob | 55000 | Sales + 3 | Charlie | 50000 | HR + 4 | David | 50000 | Marketing + 5 | Eve | | Finance + 6 | | 70000 | IT + 7 | Frank | 60000 | Engineering + 8 | GRACE | 45000 | sales + 9 | John Doe | 80000 | Executive + 10 | Kate | 85000 | Engineering + 11 | Laura | 50000 | + 12 | | 50000 | Support +(12 rows) + +-- INSERT SET with RETURNING clause +insert into insertsettest set id=13, name='Mike', salary=90000, dept='Management' returning id, name, salary, dept; + id | name | salary | dept +----+------+--------+------------ + 13 | Mike | 90000 | Management +(1 row) + +insert into insertsettest set id=14, name='Nancy', salary=95000, dept='Executive' returning id, name, salary; + id | name | salary +----+-------+-------- + 14 | Nancy | 95000 +(1 row) + +-- INSERT SET with ON CONFLICT DO UPDATE +create table insertsetpk ( + id int primary key, + value text, + counter int default 0 +); +insert into insertsetpk set id=1, value='first', counter=1; +insert into insertsetpk set id=2, value='second', counter=2; +-- Test ON CONFLICT DO UPDATE with INSERT SET +insert into insertsetpk set id=1, value='updated', counter=10 + on conflict (id) do update set value=excluded.value, counter=excluded.counter; +insert into insertsetpk set id=2, value='also updated', counter=20 + on conflict (id) do update set counter=insertsetpk.counter + excluded.counter; +select * from insertsetpk order by id; + id | value | counter +----+---------+--------- + 1 | updated | 10 + 2 | second | 22 +(2 rows) + +-- Test ON CONFLICT DO NOTHING with INSERT SET +insert into insertsetpk set id=1, value='ignored', counter=100 + on conflict (id) do nothing; +select * from insertsetpk order by id; + id | value | counter +----+---------+--------- + 1 | updated | 10 + 2 | second | 22 +(2 rows) + +-- INSERT SET with OVERRIDING SYSTEM VALUE (for generated columns) +create table insertsetgen ( + id int generated always as identity, + data text +); +-- This should fail (can't override without OVERRIDING clause) +insert into insertsetgen set id=100, data='test'; +ERROR: cannot insert a non-DEFAULT value into column "id" +DETAIL: Column "id" is an identity column defined as GENERATED ALWAYS. +HINT: Use OVERRIDING SYSTEM VALUE to override. +-- This should work +insert into insertsetgen overriding system value set id=100, data='test'; +insert into insertsetgen set data='auto-generated'; +select * from insertsetgen order by id; + id | data +-----+---------------- + 1 | auto-generated + 100 | test +(2 rows) + +drop table insertsetgen; +-- INSERT SET with CHECK constraints +create table insertsetcheck ( + id int, + age int check (age >= 0 and age <= 150), + score int check (score between 0 and 100) +); +insert into insertsetcheck set id=1, age=25, score=85; +insert into insertsetcheck set id=2, age=30, score=92; +-- These should fail +insert into insertsetcheck set id=3, age=-5, score=50; -- age check fails +ERROR: new row for relation "insertsetcheck" violates check constraint "insertsetcheck_age_check" +DETAIL: Failing row contains (3, -5, 50). +insert into insertsetcheck set id=4, age=25, score=150; -- score check fails +ERROR: new row for relation "insertsetcheck" violates check constraint "insertsetcheck_score_check" +DETAIL: Failing row contains (4, 25, 150). +select * from insertsetcheck order by id; + id | age | score +----+-----+------- + 1 | 25 | 85 + 2 | 30 | 92 +(2 rows) + +drop table insertsetcheck; +-- INSERT SET with partitioned tables +create table insertsetpart ( + id int, + category text, + value int +) partition by list (category); +create table insertsetpart_a partition of insertsetpart for values in ('A'); +create table insertsetpart_b partition of insertsetpart for values in ('B'); +create table insertsetpart_c partition of insertsetpart for values in ('C'); +insert into insertsetpart set id=1, category='A', value=100; +insert into insertsetpart set id=2, category='B', value=200; +insert into insertsetpart set id=3, category='C', value=300; +insert into insertsetpart set id=4, category='A', value=150; +select tableoid::regclass, * from insertsetpart order by id; + tableoid | id | category | value +-----------------+----+----------+------- + insertsetpart_a | 1 | A | 100 + insertsetpart_b | 2 | B | 200 + insertsetpart_c | 3 | C | 300 + insertsetpart_a | 4 | A | 150 +(4 rows) + +drop table insertsetpart; +-- INSERT SET with inheritance +create table insertsetparent ( + id int, + parent_col text +); +create table insertsetchild ( + child_col text +) inherits (insertsetparent); +insert into insertsetparent set id=1, parent_col='parent data'; +insert into insertsetchild set id=2, parent_col='from child', child_col='child data'; +select * from insertsetparent order by id; + id | parent_col +----+------------- + 1 | parent data + 2 | from child +(2 rows) + +select * from insertsetchild; + id | parent_col | child_col +----+------------+------------ + 2 | from child | child data +(1 row) + +drop table insertsetchild; +drop table insertsetparent; +-- INSERT SET error cases +-- Duplicate column names (should fail) +insert into insertsettest set id=15, name='Test', id=16; +ERROR: column "id" specified more than once +LINE 1: insert into insertsettest set id=15, name='Test', id=16; + ^ +-- Non-existent column (should fail) +insert into insertsettest set id=15, nonexistent='value'; +ERROR: column "nonexistent" of relation "insertsettest" does not exist +LINE 1: insert into insertsettest set id=15, nonexistent='value'; + ^ +-- Type mismatch (should fail) +insert into insertsettest set id='not a number', name='Test'; +ERROR: invalid input syntax for type integer: "not a number" +LINE 1: insert into insertsettest set id='not a number', name='Test'... + ^ +-- Multi-column assignment syntax (should fail - this is UPDATE syntax) +insert into insertsettest set (id, name) = (20, 'Test'); +ERROR: syntax error at or near "," +LINE 1: insert into insertsettest set (id, name) = (20, 'Test'); + ^ +-- Multi-column assignment with subquery (should fail) +insert into insertsettest set (id, name) = (select 21, 'Test'); +ERROR: syntax error at or near "," +LINE 1: insert into insertsettest set (id, name) = (select 21, 'Test... + ^ +-- INSERT SET with CTE +with new_values as ( + select 15 as new_id, 'Oliver' as new_name, 85000 as new_salary +) +insert into insertsettest +select new_id, new_name, new_salary, 'Sales' from new_values; +-- Verify CTE insert worked (not using SET syntax, but for completeness) +select id, name, salary, dept from insertsettest where id = 15; + id | name | salary | dept +----+--------+--------+------- + 15 | Oliver | 85000 | Sales +(1 row) + +-- Multi-row INSERT SET syntax +create table insertsetmulti ( + c1 int, + c2 int, + c3 int +); +-- Basic multi-row with same column order +insert into insertsetmulti set (c1=1, c2=2, c3=3), (c1=4, c2=5, c3=6); +select * from insertsetmulti order by c1; + c1 | c2 | c3 +----+----+---- + 1 | 2 | 3 + 4 | 5 | 6 +(2 rows) + +-- Multi-row with different column orders +-- This tests that column-value matching works correctly across rows +insert into insertsetmulti set (c2=20, c1=10, c3=30), (c1=40, c3=60, c2=50), (c3=90, c2=80, c1=70); +select * from insertsetmulti order by c1; + c1 | c2 | c3 +----+----+---- + 1 | 2 | 3 + 4 | 5 | 6 + 10 | 20 | 30 + 40 | 50 | 60 + 70 | 80 | 90 +(5 rows) + +-- Multi-row with mixed expressions +insert into insertsetmulti set (c1=100, c2=200, c3=300), (c2=500, c1=400, c3=600); +select * from insertsetmulti order by c1; + c1 | c2 | c3 +-----+-----+----- + 1 | 2 | 3 + 4 | 5 | 6 + 10 | 20 | 30 + 40 | 50 | 60 + 70 | 80 | 90 + 100 | 200 | 300 + 400 | 500 | 600 +(7 rows) + +-- Test different column sets in multi-row INSERT SET +-- First row has all columns, second row has subset (c3 should get DEFAULT/NULL) +insert into insertsetmulti set (c1=1000, c2=2000, c3=3000), (c1=4000, c2=5000); +select * from insertsetmulti where c1 >= 1000 order by c1; + c1 | c2 | c3 +------+------+------ + 1000 | 2000 | 3000 + 4000 | 5000 | +(2 rows) + +-- First row has subset, second row has all columns (c2 in first row should get DEFAULT/NULL) +insert into insertsetmulti set (c1=1001, c3=3001), (c1=4001, c2=5001, c3=6001); +select * from insertsetmulti where c1 >= 1001 order by c1; + c1 | c2 | c3 +------+------+------ + 1001 | | 3001 + 4000 | 5000 | + 4001 | 5001 | 6001 +(3 rows) + +-- Different subsets in each row (union of all columns used, missing get DEFAULT/NULL) +insert into insertsetmulti set (c1=1002, c2=2002), (c1=4002, c3=6002); +select * from insertsetmulti where c1 >= 1002 order by c1; + c1 | c2 | c3 +------+------+------ + 1002 | 2002 | + 4000 | 5000 | + 4001 | 5001 | 6001 + 4002 | | 6002 +(4 rows) + +-- INSERT SET targeting different subfields/elements of the same column +-- (these are distinct targets, not duplicate column assignments) +create type insertset_comp as (x int, y int); +create table insertsetfield (id int primary key, c insertset_comp); +create table insertsetarr (id int primary key, arr int[]); +-- Different subfields of the same composite column +insert into insertsetfield set id=1, c.x=7, c.y=8; +select * from insertsetfield order by id; + id | c +----+------- + 1 | (7,8) +(1 row) + +-- Different elements of the same array column +insert into insertsetarr set id=1, arr[1]=30, arr[2]=40; +select * from insertsetarr order by id; + id | arr +----+--------- + 1 | {30,40} +(1 row) + +-- A whole-column assignment still conflicts with a partial assignment of +-- the same column (should fail) +insert into insertsetfield set id=2, c=row(1,2), c.x=9; +ERROR: column "c" specified more than once +LINE 1: insert into insertsetfield set id=2, c=row(1,2), c.x=9; + ^ +-- Multi-row SET where rows target different elements of the same array +-- column can't be represented by the shared column list used for +-- multi-row SET (same limitation as INSERT ... (col-list) VALUES), so it +-- must be rejected rather than silently misassigning values (should fail) +insert into insertsetarr set (id=2, arr[1]=111), (id=3, arr[2]=222); +ERROR: array/field indirection target "arr" must be specified identically in every row of a multi-row INSERT ... SET +LINE 1: insert into insertsetarr set (id=2, arr[1]=111), (id=3, arr[... + ^ +DETAIL: One or more rows omit this target, assign to the whole column instead, or use different indirection. +select * from insertsetarr order by id; + id | arr +----+--------- + 1 | {30,40} +(1 row) + +-- Multi-row SET where rows target the same element of the same array +-- column works fine, just like the equivalent column-list VALUES form +insert into insertsetarr set (id=4, arr[1]=1), (id=5, arr[1]=2); +select * from insertsetarr order by id; + id | arr +----+--------- + 1 | {30,40} + 4 | {1} + 5 | {2} +(3 rows) + +-- A row that omits an indirection-targeted column entirely, while other +-- rows in the same statement target an element/subfield of it, can't be +-- represented either: the shared target's expression is applied uniformly +-- to every row, so this row's "don't touch this column" can't be +-- distinguished from "set this element to its default" (which is itself +-- illegal). Must be rejected (should fail). +create table insertsetarrdef (id int primary key, arr int[] default array[1]); +insert into insertsetarrdef set (id=70, arr[1]=1), (id=71, arr[1]=2), (id=72); +ERROR: array/field indirection target "arr" must be specified identically in every row of a multi-row INSERT ... SET +LINE 1: insert into insertsetarrdef set (id=70, arr[1]=1), (id=71, a... + ^ +DETAIL: One or more rows omit this target, assign to the whole column instead, or use different indirection. +select * from insertsetarrdef order by id; + id | arr +----+----- +(0 rows) + +-- Likewise, a row assigning the whole column (even to DEFAULT) can't be +-- mixed with another row that targets one of its elements (should fail) +insert into insertsetarrdef set (id=1, arr[1]=1), (id=2, arr=default); +ERROR: array/field indirection target "arr" must be specified identically in every row of a multi-row INSERT ... SET +LINE 1: insert into insertsetarrdef set (id=1, arr[1]=1), (id=2, arr... + ^ +DETAIL: One or more rows omit this target, assign to the whole column instead, or use different indirection. +select * from insertsetarrdef order by id; + id | arr +----+----- +(0 rows) + +drop table insertsetfield; +drop table insertsetarr; +drop table insertsetarrdef; +drop type insertset_comp; +-- Cleanup +drop table insertsettest; +drop table insertsetpk; +drop table insertsetmulti; diff --git a/src/test/regress/sql/insert.sql b/src/test/regress/sql/insert.sql index 2b086eeb6d7..147e2b2faec 100644 --- a/src/test/regress/sql/insert.sql +++ b/src/test/regress/sql/insert.sql @@ -674,3 +674,260 @@ alter table returningwrtest2 drop c; alter table returningwrtest attach partition returningwrtest2 for values in (2); insert into returningwrtest values (2, 'foo') returning returningwrtest; drop table returningwrtest; + +-- +-- INSERT ... SET syntax tests +-- +create table insertsettest ( + id int, + name text, + salary int default 50000, + dept text, + created_at timestamp default now() +); + +-- Basic INSERT SET syntax +insert into insertsettest set id=1, name='Alice', salary=60000, dept='Engineering'; +insert into insertsettest set name='Bob', id=2, dept='Sales', salary=55000; + +-- INSERT SET with DEFAULT keyword +insert into insertsettest set id=3, name='Charlie', salary=DEFAULT, dept='HR'; +insert into insertsettest set id=4, name='David', dept='Marketing'; -- salary should use default + +-- INSERT SET with NULL values +insert into insertsettest set id=5, name='Eve', salary=NULL, dept='Finance'; +insert into insertsettest set id=6, name=NULL, salary=70000, dept='IT'; + +-- INSERT SET with expressions +insert into insertsettest set id=7, name='Frank', salary=50000+10000, dept='Engineering'; +insert into insertsettest set id=8, name=upper('grace'), salary=45000, dept=lower('SALES'); + +-- INSERT SET with functions +insert into insertsettest set id=9, name=concat('John', ' ', 'Doe'), salary=80000, dept='Executive'; + +-- INSERT SET with subqueries +insert into insertsettest set id=10, name='Kate', salary=(select max(salary) + 5000 from insertsettest), dept='Engineering'; + +-- INSERT SET with column subset (others should be NULL or DEFAULT) +insert into insertsettest set id=11, name='Laura'; +insert into insertsettest set id=12, dept='Support'; + +-- Verify all inserts +select id, name, salary, dept from insertsettest order by id; + +-- INSERT SET with RETURNING clause +insert into insertsettest set id=13, name='Mike', salary=90000, dept='Management' returning id, name, salary, dept; +insert into insertsettest set id=14, name='Nancy', salary=95000, dept='Executive' returning id, name, salary; + +-- INSERT SET with ON CONFLICT DO UPDATE +create table insertsetpk ( + id int primary key, + value text, + counter int default 0 +); + +insert into insertsetpk set id=1, value='first', counter=1; +insert into insertsetpk set id=2, value='second', counter=2; + +-- Test ON CONFLICT DO UPDATE with INSERT SET +insert into insertsetpk set id=1, value='updated', counter=10 + on conflict (id) do update set value=excluded.value, counter=excluded.counter; + +insert into insertsetpk set id=2, value='also updated', counter=20 + on conflict (id) do update set counter=insertsetpk.counter + excluded.counter; + +select * from insertsetpk order by id; + +-- Test ON CONFLICT DO NOTHING with INSERT SET +insert into insertsetpk set id=1, value='ignored', counter=100 + on conflict (id) do nothing; + +select * from insertsetpk order by id; + +-- INSERT SET with OVERRIDING SYSTEM VALUE (for generated columns) +create table insertsetgen ( + id int generated always as identity, + data text +); + +-- This should fail (can't override without OVERRIDING clause) +insert into insertsetgen set id=100, data='test'; + +-- This should work +insert into insertsetgen overriding system value set id=100, data='test'; +insert into insertsetgen set data='auto-generated'; + +select * from insertsetgen order by id; + +drop table insertsetgen; + +-- INSERT SET with CHECK constraints +create table insertsetcheck ( + id int, + age int check (age >= 0 and age <= 150), + score int check (score between 0 and 100) +); + +insert into insertsetcheck set id=1, age=25, score=85; +insert into insertsetcheck set id=2, age=30, score=92; + +-- These should fail +insert into insertsetcheck set id=3, age=-5, score=50; -- age check fails +insert into insertsetcheck set id=4, age=25, score=150; -- score check fails + +select * from insertsetcheck order by id; + +drop table insertsetcheck; + +-- INSERT SET with partitioned tables +create table insertsetpart ( + id int, + category text, + value int +) partition by list (category); + +create table insertsetpart_a partition of insertsetpart for values in ('A'); +create table insertsetpart_b partition of insertsetpart for values in ('B'); +create table insertsetpart_c partition of insertsetpart for values in ('C'); + +insert into insertsetpart set id=1, category='A', value=100; +insert into insertsetpart set id=2, category='B', value=200; +insert into insertsetpart set id=3, category='C', value=300; +insert into insertsetpart set id=4, category='A', value=150; + +select tableoid::regclass, * from insertsetpart order by id; + +drop table insertsetpart; + +-- INSERT SET with inheritance +create table insertsetparent ( + id int, + parent_col text +); + +create table insertsetchild ( + child_col text +) inherits (insertsetparent); + +insert into insertsetparent set id=1, parent_col='parent data'; +insert into insertsetchild set id=2, parent_col='from child', child_col='child data'; + +select * from insertsetparent order by id; +select * from insertsetchild; + +drop table insertsetchild; +drop table insertsetparent; + +-- INSERT SET error cases +-- Duplicate column names (should fail) +insert into insertsettest set id=15, name='Test', id=16; + +-- Non-existent column (should fail) +insert into insertsettest set id=15, nonexistent='value'; + +-- Type mismatch (should fail) +insert into insertsettest set id='not a number', name='Test'; + +-- Multi-column assignment syntax (should fail - this is UPDATE syntax) +insert into insertsettest set (id, name) = (20, 'Test'); + +-- Multi-column assignment with subquery (should fail) +insert into insertsettest set (id, name) = (select 21, 'Test'); + +-- INSERT SET with CTE +with new_values as ( + select 15 as new_id, 'Oliver' as new_name, 85000 as new_salary +) +insert into insertsettest +select new_id, new_name, new_salary, 'Sales' from new_values; + +-- Verify CTE insert worked (not using SET syntax, but for completeness) +select id, name, salary, dept from insertsettest where id = 15; + +-- Multi-row INSERT SET syntax +create table insertsetmulti ( + c1 int, + c2 int, + c3 int +); + +-- Basic multi-row with same column order +insert into insertsetmulti set (c1=1, c2=2, c3=3), (c1=4, c2=5, c3=6); +select * from insertsetmulti order by c1; + +-- Multi-row with different column orders +-- This tests that column-value matching works correctly across rows +insert into insertsetmulti set (c2=20, c1=10, c3=30), (c1=40, c3=60, c2=50), (c3=90, c2=80, c1=70); +select * from insertsetmulti order by c1; + +-- Multi-row with mixed expressions +insert into insertsetmulti set (c1=100, c2=200, c3=300), (c2=500, c1=400, c3=600); +select * from insertsetmulti order by c1; + +-- Test different column sets in multi-row INSERT SET +-- First row has all columns, second row has subset (c3 should get DEFAULT/NULL) +insert into insertsetmulti set (c1=1000, c2=2000, c3=3000), (c1=4000, c2=5000); +select * from insertsetmulti where c1 >= 1000 order by c1; + +-- First row has subset, second row has all columns (c2 in first row should get DEFAULT/NULL) +insert into insertsetmulti set (c1=1001, c3=3001), (c1=4001, c2=5001, c3=6001); +select * from insertsetmulti where c1 >= 1001 order by c1; + +-- Different subsets in each row (union of all columns used, missing get DEFAULT/NULL) +insert into insertsetmulti set (c1=1002, c2=2002), (c1=4002, c3=6002); +select * from insertsetmulti where c1 >= 1002 order by c1; + +-- INSERT SET targeting different subfields/elements of the same column +-- (these are distinct targets, not duplicate column assignments) +create type insertset_comp as (x int, y int); +create table insertsetfield (id int primary key, c insertset_comp); +create table insertsetarr (id int primary key, arr int[]); + +-- Different subfields of the same composite column +insert into insertsetfield set id=1, c.x=7, c.y=8; +select * from insertsetfield order by id; + +-- Different elements of the same array column +insert into insertsetarr set id=1, arr[1]=30, arr[2]=40; +select * from insertsetarr order by id; + +-- A whole-column assignment still conflicts with a partial assignment of +-- the same column (should fail) +insert into insertsetfield set id=2, c=row(1,2), c.x=9; + +-- Multi-row SET where rows target different elements of the same array +-- column can't be represented by the shared column list used for +-- multi-row SET (same limitation as INSERT ... (col-list) VALUES), so it +-- must be rejected rather than silently misassigning values (should fail) +insert into insertsetarr set (id=2, arr[1]=111), (id=3, arr[2]=222); +select * from insertsetarr order by id; + +-- Multi-row SET where rows target the same element of the same array +-- column works fine, just like the equivalent column-list VALUES form +insert into insertsetarr set (id=4, arr[1]=1), (id=5, arr[1]=2); +select * from insertsetarr order by id; + +-- A row that omits an indirection-targeted column entirely, while other +-- rows in the same statement target an element/subfield of it, can't be +-- represented either: the shared target's expression is applied uniformly +-- to every row, so this row's "don't touch this column" can't be +-- distinguished from "set this element to its default" (which is itself +-- illegal). Must be rejected (should fail). +create table insertsetarrdef (id int primary key, arr int[] default array[1]); +insert into insertsetarrdef set (id=70, arr[1]=1), (id=71, arr[1]=2), (id=72); +select * from insertsetarrdef order by id; + +-- Likewise, a row assigning the whole column (even to DEFAULT) can't be +-- mixed with another row that targets one of its elements (should fail) +insert into insertsetarrdef set (id=1, arr[1]=1), (id=2, arr=default); +select * from insertsetarrdef order by id; + +drop table insertsetfield; +drop table insertsetarr; +drop table insertsetarrdef; +drop type insertset_comp; + +-- Cleanup +drop table insertsettest; +drop table insertsetpk; +drop table insertsetmulti; -- 2.47.3