v11-0002-ON-CONFLICT-DO-SELCT-Fixes-after-review.patch
application/octet-stream
Filename: v11-0002-ON-CONFLICT-DO-SELCT-Fixes-after-review.patch
Type: application/octet-stream
Part: 1
Message:
Re: ON CONFLICT DO SELECT (take 3)
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch v11-0002
Subject: ON CONFLICT DO SELCT - Fixes after review
| File | + | − |
|---|---|---|
| contrib/pgrowlocks/expected/on-conflict-do-select.out | 80 | 0 |
| contrib/pgrowlocks/Makefile | 1 | 1 |
| contrib/pgrowlocks/meson.build | 1 | 0 |
| contrib/pgrowlocks/specs/on-conflict-do-select.spec | 39 | 0 |
| src/backend/optimizer/util/plancat.c | 8 | 2 |
| src/backend/rewrite/rewriteHandler.c | 21 | 18 |
| src/include/nodes/primnodes.h | 3 | 3 |
| src/test/regress/expected/constraints.out | 7 | 1 |
| src/test/regress/expected/rowsecurity.out | 49 | 1 |
| src/test/regress/expected/updatable_views.out | 31 | 0 |
| src/test/regress/sql/constraints.sql | 6 | 1 |
| src/test/regress/sql/rowsecurity.sql | 43 | 2 |
| src/test/regress/sql/updatable_views.sql | 8 | 0 |
From 77a92ccba1dba02c62cbad86b56992fdda292b17 Mon Sep 17 00:00:00 2001
From: Viktor Holmberg <v@viktorh.net>
Date: Mon, 17 Nov 2025 14:10:57 +0100
Subject: [PATCH v11 2/2] ON CONFLICT DO SELCT - Fixes after review
- updatable views fixed + tested
- comments
- (more) tests for RLS
- tests and fixes for exclusion constraints
---
contrib/pgrowlocks/Makefile | 2 +-
.../expected/on-conflict-do-select.out | 80 +++++++++++++++++++
contrib/pgrowlocks/meson.build | 1 +
.../specs/on-conflict-do-select.spec | 39 +++++++++
src/backend/optimizer/util/plancat.c | 10 ++-
src/backend/rewrite/rewriteHandler.c | 39 ++++-----
src/include/nodes/primnodes.h | 6 +-
src/test/regress/expected/constraints.out | 8 +-
src/test/regress/expected/rowsecurity.out | 50 +++++++++++-
src/test/regress/expected/updatable_views.out | 31 +++++++
src/test/regress/sql/constraints.sql | 7 +-
src/test/regress/sql/rowsecurity.sql | 45 ++++++++++-
src/test/regress/sql/updatable_views.sql | 8 ++
13 files changed, 297 insertions(+), 29 deletions(-)
create mode 100644 contrib/pgrowlocks/expected/on-conflict-do-select.out
create mode 100644 contrib/pgrowlocks/specs/on-conflict-do-select.spec
diff --git a/contrib/pgrowlocks/Makefile b/contrib/pgrowlocks/Makefile
index e8080646643..a1e25b101a9 100644
--- a/contrib/pgrowlocks/Makefile
+++ b/contrib/pgrowlocks/Makefile
@@ -9,7 +9,7 @@ EXTENSION = pgrowlocks
DATA = pgrowlocks--1.2.sql pgrowlocks--1.1--1.2.sql pgrowlocks--1.0--1.1.sql
PGFILEDESC = "pgrowlocks - display row locking information"
-ISOLATION = pgrowlocks
+ISOLATION = pgrowlocks on-conflict-do-select
ISOLATION_OPTS = --load-extension=pgrowlocks
ifdef USE_PGXS
diff --git a/contrib/pgrowlocks/expected/on-conflict-do-select.out b/contrib/pgrowlocks/expected/on-conflict-do-select.out
new file mode 100644
index 00000000000..0bafa556844
--- /dev/null
+++ b/contrib/pgrowlocks/expected/on-conflict-do-select.out
@@ -0,0 +1,80 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_doselect_nolock s2_rowlocks s1_rollback
+step s1_begin: BEGIN;
+step s1_doselect_nolock: INSERT INTO conflict_test VALUES (1, 'new') ON CONFLICT (key) DO SELECT RETURNING *;
+key|val
+---+--------
+ 1|original
+(1 row)
+
+step s2_rowlocks: SELECT locked_row, multi, modes FROM pgrowlocks('conflict_test');
+locked_row|multi|modes
+----------+-----+-----
+(0 rows)
+
+step s1_rollback: ROLLBACK;
+
+starting permutation: s1_begin s1_doselect_keyshare s2_rowlocks s1_rollback
+step s1_begin: BEGIN;
+step s1_doselect_keyshare: INSERT INTO conflict_test VALUES (1, 'new') ON CONFLICT (key) DO SELECT FOR KEY SHARE RETURNING *;
+key|val
+---+--------
+ 1|original
+(1 row)
+
+step s2_rowlocks: SELECT locked_row, multi, modes FROM pgrowlocks('conflict_test');
+locked_row|multi|modes
+----------+-----+-----------------
+(0,1) |f |{"For Key Share"}
+(1 row)
+
+step s1_rollback: ROLLBACK;
+
+starting permutation: s1_begin s1_doselect_share s2_rowlocks s1_rollback
+step s1_begin: BEGIN;
+step s1_doselect_share: INSERT INTO conflict_test VALUES (1, 'new') ON CONFLICT (key) DO SELECT FOR SHARE RETURNING *;
+key|val
+---+--------
+ 1|original
+(1 row)
+
+step s2_rowlocks: SELECT locked_row, multi, modes FROM pgrowlocks('conflict_test');
+locked_row|multi|modes
+----------+-----+-------------
+(0,1) |f |{"For Share"}
+(1 row)
+
+step s1_rollback: ROLLBACK;
+
+starting permutation: s1_begin s1_doselect_nokeyupd s2_rowlocks s1_rollback
+step s1_begin: BEGIN;
+step s1_doselect_nokeyupd: INSERT INTO conflict_test VALUES (1, 'new') ON CONFLICT (key) DO SELECT FOR NO KEY UPDATE RETURNING *;
+key|val
+---+--------
+ 1|original
+(1 row)
+
+step s2_rowlocks: SELECT locked_row, multi, modes FROM pgrowlocks('conflict_test');
+locked_row|multi|modes
+----------+-----+---------------------
+(0,1) |f |{"For No Key Update"}
+(1 row)
+
+step s1_rollback: ROLLBACK;
+
+starting permutation: s1_begin s1_doselect_update s2_rowlocks s1_rollback
+step s1_begin: BEGIN;
+step s1_doselect_update: INSERT INTO conflict_test VALUES (1, 'new') ON CONFLICT (key) DO SELECT FOR UPDATE RETURNING *;
+key|val
+---+--------
+ 1|original
+(1 row)
+
+step s2_rowlocks: SELECT locked_row, multi, modes FROM pgrowlocks('conflict_test');
+locked_row|multi|modes
+----------+-----+--------------
+(0,1) |f |{"For Update"}
+(1 row)
+
+step s1_rollback: ROLLBACK;
diff --git a/contrib/pgrowlocks/meson.build b/contrib/pgrowlocks/meson.build
index 6007a76ae75..7ebeae55395 100644
--- a/contrib/pgrowlocks/meson.build
+++ b/contrib/pgrowlocks/meson.build
@@ -31,6 +31,7 @@ tests += {
'isolation': {
'specs': [
'pgrowlocks',
+ 'on-conflict-do-select',
],
'regress_args': ['--load-extension=pgrowlocks'],
},
diff --git a/contrib/pgrowlocks/specs/on-conflict-do-select.spec b/contrib/pgrowlocks/specs/on-conflict-do-select.spec
new file mode 100644
index 00000000000..bbd571f4c21
--- /dev/null
+++ b/contrib/pgrowlocks/specs/on-conflict-do-select.spec
@@ -0,0 +1,39 @@
+# Tests for ON CONFLICT DO SELECT with row-level locking
+
+setup
+{
+ CREATE TABLE conflict_test (key int PRIMARY KEY, val text);
+ INSERT INTO conflict_test VALUES (1, 'original');
+}
+
+teardown
+{
+ DROP TABLE conflict_test;
+}
+
+session s1
+step s1_begin { BEGIN; }
+step s1_doselect_nolock { INSERT INTO conflict_test VALUES (1, 'new') ON CONFLICT (key) DO SELECT RETURNING *; }
+step s1_doselect_keyshare { INSERT INTO conflict_test VALUES (1, 'new') ON CONFLICT (key) DO SELECT FOR KEY SHARE RETURNING *; }
+step s1_doselect_share { INSERT INTO conflict_test VALUES (1, 'new') ON CONFLICT (key) DO SELECT FOR SHARE RETURNING *; }
+step s1_doselect_nokeyupd { INSERT INTO conflict_test VALUES (1, 'new') ON CONFLICT (key) DO SELECT FOR NO KEY UPDATE RETURNING *; }
+step s1_doselect_update { INSERT INTO conflict_test VALUES (1, 'new') ON CONFLICT (key) DO SELECT FOR UPDATE RETURNING *; }
+step s1_rollback { ROLLBACK; }
+
+session s2
+step s2_rowlocks { SELECT locked_row, multi, modes FROM pgrowlocks('conflict_test'); }
+
+# Test 1: No locking - should not show in pgrowlocks
+permutation s1_begin s1_doselect_nolock s2_rowlocks s1_rollback
+
+# Test 2: FOR KEY SHARE - should show lock
+permutation s1_begin s1_doselect_keyshare s2_rowlocks s1_rollback
+
+# Test 3: FOR SHARE - should show lock
+permutation s1_begin s1_doselect_share s2_rowlocks s1_rollback
+
+# Test 4: FOR NO KEY UPDATE - should show lock
+permutation s1_begin s1_doselect_nokeyupd s2_rowlocks s1_rollback
+
+# Test 5: FOR UPDATE - should show lock
+permutation s1_begin s1_doselect_update s2_rowlocks s1_rollback
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index d950bd93002..0a0335fedb7 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -923,10 +923,16 @@ infer_arbiter_indexes(PlannerInfo *root)
*/
if (indexOidFromConstraint == idxForm->indexrelid)
{
- if (idxForm->indisexclusion && onconflict->action == ONCONFLICT_UPDATE)
+ if (idxForm->indisexclusion &&
+ (onconflict->action == ONCONFLICT_UPDATE ||
+ onconflict->action == ONCONFLICT_SELECT))
+ /* INSERT into an exclusion constraint can conflict with multiple rows.
+ * So ON CONFLICT UPDATE OR SELECT would have to update/select mutliple rows
+ * in those cases. Which seems weird - so block it with an error. */
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("ON CONFLICT DO UPDATE not supported with exclusion constraints")));
+ errmsg("ON CONFLICT DO %s not supported with exclusion constraints",
+ onconflict->action == ONCONFLICT_UPDATE ? "UPDATE" : "SELECT")));
results = lappend_oid(results, idxForm->indexrelid);
list_free(indexList);
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index f3cd32b7222..cf91c72d40b 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3653,11 +3653,12 @@ rewriteTargetView(Query *parsetree, Relation view)
}
/*
- * For INSERT .. ON CONFLICT .. DO UPDATE, we must also update assorted
- * stuff in the onConflict data structure.
+ * For INSERT .. ON CONFLICT .. DO UPDATE/SELECT, we must also update
+ * assorted stuff in the onConflict data structure.
*/
if (parsetree->onConflict &&
- parsetree->onConflict->action == ONCONFLICT_UPDATE)
+ (parsetree->onConflict->action == ONCONFLICT_UPDATE ||
+ parsetree->onConflict->action == ONCONFLICT_SELECT))
{
Index old_exclRelIndex,
new_exclRelIndex;
@@ -3666,28 +3667,30 @@ rewriteTargetView(Query *parsetree, Relation view)
List *tmp_tlist;
/*
- * Like the INSERT/UPDATE code above, update the resnos in the
- * auxiliary UPDATE targetlist to refer to columns of the base
- * relation.
+ * For ON CONFLICT DO UPDATE, update the resnos in the auxiliary
+ * UPDATE targetlist to refer to columns of the base relation.
*/
- foreach(lc, parsetree->onConflict->onConflictSet)
+ if (parsetree->onConflict->action == ONCONFLICT_UPDATE)
{
- TargetEntry *tle = (TargetEntry *) lfirst(lc);
- TargetEntry *view_tle;
+ foreach(lc, parsetree->onConflict->onConflictSet)
+ {
+ TargetEntry *tle = (TargetEntry *) lfirst(lc);
+ TargetEntry *view_tle;
- if (tle->resjunk)
- continue;
+ if (tle->resjunk)
+ continue;
- view_tle = get_tle_by_resno(view_targetlist, tle->resno);
- if (view_tle != NULL && !view_tle->resjunk && IsA(view_tle->expr, Var))
- tle->resno = ((Var *) view_tle->expr)->varattno;
- else
- elog(ERROR, "attribute number %d not found in view targetlist",
- tle->resno);
+ view_tle = get_tle_by_resno(view_targetlist, tle->resno);
+ if (view_tle != NULL && !view_tle->resjunk && IsA(view_tle->expr, Var))
+ tle->resno = ((Var *) view_tle->expr)->varattno;
+ else
+ elog(ERROR, "attribute number %d not found in view targetlist",
+ tle->resno);
+ }
}
/*
- * Also, create a new RTE for the EXCLUDED pseudo-relation, using the
+ * Create a new RTE for the EXCLUDED pseudo-relation, using the
* query's new base rel (which may well have a different column list
* from the view, hence we need a new column alias list). This should
* match transformOnConflictClause. In particular, note that the
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index d87686de000..fe9677bdf3c 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -2364,14 +2364,14 @@ typedef struct FromExpr
*
* The optimizer requires a list of inference elements, and optionally a WHERE
* clause to infer a unique index. The unique index (or, occasionally,
- * indexes) inferred are used to arbitrate whether or not the alternative ON
- * CONFLICT path is taken.
+ * indexes) inferred are used to arbitrate whether or not the alternative
+ * ON CONFLICT path is taken.
*----------
*/
typedef struct OnConflictExpr
{
NodeTag type;
- OnConflictAction action; /* DO NOTHING or UPDATE? */
+ OnConflictAction action; /* NONE, DO NOTHING, DO UPDATE, DO SELECT ? */
/* Arbiter */
List *arbiterElems; /* unique index arbiter list (of
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 1bbf59cca02..8bc1f0cd5ab 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -776,10 +776,16 @@ DETAIL: Key (c1, (c2::circle))=(<(20,20),10>, <(0,0),4>) conflicts with existin
-- succeed, because violation is ignored
INSERT INTO circles VALUES('<(20,20), 10>', '<(0,0), 4>')
ON CONFLICT ON CONSTRAINT circles_c1_c2_excl DO NOTHING;
--- fail, because DO UPDATE variant requires unique index
+-- fail, because DO UPDATE variant requires unique index.
+-- (without a unique index, we can't know which row to update)
INSERT INTO circles VALUES('<(20,20), 10>', '<(0,0), 4>')
ON CONFLICT ON CONSTRAINT circles_c1_c2_excl DO UPDATE SET c2 = EXCLUDED.c2;
ERROR: ON CONFLICT DO UPDATE not supported with exclusion constraints
+-- fail, just like DO UPDATE.
+-- otherwise, we could return multiple rows which seems odd, if not exactly wrong
+INSERT INTO circles VALUES('<(20,20), 10>', '<(0,0), 4>')
+ ON CONFLICT ON CONSTRAINT circles_c1_c2_excl DO SELECT RETURNING *;
+ERROR: ON CONFLICT DO SELECT not supported with exclusion constraints
-- succeed because c1 doesn't overlap
INSERT INTO circles VALUES('<(20,20), 1>', '<(0,0), 5>');
-- succeed because c2 doesn't overlap
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index a01a2c883fd..d6a2be1f96e 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -2436,10 +2436,58 @@ INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel')
ON CONFLICT (did) DO UPDATE SET dauthor = 'regress_rls_carol';
ERROR: new row violates row-level security policy for table "document"
--
+-- INSERT ... ON CONFLICT DO SELECT and Row-level security
+--
+SET SESSION AUTHORIZATION regress_rls_alice;
+DROP POLICY p3_with_all ON document;
+CREATE POLICY p1_select_novels ON document FOR SELECT
+ USING (cid = (SELECT cid from category WHERE cname = 'novel'));
+CREATE POLICY p2_insert_own ON document FOR INSERT
+ WITH CHECK (dauthor = current_user);
+CREATE POLICY p3_update_novels ON document FOR UPDATE
+ USING (cid = (SELECT cid from category WHERE cname = 'novel'))
+ WITH CHECK (dauthor = current_user);
+SET SESSION AUTHORIZATION regress_rls_bob;
+-- DO SELECT requires SELECT rights, should succeed for novel
+INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel')
+ ON CONFLICT (did) DO SELECT RETURNING did, dauthor, dtitle;
+ did | dauthor | dtitle
+-----+-----------------+----------------
+ 1 | regress_rls_bob | my first novel
+(1 row)
+
+-- DO SELECT requires SELECT rights, should fail for non-novel
+INSERT INTO document VALUES (33, (SELECT cid from category WHERE cname = 'science fiction'), 1, 'regress_rls_bob', 'another sci-fi')
+ ON CONFLICT (did) DO SELECT RETURNING did, dauthor, dtitle;
+ERROR: new row violates row-level security policy for table "document"
+-- DO SELECT with WHERE and EXCLUDED reference
+INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel')
+ ON CONFLICT (did) DO SELECT WHERE excluded.dlevel = 1 RETURNING did, dauthor, dtitle;
+ did | dauthor | dtitle
+-----+-----------------+----------------
+ 1 | regress_rls_bob | my first novel
+(1 row)
+
+-- DO SELECT FOR UPDATE requires both SELECT and UPDATE rights, should succeed for novel
+INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel')
+ ON CONFLICT (did) DO SELECT FOR UPDATE RETURNING did, dauthor, dtitle;
+ did | dauthor | dtitle
+-----+-----------------+----------------
+ 1 | regress_rls_bob | my first novel
+(1 row)
+
+-- DO SELECT FOR UPDATE requires UPDATE rights, should fail for non-novel
+INSERT INTO document VALUES (33, (SELECT cid from category WHERE cname = 'science fiction'), 1, 'regress_rls_bob', 'another sci-fi')
+ ON CONFLICT (did) DO SELECT FOR UPDATE RETURNING did, dauthor, dtitle;
+ERROR: new row violates row-level security policy for table "document"
+SET SESSION AUTHORIZATION regress_rls_alice;
+DROP POLICY p1_select_novels ON document;
+DROP POLICY p2_insert_own ON document;
+DROP POLICY p3_update_novels ON document;
+--
-- MERGE
--
RESET SESSION AUTHORIZATION;
-DROP POLICY p3_with_all ON document;
ALTER TABLE document ADD COLUMN dnotes text DEFAULT '';
-- all documents are readable
CREATE POLICY p1 ON document FOR SELECT USING (true);
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index 03df7e75b7b..a3c811effc8 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -316,6 +316,37 @@ SELECT * FROM rw_view15;
3 | UNSPECIFIED
(6 rows)
+-- Test ON CONFLICT DO SELECT with updatable views
+-- This tests behavior consistency between DO SELECT and DO UPDATE when using WHERE clauses
+-- Note: rw_view15 is defined as "SELECT a, upper(b) FROM base_tbl" where base_tbl.b has DEFAULT 'Unspecified'
+INSERT INTO rw_view15 (a) VALUES (3) ON CONFLICT (a) DO SELECT RETURNING *; -- needs RETURNING, should return existing row
+ a | upper
+---+-------------
+ 3 | UNSPECIFIED
+(1 row)
+
+INSERT INTO rw_view15 (a) VALUES (3) ON CONFLICT (a) DO SELECT WHERE excluded.upper = 'UNSPECIFIED' RETURNING *; -- WHERE on view column (uppercase)
+ a | upper
+---+-------------
+ 3 | UNSPECIFIED
+(1 row)
+
+INSERT INTO rw_view15 (a) VALUES (3) ON CONFLICT (a) DO UPDATE SET a = excluded.a WHERE excluded.upper = 'UNSPECIFIED' RETURNING *; -- compare DO UPDATE with same WHERE
+ a | upper
+---+-------------
+ 3 | UNSPECIFIED
+(1 row)
+
+INSERT INTO rw_view15 (a) VALUES (3) ON CONFLICT (a) DO SELECT WHERE excluded.upper = 'Unspecified' RETURNING *; -- WHERE on excluded value (mixed case)
+ a | upper
+---+-------
+(0 rows)
+
+INSERT INTO rw_view15 (a) VALUES (3) ON CONFLICT (a) DO UPDATE SET a = excluded.a WHERE excluded.upper = 'Unspecified' RETURNING *; -- compare DO UPDATE with same WHERE
+ a | upper
+---+-------
+(0 rows)
+
SELECT * FROM rw_view15;
a | upper
----+-------------
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index 733a1dbccfe..b093e92850f 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -565,9 +565,14 @@ INSERT INTO circles VALUES('<(20,20), 10>', '<(0,0), 4>');
-- succeed, because violation is ignored
INSERT INTO circles VALUES('<(20,20), 10>', '<(0,0), 4>')
ON CONFLICT ON CONSTRAINT circles_c1_c2_excl DO NOTHING;
--- fail, because DO UPDATE variant requires unique index
+-- fail, because DO UPDATE variant requires unique index.
+-- (without a unique index, we can't know which row to update)
INSERT INTO circles VALUES('<(20,20), 10>', '<(0,0), 4>')
ON CONFLICT ON CONSTRAINT circles_c1_c2_excl DO UPDATE SET c2 = EXCLUDED.c2;
+-- fail, just like DO UPDATE.
+-- otherwise, we could return multiple rows which seems odd, if not exactly wrong
+INSERT INTO circles VALUES('<(20,20), 10>', '<(0,0), 4>')
+ ON CONFLICT ON CONSTRAINT circles_c1_c2_excl DO SELECT RETURNING *;
-- succeed because c1 doesn't overlap
INSERT INTO circles VALUES('<(20,20), 1>', '<(0,0), 5>');
-- succeed because c2 doesn't overlap
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index e5b78810e69..9d3c4f21b17 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -966,11 +966,52 @@ INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel')
ON CONFLICT (did) DO UPDATE SET dauthor = 'regress_rls_carol';
--
--- MERGE
+-- INSERT ... ON CONFLICT DO SELECT and Row-level security
--
-RESET SESSION AUTHORIZATION;
+
+SET SESSION AUTHORIZATION regress_rls_alice;
DROP POLICY p3_with_all ON document;
+CREATE POLICY p1_select_novels ON document FOR SELECT
+ USING (cid = (SELECT cid from category WHERE cname = 'novel'));
+CREATE POLICY p2_insert_own ON document FOR INSERT
+ WITH CHECK (dauthor = current_user);
+CREATE POLICY p3_update_novels ON document FOR UPDATE
+ USING (cid = (SELECT cid from category WHERE cname = 'novel'))
+ WITH CHECK (dauthor = current_user);
+
+SET SESSION AUTHORIZATION regress_rls_bob;
+
+-- DO SELECT requires SELECT rights, should succeed for novel
+INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel')
+ ON CONFLICT (did) DO SELECT RETURNING did, dauthor, dtitle;
+
+-- DO SELECT requires SELECT rights, should fail for non-novel
+INSERT INTO document VALUES (33, (SELECT cid from category WHERE cname = 'science fiction'), 1, 'regress_rls_bob', 'another sci-fi')
+ ON CONFLICT (did) DO SELECT RETURNING did, dauthor, dtitle;
+
+-- DO SELECT with WHERE and EXCLUDED reference
+INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel')
+ ON CONFLICT (did) DO SELECT WHERE excluded.dlevel = 1 RETURNING did, dauthor, dtitle;
+
+-- DO SELECT FOR UPDATE requires both SELECT and UPDATE rights, should succeed for novel
+INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel')
+ ON CONFLICT (did) DO SELECT FOR UPDATE RETURNING did, dauthor, dtitle;
+
+-- DO SELECT FOR UPDATE requires UPDATE rights, should fail for non-novel
+INSERT INTO document VALUES (33, (SELECT cid from category WHERE cname = 'science fiction'), 1, 'regress_rls_bob', 'another sci-fi')
+ ON CONFLICT (did) DO SELECT FOR UPDATE RETURNING did, dauthor, dtitle;
+
+SET SESSION AUTHORIZATION regress_rls_alice;
+DROP POLICY p1_select_novels ON document;
+DROP POLICY p2_insert_own ON document;
+DROP POLICY p3_update_novels ON document;
+
+
+--
+-- MERGE
+--
+RESET SESSION AUTHORIZATION;
ALTER TABLE document ADD COLUMN dnotes text DEFAULT '';
-- all documents are readable
diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql
index c071fffc116..d9f1ca5bd97 100644
--- a/src/test/regress/sql/updatable_views.sql
+++ b/src/test/regress/sql/updatable_views.sql
@@ -106,6 +106,14 @@ INSERT INTO rw_view15 (a) VALUES (3) ON CONFLICT (a) DO UPDATE set a = excluded.
SELECT * FROM rw_view15;
INSERT INTO rw_view15 (a) VALUES (3) ON CONFLICT (a) DO UPDATE set upper = 'blarg'; -- fails
SELECT * FROM rw_view15;
+-- Test ON CONFLICT DO SELECT with updatable views
+-- This tests behavior consistency between DO SELECT and DO UPDATE when using WHERE clauses
+-- Note: rw_view15 is defined as "SELECT a, upper(b) FROM base_tbl" where base_tbl.b has DEFAULT 'Unspecified'
+INSERT INTO rw_view15 (a) VALUES (3) ON CONFLICT (a) DO SELECT RETURNING *; -- needs RETURNING, should return existing row
+INSERT INTO rw_view15 (a) VALUES (3) ON CONFLICT (a) DO SELECT WHERE excluded.upper = 'UNSPECIFIED' RETURNING *; -- WHERE on view column (uppercase)
+INSERT INTO rw_view15 (a) VALUES (3) ON CONFLICT (a) DO UPDATE SET a = excluded.a WHERE excluded.upper = 'UNSPECIFIED' RETURNING *; -- compare DO UPDATE with same WHERE
+INSERT INTO rw_view15 (a) VALUES (3) ON CONFLICT (a) DO SELECT WHERE excluded.upper = 'Unspecified' RETURNING *; -- WHERE on excluded value (mixed case)
+INSERT INTO rw_view15 (a) VALUES (3) ON CONFLICT (a) DO UPDATE SET a = excluded.a WHERE excluded.upper = 'Unspecified' RETURNING *; -- compare DO UPDATE with same WHERE
SELECT * FROM rw_view15;
ALTER VIEW rw_view15 ALTER COLUMN upper SET DEFAULT 'NOT SET';
INSERT INTO rw_view15 (a) VALUES (4); -- should fail
--
2.48.1