1
0
mirror of https://github.com/postgres/postgres.git synced 2025-07-27 12:41:57 +03:00

MERGE SQL Command following SQL:2016

MERGE performs actions that modify rows in the target table
using a source table or query. MERGE provides a single SQL
statement that can conditionally INSERT/UPDATE/DELETE rows
a task that would other require multiple PL statements.
e.g.

MERGE INTO target AS t
USING source AS s
ON t.tid = s.sid
WHEN MATCHED AND t.balance > s.delta THEN
  UPDATE SET balance = t.balance - s.delta
WHEN MATCHED THEN
  DELETE
WHEN NOT MATCHED AND s.delta > 0 THEN
  INSERT VALUES (s.sid, s.delta)
WHEN NOT MATCHED THEN
  DO NOTHING;

MERGE works with regular and partitioned tables, including
column and row security enforcement, as well as support for
row, statement and transition triggers.

MERGE is optimized for OLTP and is parameterizable, though
also useful for large scale ETL/ELT. MERGE is not intended
to be used in preference to existing single SQL commands
for INSERT, UPDATE or DELETE since there is some overhead.
MERGE can be used statically from PL/pgSQL.

MERGE does not yet support inheritance, write rules,
RETURNING clauses, updatable views or foreign tables.
MERGE follows SQL Standard per the most recent SQL:2016.

Includes full tests and documentation, including full
isolation tests to demonstrate the concurrent behavior.

This version written from scratch in 2017 by Simon Riggs,
using docs and tests originally written in 2009. Later work
from Pavan Deolasee has been both complex and deep, leaving
the lead author credit now in his hands.
Extensive discussion of concurrency from Peter Geoghegan,
with thanks for the time and effort contributed.

Various issues reported via sqlsmith by Andreas Seltenreich

Authors: Pavan Deolasee, Simon Riggs
Reviewer: Peter Geoghegan, Amit Langote, Tomas Vondra, Simon Riggs

Discussion:
https://postgr.es/m/CANP8+jKitBSrB7oTgT9CY2i1ObfOt36z0XMraQc+Xrz8QB0nXA@mail.gmail.com
https://postgr.es/m/CAH2-WzkJdBuxj9PO=2QaO9-3h3xGbQPZ34kJH=HukRekwM-GZg@mail.gmail.com
This commit is contained in:
Simon Riggs
2018-04-03 09:28:16 +01:00
parent aa5877bb26
commit d204ef6377
82 changed files with 2600 additions and 165 deletions

View File

@ -33,6 +33,10 @@ test: insert-conflict-do-update
test: insert-conflict-do-update-2
test: insert-conflict-do-update-3
test: insert-conflict-toast
test: merge-insert-update
test: merge-delete
test: merge-update
test: merge-match-recheck
test: delete-abort-savept
test: delete-abort-savept-2
test: aborted-keyrevoke

View File

@ -386,3 +386,58 @@ CREATE TABLE itest_child PARTITION OF itest_parent (
) FOR VALUES FROM ('2016-07-01') TO ('2016-08-01'); -- error
ERROR: identity columns are not supported on partitions
DROP TABLE itest_parent;
-- MERGE tests
CREATE TABLE itest14 (a int GENERATED ALWAYS AS IDENTITY, b text);
CREATE TABLE itest15 (a int GENERATED BY DEFAULT AS IDENTITY, b text);
MERGE INTO itest14 t
USING (SELECT 10 AS s_a, 'inserted by merge' AS s_b) s
ON t.a = s.s_a
WHEN NOT MATCHED THEN
INSERT (a, b) VALUES (s.s_a, s.s_b);
ERROR: cannot insert into column "a"
DETAIL: Column "a" is an identity column defined as GENERATED ALWAYS.
HINT: Use OVERRIDING SYSTEM VALUE to override.
MERGE INTO itest14 t
USING (SELECT 20 AS s_a, 'inserted by merge' AS s_b) s
ON t.a = s.s_a
WHEN NOT MATCHED THEN
INSERT (a, b) OVERRIDING USER VALUE VALUES (s.s_a, s.s_b);
ERROR: cannot insert into column "a"
DETAIL: Column "a" is an identity column defined as GENERATED ALWAYS.
HINT: Use OVERRIDING SYSTEM VALUE to override.
MERGE INTO itest14 t
USING (SELECT 30 AS s_a, 'inserted by merge' AS s_b) s
ON t.a = s.s_a
WHEN NOT MATCHED THEN
INSERT (a, b) OVERRIDING SYSTEM VALUE VALUES (s.s_a, s.s_b);
MERGE INTO itest15 t
USING (SELECT 10 AS s_a, 'inserted by merge' AS s_b) s
ON t.a = s.s_a
WHEN NOT MATCHED THEN
INSERT (a, b) VALUES (s.s_a, s.s_b);
MERGE INTO itest15 t
USING (SELECT 20 AS s_a, 'inserted by merge' AS s_b) s
ON t.a = s.s_a
WHEN NOT MATCHED THEN
INSERT (a, b) OVERRIDING USER VALUE VALUES (s.s_a, s.s_b);
MERGE INTO itest15 t
USING (SELECT 30 AS s_a, 'inserted by merge' AS s_b) s
ON t.a = s.s_a
WHEN NOT MATCHED THEN
INSERT (a, b) OVERRIDING SYSTEM VALUE VALUES (s.s_a, s.s_b);
SELECT * FROM itest14;
a | b
----+-------------------
30 | inserted by merge
(1 row)
SELECT * FROM itest15;
a | b
----+-------------------
10 | inserted by merge
1 | inserted by merge
30 | inserted by merge
(3 rows)
DROP TABLE itest14;
DROP TABLE itest15;

View File

@ -517,6 +517,104 @@ SELECT atest6 FROM atest6; -- ok
(0 rows)
COPY atest6 TO stdout; -- ok
-- test column privileges with MERGE
SET SESSION AUTHORIZATION regress_priv_user1;
CREATE TABLE mtarget (a int, b text);
CREATE TABLE msource (a int, b text);
INSERT INTO mtarget VALUES (1, 'init1'), (2, 'init2');
INSERT INTO msource VALUES (1, 'source1'), (2, 'source2'), (3, 'source3');
GRANT SELECT (a) ON msource TO regress_priv_user4;
GRANT SELECT (a) ON mtarget TO regress_priv_user4;
GRANT INSERT (a,b) ON mtarget TO regress_priv_user4;
GRANT UPDATE (b) ON mtarget TO regress_priv_user4;
SET SESSION AUTHORIZATION regress_priv_user4;
--
-- test source privileges
--
-- fail (no SELECT priv on s.b)
MERGE INTO mtarget t USING msource s ON t.a = s.a
WHEN MATCHED THEN
UPDATE SET b = s.b
WHEN NOT MATCHED THEN
INSERT VALUES (a, NULL);
ERROR: permission denied for table msource
-- fail (s.b used in the INSERTed values)
MERGE INTO mtarget t USING msource s ON t.a = s.a
WHEN MATCHED THEN
UPDATE SET b = 'x'
WHEN NOT MATCHED THEN
INSERT VALUES (a, b);
ERROR: permission denied for table msource
-- fail (s.b used in the WHEN quals)
MERGE INTO mtarget t USING msource s ON t.a = s.a
WHEN MATCHED AND s.b = 'x' THEN
UPDATE SET b = 'x'
WHEN NOT MATCHED THEN
INSERT VALUES (a, NULL);
ERROR: permission denied for table msource
-- this should be ok since only s.a is accessed
BEGIN;
MERGE INTO mtarget t USING msource s ON t.a = s.a
WHEN MATCHED THEN
UPDATE SET b = 'ok'
WHEN NOT MATCHED THEN
INSERT VALUES (a, NULL);
ROLLBACK;
SET SESSION AUTHORIZATION regress_priv_user1;
GRANT SELECT (b) ON msource TO regress_priv_user4;
SET SESSION AUTHORIZATION regress_priv_user4;
-- should now be ok
BEGIN;
MERGE INTO mtarget t USING msource s ON t.a = s.a
WHEN MATCHED THEN
UPDATE SET b = s.b
WHEN NOT MATCHED THEN
INSERT VALUES (a, b);
ROLLBACK;
--
-- test target privileges
--
-- fail (no SELECT priv on t.b)
MERGE INTO mtarget t USING msource s ON t.a = s.a
WHEN MATCHED THEN
UPDATE SET b = t.b
WHEN NOT MATCHED THEN
INSERT VALUES (a, NULL);
ERROR: permission denied for table mtarget
-- fail (no UPDATE on t.a)
MERGE INTO mtarget t USING msource s ON t.a = s.a
WHEN MATCHED THEN
UPDATE SET b = s.b, a = t.a + 1
WHEN NOT MATCHED THEN
INSERT VALUES (a, b);
ERROR: permission denied for table mtarget
-- fail (no SELECT on t.b)
MERGE INTO mtarget t USING msource s ON t.a = s.a
WHEN MATCHED AND t.b IS NOT NULL THEN
UPDATE SET b = s.b
WHEN NOT MATCHED THEN
INSERT VALUES (a, b);
ERROR: permission denied for table mtarget
-- ok
BEGIN;
MERGE INTO mtarget t USING msource s ON t.a = s.a
WHEN MATCHED THEN
UPDATE SET b = s.b;
ROLLBACK;
-- fail (no DELETE)
MERGE INTO mtarget t USING msource s ON t.a = s.a
WHEN MATCHED AND t.b IS NOT NULL THEN
DELETE;
ERROR: permission denied for table mtarget
-- grant delete privileges
SET SESSION AUTHORIZATION regress_priv_user1;
GRANT DELETE ON mtarget TO regress_priv_user4;
-- should be ok now
BEGIN;
MERGE INTO mtarget t USING msource s ON t.a = s.a
WHEN MATCHED AND t.b IS NOT NULL THEN
DELETE;
ROLLBACK;
-- check error reporting with column privs
SET SESSION AUTHORIZATION regress_priv_user1;
CREATE TABLE t1 (c1 int, c2 int, c3 int check (c3 < 5), primary key (c1, c2));

View File

@ -2138,6 +2138,188 @@ ERROR: new row violates row-level security policy (USING expression) for table
INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'my first novel')
ON CONFLICT (did) DO UPDATE SET dauthor = 'regress_rls_carol';
ERROR: new row violates row-level security policy for table "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);
-- one may insert documents only authored by them
CREATE POLICY p2 ON document FOR INSERT WITH CHECK (dauthor = current_user);
-- one may only update documents in 'novel' category
CREATE POLICY p3 ON document FOR UPDATE
USING (cid = (SELECT cid from category WHERE cname = 'novel'))
WITH CHECK (dauthor = current_user);
-- one may only delete documents in 'manga' category
CREATE POLICY p4 ON document FOR DELETE
USING (cid = (SELECT cid from category WHERE cname = 'manga'));
SELECT * FROM document;
did | cid | dlevel | dauthor | dtitle | dnotes
-----+-----+--------+-------------------+----------------------------------+--------
1 | 11 | 1 | regress_rls_bob | my first novel |
3 | 22 | 2 | regress_rls_bob | my science fiction |
4 | 44 | 1 | regress_rls_bob | my first manga |
5 | 44 | 2 | regress_rls_bob | my second manga |
6 | 22 | 1 | regress_rls_carol | great science fiction |
7 | 33 | 2 | regress_rls_carol | great technology book |
8 | 44 | 1 | regress_rls_carol | great manga |
9 | 22 | 1 | regress_rls_dave | awesome science fiction |
10 | 33 | 2 | regress_rls_dave | awesome technology book |
11 | 33 | 1 | regress_rls_carol | hoge |
33 | 22 | 1 | regress_rls_bob | okay science fiction |
2 | 11 | 2 | regress_rls_bob | my first novel |
78 | 33 | 1 | regress_rls_bob | some technology novel |
79 | 33 | 1 | regress_rls_bob | technology book, can only insert |
(14 rows)
SET SESSION AUTHORIZATION regress_rls_bob;
-- Fails, since update violates WITH CHECK qual on dauthor
MERGE INTO document d
USING (SELECT 1 as sdid) s
ON did = s.sdid
WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge1 ', dauthor = 'regress_rls_alice';
ERROR: new row violates row-level security policy for table "document"
-- Should be OK since USING and WITH CHECK quals pass
MERGE INTO document d
USING (SELECT 1 as sdid) s
ON did = s.sdid
WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge2 ';
-- Even when dauthor is updated explicitly, but to the existing value
MERGE INTO document d
USING (SELECT 1 as sdid) s
ON did = s.sdid
WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge3 ', dauthor = 'regress_rls_bob';
-- There is a MATCH for did = 3, but UPDATE's USING qual does not allow
-- updating an item in category 'science fiction'
MERGE INTO document d
USING (SELECT 3 as sdid) s
ON did = s.sdid
WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge ';
ERROR: target row violates row-level security policy (USING expression) for table "document"
-- The same thing with DELETE action, but fails again because no permissions
-- to delete items in 'science fiction' category that did 3 belongs to.
MERGE INTO document d
USING (SELECT 3 as sdid) s
ON did = s.sdid
WHEN MATCHED THEN
DELETE;
ERROR: target row violates row-level security policy (USING expression) for table "document"
-- Document with did 4 belongs to 'manga' category which is allowed for
-- deletion. But this fails because the UPDATE action is matched first and
-- UPDATE policy does not allow updation in the category.
MERGE INTO document d
USING (SELECT 4 as sdid) s
ON did = s.sdid
WHEN MATCHED AND dnotes = '' THEN
UPDATE SET dnotes = dnotes || ' notes added by merge '
WHEN MATCHED THEN
DELETE;
ERROR: target row violates row-level security policy (USING expression) for table "document"
-- UPDATE action is not matched this time because of the WHEN AND qual.
-- DELETE still fails because role regress_rls_bob does not have SELECT
-- privileges on 'manga' category row in the category table.
MERGE INTO document d
USING (SELECT 4 as sdid) s
ON did = s.sdid
WHEN MATCHED AND dnotes <> '' THEN
UPDATE SET dnotes = dnotes || ' notes added by merge '
WHEN MATCHED THEN
DELETE;
ERROR: target row violates row-level security policy (USING expression) for table "document"
SELECT * FROM document WHERE did = 4;
did | cid | dlevel | dauthor | dtitle | dnotes
-----+-----+--------+-----------------+----------------+--------
4 | 44 | 1 | regress_rls_bob | my first manga |
(1 row)
-- Switch to regress_rls_carol role and try the DELETE again. It should succeed
-- this time
RESET SESSION AUTHORIZATION;
SET SESSION AUTHORIZATION regress_rls_carol;
MERGE INTO document d
USING (SELECT 4 as sdid) s
ON did = s.sdid
WHEN MATCHED AND dnotes <> '' THEN
UPDATE SET dnotes = dnotes || ' notes added by merge '
WHEN MATCHED THEN
DELETE;
-- Switch back to regress_rls_bob role
RESET SESSION AUTHORIZATION;
SET SESSION AUTHORIZATION regress_rls_bob;
-- Try INSERT action. This fails because we are trying to insert
-- dauthor = regress_rls_dave and INSERT's WITH CHECK does not allow
-- that
MERGE INTO document d
USING (SELECT 12 as sdid) s
ON did = s.sdid
WHEN MATCHED THEN
DELETE
WHEN NOT MATCHED THEN
INSERT VALUES (12, 11, 1, 'regress_rls_dave', 'another novel');
ERROR: new row violates row-level security policy for table "document"
-- This should be fine
MERGE INTO document d
USING (SELECT 12 as sdid) s
ON did = s.sdid
WHEN MATCHED THEN
DELETE
WHEN NOT MATCHED THEN
INSERT VALUES (12, 11, 1, 'regress_rls_bob', 'another novel');
-- ok
MERGE INTO document d
USING (SELECT 1 as sdid) s
ON did = s.sdid
WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge4 '
WHEN NOT MATCHED THEN
INSERT VALUES (12, 11, 1, 'regress_rls_bob', 'another novel');
-- drop and create a new SELECT policy which prevents us from reading
-- any document except with category 'magna'
RESET SESSION AUTHORIZATION;
DROP POLICY p1 ON document;
CREATE POLICY p1 ON document FOR SELECT
USING (cid = (SELECT cid from category WHERE cname = 'manga'));
SET SESSION AUTHORIZATION regress_rls_bob;
-- MERGE can no longer see the matching row and hence attempts the
-- NOT MATCHED action, which results in unique key violation
MERGE INTO document d
USING (SELECT 1 as sdid) s
ON did = s.sdid
WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge5 '
WHEN NOT MATCHED THEN
INSERT VALUES (12, 11, 1, 'regress_rls_bob', 'another novel');
ERROR: duplicate key value violates unique constraint "document_pkey"
RESET SESSION AUTHORIZATION;
-- drop the restrictive SELECT policy so that we can look at the
-- final state of the table
DROP POLICY p1 ON document;
-- Just check everything went per plan
SELECT * FROM document;
did | cid | dlevel | dauthor | dtitle | dnotes
-----+-----+--------+-------------------+----------------------------------+-----------------------------------------------------------------------
3 | 22 | 2 | regress_rls_bob | my science fiction |
5 | 44 | 2 | regress_rls_bob | my second manga |
6 | 22 | 1 | regress_rls_carol | great science fiction |
7 | 33 | 2 | regress_rls_carol | great technology book |
8 | 44 | 1 | regress_rls_carol | great manga |
9 | 22 | 1 | regress_rls_dave | awesome science fiction |
10 | 33 | 2 | regress_rls_dave | awesome technology book |
11 | 33 | 1 | regress_rls_carol | hoge |
33 | 22 | 1 | regress_rls_bob | okay science fiction |
2 | 11 | 2 | regress_rls_bob | my first novel |
78 | 33 | 1 | regress_rls_bob | some technology novel |
79 | 33 | 1 | regress_rls_bob | technology book, can only insert |
12 | 11 | 1 | regress_rls_bob | another novel |
1 | 11 | 1 | regress_rls_bob | my first novel | notes added by merge2 notes added by merge3 notes added by merge4
(14 rows)
--
-- ROLE/GROUP
--

View File

@ -3265,6 +3265,37 @@ CREATE RULE rules_parted_table_insert AS ON INSERT to rules_parted_table
ALTER RULE rules_parted_table_insert ON rules_parted_table RENAME TO rules_parted_table_insert_redirect;
DROP TABLE rules_parted_table;
--
-- test MERGE
--
CREATE TABLE rule_merge1 (a int, b text);
CREATE TABLE rule_merge2 (a int, b text);
CREATE RULE rule1 AS ON INSERT TO rule_merge1
DO INSTEAD INSERT INTO rule_merge2 VALUES (NEW.*);
CREATE RULE rule2 AS ON UPDATE TO rule_merge1
DO INSTEAD UPDATE rule_merge2 SET a = NEW.a, b = NEW.b
WHERE a = OLD.a;
CREATE RULE rule3 AS ON DELETE TO rule_merge1
DO INSTEAD DELETE FROM rule_merge2 WHERE a = OLD.a;
-- MERGE not supported for table with rules
MERGE INTO rule_merge1 t USING (SELECT 1 AS a) s
ON t.a = s.a
WHEN MATCHED AND t.a < 2 THEN
UPDATE SET b = b || ' updated by merge'
WHEN MATCHED AND t.a > 2 THEN
DELETE
WHEN NOT MATCHED THEN
INSERT VALUES (s.a, '');
ERROR: MERGE is not supported for relations with rules
-- should be ok with the other table though
MERGE INTO rule_merge2 t USING (SELECT 1 AS a) s
ON t.a = s.a
WHEN MATCHED AND t.a < 2 THEN
UPDATE SET b = b || ' updated by merge'
WHEN MATCHED AND t.a > 2 THEN
DELETE
WHEN NOT MATCHED THEN
INSERT VALUES (s.a, '');
--
-- Test enabling/disabling
--
CREATE TABLE ruletest1 (a int);

View File

@ -2761,6 +2761,54 @@ delete from self_ref where a = 1;
NOTICE: trigger_func(self_ref) called: action = DELETE, when = BEFORE, level = STATEMENT
NOTICE: trigger = self_ref_s_trig, old table = (1,), (2,1), (3,2), (4,3)
drop table self_ref;
--
-- test transition tables with MERGE
--
create table merge_target_table (a int primary key, b text);
create trigger merge_target_table_insert_trig
after insert on merge_target_table referencing new table as new_table
for each statement execute procedure dump_insert();
create trigger merge_target_table_update_trig
after update on merge_target_table referencing old table as old_table new table as new_table
for each statement execute procedure dump_update();
create trigger merge_target_table_delete_trig
after delete on merge_target_table referencing old table as old_table
for each statement execute procedure dump_delete();
create table merge_source_table (a int, b text);
insert into merge_source_table
values (1, 'initial1'), (2, 'initial2'),
(3, 'initial3'), (4, 'initial4');
merge into merge_target_table t
using merge_source_table s
on t.a = s.a
when not matched then
insert values (a, b);
NOTICE: trigger = merge_target_table_insert_trig, new table = (1,initial1), (2,initial2), (3,initial3), (4,initial4)
merge into merge_target_table t
using merge_source_table s
on t.a = s.a
when matched and s.a <= 2 then
update set b = t.b || ' updated by merge'
when matched and s.a > 2 then
delete
when not matched then
insert values (a, b);
NOTICE: trigger = merge_target_table_delete_trig, old table = (3,initial3), (4,initial4)
NOTICE: trigger = merge_target_table_update_trig, old table = (1,initial1), (2,initial2), new table = (1,"initial1 updated by merge"), (2,"initial2 updated by merge")
NOTICE: trigger = merge_target_table_insert_trig, new table = <NULL>
merge into merge_target_table t
using merge_source_table s
on t.a = s.a
when matched and s.a <= 2 then
update set b = t.b || ' updated again by merge'
when matched and s.a > 2 then
delete
when not matched then
insert values (a, b);
NOTICE: trigger = merge_target_table_delete_trig, old table = <NULL>
NOTICE: trigger = merge_target_table_update_trig, old table = (1,"initial1 updated by merge"), (2,"initial2 updated by merge"), new table = (1,"initial1 updated by merge updated again by merge"), (2,"initial2 updated by merge updated again by merge")
NOTICE: trigger = merge_target_table_insert_trig, new table = (3,initial3), (4,initial4)
drop table merge_source_table, merge_target_table;
-- cleanup
drop function dump_insert();
drop function dump_update();

View File

@ -84,7 +84,7 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
# ----------
# Another group of parallel tests
# ----------
test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password func_index
test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password func_index merge
# ----------
# Another group of parallel tests

View File

@ -123,6 +123,7 @@ test: tablesample
test: groupingsets
test: drop_operator
test: password
test: merge
test: alter_generic
test: alter_operator
test: misc

View File

@ -246,3 +246,48 @@ CREATE TABLE itest_child PARTITION OF itest_parent (
f3 WITH OPTIONS GENERATED ALWAYS AS IDENTITY
) FOR VALUES FROM ('2016-07-01') TO ('2016-08-01'); -- error
DROP TABLE itest_parent;
-- MERGE tests
CREATE TABLE itest14 (a int GENERATED ALWAYS AS IDENTITY, b text);
CREATE TABLE itest15 (a int GENERATED BY DEFAULT AS IDENTITY, b text);
MERGE INTO itest14 t
USING (SELECT 10 AS s_a, 'inserted by merge' AS s_b) s
ON t.a = s.s_a
WHEN NOT MATCHED THEN
INSERT (a, b) VALUES (s.s_a, s.s_b);
MERGE INTO itest14 t
USING (SELECT 20 AS s_a, 'inserted by merge' AS s_b) s
ON t.a = s.s_a
WHEN NOT MATCHED THEN
INSERT (a, b) OVERRIDING USER VALUE VALUES (s.s_a, s.s_b);
MERGE INTO itest14 t
USING (SELECT 30 AS s_a, 'inserted by merge' AS s_b) s
ON t.a = s.s_a
WHEN NOT MATCHED THEN
INSERT (a, b) OVERRIDING SYSTEM VALUE VALUES (s.s_a, s.s_b);
MERGE INTO itest15 t
USING (SELECT 10 AS s_a, 'inserted by merge' AS s_b) s
ON t.a = s.s_a
WHEN NOT MATCHED THEN
INSERT (a, b) VALUES (s.s_a, s.s_b);
MERGE INTO itest15 t
USING (SELECT 20 AS s_a, 'inserted by merge' AS s_b) s
ON t.a = s.s_a
WHEN NOT MATCHED THEN
INSERT (a, b) OVERRIDING USER VALUE VALUES (s.s_a, s.s_b);
MERGE INTO itest15 t
USING (SELECT 30 AS s_a, 'inserted by merge' AS s_b) s
ON t.a = s.s_a
WHEN NOT MATCHED THEN
INSERT (a, b) OVERRIDING SYSTEM VALUE VALUES (s.s_a, s.s_b);
SELECT * FROM itest14;
SELECT * FROM itest15;
DROP TABLE itest14;
DROP TABLE itest15;

View File

@ -349,6 +349,114 @@ UPDATE atest5 SET one = 1; -- fail
SELECT atest6 FROM atest6; -- ok
COPY atest6 TO stdout; -- ok
-- test column privileges with MERGE
SET SESSION AUTHORIZATION regress_priv_user1;
CREATE TABLE mtarget (a int, b text);
CREATE TABLE msource (a int, b text);
INSERT INTO mtarget VALUES (1, 'init1'), (2, 'init2');
INSERT INTO msource VALUES (1, 'source1'), (2, 'source2'), (3, 'source3');
GRANT SELECT (a) ON msource TO regress_priv_user4;
GRANT SELECT (a) ON mtarget TO regress_priv_user4;
GRANT INSERT (a,b) ON mtarget TO regress_priv_user4;
GRANT UPDATE (b) ON mtarget TO regress_priv_user4;
SET SESSION AUTHORIZATION regress_priv_user4;
--
-- test source privileges
--
-- fail (no SELECT priv on s.b)
MERGE INTO mtarget t USING msource s ON t.a = s.a
WHEN MATCHED THEN
UPDATE SET b = s.b
WHEN NOT MATCHED THEN
INSERT VALUES (a, NULL);
-- fail (s.b used in the INSERTed values)
MERGE INTO mtarget t USING msource s ON t.a = s.a
WHEN MATCHED THEN
UPDATE SET b = 'x'
WHEN NOT MATCHED THEN
INSERT VALUES (a, b);
-- fail (s.b used in the WHEN quals)
MERGE INTO mtarget t USING msource s ON t.a = s.a
WHEN MATCHED AND s.b = 'x' THEN
UPDATE SET b = 'x'
WHEN NOT MATCHED THEN
INSERT VALUES (a, NULL);
-- this should be ok since only s.a is accessed
BEGIN;
MERGE INTO mtarget t USING msource s ON t.a = s.a
WHEN MATCHED THEN
UPDATE SET b = 'ok'
WHEN NOT MATCHED THEN
INSERT VALUES (a, NULL);
ROLLBACK;
SET SESSION AUTHORIZATION regress_priv_user1;
GRANT SELECT (b) ON msource TO regress_priv_user4;
SET SESSION AUTHORIZATION regress_priv_user4;
-- should now be ok
BEGIN;
MERGE INTO mtarget t USING msource s ON t.a = s.a
WHEN MATCHED THEN
UPDATE SET b = s.b
WHEN NOT MATCHED THEN
INSERT VALUES (a, b);
ROLLBACK;
--
-- test target privileges
--
-- fail (no SELECT priv on t.b)
MERGE INTO mtarget t USING msource s ON t.a = s.a
WHEN MATCHED THEN
UPDATE SET b = t.b
WHEN NOT MATCHED THEN
INSERT VALUES (a, NULL);
-- fail (no UPDATE on t.a)
MERGE INTO mtarget t USING msource s ON t.a = s.a
WHEN MATCHED THEN
UPDATE SET b = s.b, a = t.a + 1
WHEN NOT MATCHED THEN
INSERT VALUES (a, b);
-- fail (no SELECT on t.b)
MERGE INTO mtarget t USING msource s ON t.a = s.a
WHEN MATCHED AND t.b IS NOT NULL THEN
UPDATE SET b = s.b
WHEN NOT MATCHED THEN
INSERT VALUES (a, b);
-- ok
BEGIN;
MERGE INTO mtarget t USING msource s ON t.a = s.a
WHEN MATCHED THEN
UPDATE SET b = s.b;
ROLLBACK;
-- fail (no DELETE)
MERGE INTO mtarget t USING msource s ON t.a = s.a
WHEN MATCHED AND t.b IS NOT NULL THEN
DELETE;
-- grant delete privileges
SET SESSION AUTHORIZATION regress_priv_user1;
GRANT DELETE ON mtarget TO regress_priv_user4;
-- should be ok now
BEGIN;
MERGE INTO mtarget t USING msource s ON t.a = s.a
WHEN MATCHED AND t.b IS NOT NULL THEN
DELETE;
ROLLBACK;
-- check error reporting with column privs
SET SESSION AUTHORIZATION regress_priv_user1;
CREATE TABLE t1 (c1 int, c2 int, c3 int check (c3 < 5), primary key (c1, c2));

View File

@ -812,6 +812,162 @@ INSERT INTO document VALUES (4, (SELECT cid from category WHERE cname = 'novel')
INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'my first novel')
ON CONFLICT (did) DO UPDATE SET dauthor = 'regress_rls_carol';
--
-- 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);
-- one may insert documents only authored by them
CREATE POLICY p2 ON document FOR INSERT WITH CHECK (dauthor = current_user);
-- one may only update documents in 'novel' category
CREATE POLICY p3 ON document FOR UPDATE
USING (cid = (SELECT cid from category WHERE cname = 'novel'))
WITH CHECK (dauthor = current_user);
-- one may only delete documents in 'manga' category
CREATE POLICY p4 ON document FOR DELETE
USING (cid = (SELECT cid from category WHERE cname = 'manga'));
SELECT * FROM document;
SET SESSION AUTHORIZATION regress_rls_bob;
-- Fails, since update violates WITH CHECK qual on dauthor
MERGE INTO document d
USING (SELECT 1 as sdid) s
ON did = s.sdid
WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge1 ', dauthor = 'regress_rls_alice';
-- Should be OK since USING and WITH CHECK quals pass
MERGE INTO document d
USING (SELECT 1 as sdid) s
ON did = s.sdid
WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge2 ';
-- Even when dauthor is updated explicitly, but to the existing value
MERGE INTO document d
USING (SELECT 1 as sdid) s
ON did = s.sdid
WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge3 ', dauthor = 'regress_rls_bob';
-- There is a MATCH for did = 3, but UPDATE's USING qual does not allow
-- updating an item in category 'science fiction'
MERGE INTO document d
USING (SELECT 3 as sdid) s
ON did = s.sdid
WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge ';
-- The same thing with DELETE action, but fails again because no permissions
-- to delete items in 'science fiction' category that did 3 belongs to.
MERGE INTO document d
USING (SELECT 3 as sdid) s
ON did = s.sdid
WHEN MATCHED THEN
DELETE;
-- Document with did 4 belongs to 'manga' category which is allowed for
-- deletion. But this fails because the UPDATE action is matched first and
-- UPDATE policy does not allow updation in the category.
MERGE INTO document d
USING (SELECT 4 as sdid) s
ON did = s.sdid
WHEN MATCHED AND dnotes = '' THEN
UPDATE SET dnotes = dnotes || ' notes added by merge '
WHEN MATCHED THEN
DELETE;
-- UPDATE action is not matched this time because of the WHEN AND qual.
-- DELETE still fails because role regress_rls_bob does not have SELECT
-- privileges on 'manga' category row in the category table.
MERGE INTO document d
USING (SELECT 4 as sdid) s
ON did = s.sdid
WHEN MATCHED AND dnotes <> '' THEN
UPDATE SET dnotes = dnotes || ' notes added by merge '
WHEN MATCHED THEN
DELETE;
SELECT * FROM document WHERE did = 4;
-- Switch to regress_rls_carol role and try the DELETE again. It should succeed
-- this time
RESET SESSION AUTHORIZATION;
SET SESSION AUTHORIZATION regress_rls_carol;
MERGE INTO document d
USING (SELECT 4 as sdid) s
ON did = s.sdid
WHEN MATCHED AND dnotes <> '' THEN
UPDATE SET dnotes = dnotes || ' notes added by merge '
WHEN MATCHED THEN
DELETE;
-- Switch back to regress_rls_bob role
RESET SESSION AUTHORIZATION;
SET SESSION AUTHORIZATION regress_rls_bob;
-- Try INSERT action. This fails because we are trying to insert
-- dauthor = regress_rls_dave and INSERT's WITH CHECK does not allow
-- that
MERGE INTO document d
USING (SELECT 12 as sdid) s
ON did = s.sdid
WHEN MATCHED THEN
DELETE
WHEN NOT MATCHED THEN
INSERT VALUES (12, 11, 1, 'regress_rls_dave', 'another novel');
-- This should be fine
MERGE INTO document d
USING (SELECT 12 as sdid) s
ON did = s.sdid
WHEN MATCHED THEN
DELETE
WHEN NOT MATCHED THEN
INSERT VALUES (12, 11, 1, 'regress_rls_bob', 'another novel');
-- ok
MERGE INTO document d
USING (SELECT 1 as sdid) s
ON did = s.sdid
WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge4 '
WHEN NOT MATCHED THEN
INSERT VALUES (12, 11, 1, 'regress_rls_bob', 'another novel');
-- drop and create a new SELECT policy which prevents us from reading
-- any document except with category 'magna'
RESET SESSION AUTHORIZATION;
DROP POLICY p1 ON document;
CREATE POLICY p1 ON document FOR SELECT
USING (cid = (SELECT cid from category WHERE cname = 'manga'));
SET SESSION AUTHORIZATION regress_rls_bob;
-- MERGE can no longer see the matching row and hence attempts the
-- NOT MATCHED action, which results in unique key violation
MERGE INTO document d
USING (SELECT 1 as sdid) s
ON did = s.sdid
WHEN MATCHED THEN
UPDATE SET dnotes = dnotes || ' notes added by merge5 '
WHEN NOT MATCHED THEN
INSERT VALUES (12, 11, 1, 'regress_rls_bob', 'another novel');
RESET SESSION AUTHORIZATION;
-- drop the restrictive SELECT policy so that we can look at the
-- final state of the table
DROP POLICY p1 ON document;
-- Just check everything went per plan
SELECT * FROM document;
--
-- ROLE/GROUP
--

View File

@ -1191,6 +1191,39 @@ CREATE RULE rules_parted_table_insert AS ON INSERT to rules_parted_table
ALTER RULE rules_parted_table_insert ON rules_parted_table RENAME TO rules_parted_table_insert_redirect;
DROP TABLE rules_parted_table;
--
-- test MERGE
--
CREATE TABLE rule_merge1 (a int, b text);
CREATE TABLE rule_merge2 (a int, b text);
CREATE RULE rule1 AS ON INSERT TO rule_merge1
DO INSTEAD INSERT INTO rule_merge2 VALUES (NEW.*);
CREATE RULE rule2 AS ON UPDATE TO rule_merge1
DO INSTEAD UPDATE rule_merge2 SET a = NEW.a, b = NEW.b
WHERE a = OLD.a;
CREATE RULE rule3 AS ON DELETE TO rule_merge1
DO INSTEAD DELETE FROM rule_merge2 WHERE a = OLD.a;
-- MERGE not supported for table with rules
MERGE INTO rule_merge1 t USING (SELECT 1 AS a) s
ON t.a = s.a
WHEN MATCHED AND t.a < 2 THEN
UPDATE SET b = b || ' updated by merge'
WHEN MATCHED AND t.a > 2 THEN
DELETE
WHEN NOT MATCHED THEN
INSERT VALUES (s.a, '');
-- should be ok with the other table though
MERGE INTO rule_merge2 t USING (SELECT 1 AS a) s
ON t.a = s.a
WHEN MATCHED AND t.a < 2 THEN
UPDATE SET b = b || ' updated by merge'
WHEN MATCHED AND t.a > 2 THEN
DELETE
WHEN NOT MATCHED THEN
INSERT VALUES (s.a, '');
--
-- Test enabling/disabling
--

View File

@ -2110,6 +2110,53 @@ delete from self_ref where a = 1;
drop table self_ref;
--
-- test transition tables with MERGE
--
create table merge_target_table (a int primary key, b text);
create trigger merge_target_table_insert_trig
after insert on merge_target_table referencing new table as new_table
for each statement execute procedure dump_insert();
create trigger merge_target_table_update_trig
after update on merge_target_table referencing old table as old_table new table as new_table
for each statement execute procedure dump_update();
create trigger merge_target_table_delete_trig
after delete on merge_target_table referencing old table as old_table
for each statement execute procedure dump_delete();
create table merge_source_table (a int, b text);
insert into merge_source_table
values (1, 'initial1'), (2, 'initial2'),
(3, 'initial3'), (4, 'initial4');
merge into merge_target_table t
using merge_source_table s
on t.a = s.a
when not matched then
insert values (a, b);
merge into merge_target_table t
using merge_source_table s
on t.a = s.a
when matched and s.a <= 2 then
update set b = t.b || ' updated by merge'
when matched and s.a > 2 then
delete
when not matched then
insert values (a, b);
merge into merge_target_table t
using merge_source_table s
on t.a = s.a
when matched and s.a <= 2 then
update set b = t.b || ' updated again by merge'
when matched and s.a > 2 then
delete
when not matched then
insert values (a, b);
drop table merge_source_table, merge_target_table;
-- cleanup
drop function dump_insert();
drop function dump_update();