From: Peter Eisentraut Date: Thu, 4 Jun 2026 09:30:41 +0000 (+0200) Subject: Require UPDATE permission on FOR PORTION OF column X-Git-Url: http://git.ipfire.org/gitweb/index.cgi?a=commitdiff_plain;h=7ac030d5b152475275e84e0abe0b3628700fbeda;p=thirdparty%2Fpostgresql.git Require UPDATE permission on FOR PORTION OF column It seems like the SQL standard does require this after all, and it makes sense because these columns get changed. (This is not to be confused with *not* requiring INSERT permission to add the temporal leftovers.) Adding the column to RTEPermissionInfo->updatedCols also fixes a couple outstanding bugs from other (non-permission) features using that bitmapset to detect changes: GENERATED columns and UPDATE OF triggers. This patch includes test cases to exercise those scenarios, including on partitioned tables. Author: Paul A. Jungwirth Reviewed-by: jian he Discussion: https://www.postgresql.org/message-id/flat/CAHg%2BQDcd%3Dt69gLf9yQexO07EJ2mx0Z70NFHo6h94X1EDA%3DhM0g%40mail.gmail.com --- diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index ffcf25a6be7..93fa66ae57c 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -1549,6 +1549,7 @@ transformForPortionOfClause(ParseState *pstate, List *funcArgs; Node *rangeTLEExpr; TargetEntry *tle; + RTEPermissionInfo *target_perminfo = pstate->p_target_nsitem->p_perminfo; /* * Whatever operator is used for intersect by temporal foreign keys, @@ -1598,14 +1599,9 @@ transformForPortionOfClause(ParseState *pstate, forPortionOf->range_name, false); result->rangeTargetList = lappend(result->rangeTargetList, tle); - /* - * The range column will change, but you don't need UPDATE permission - * on it, so we don't add to updatedCols here. XXX: If - * https://www.postgresql.org/message-id/CACJufxEtY1hdLcx%3DFhnqp-ERcV1PhbvELG5COy_CZjoEW76ZPQ%40mail.gmail.com - * is merged (only validate CHECK constraints if they depend on one of - * the columns being UPDATEd), we need to make sure that code knows - * that we are updating the application-time column. - */ + /* Mark the range column as requiring update permissions */ + target_perminfo->updatedCols = bms_add_member(target_perminfo->updatedCols, + range_attno - FirstLowInvalidHeapAttributeNumber); } else result->rangeTargetList = NIL; diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out index 16b2f998dc0..a53ed6e2d64 100644 --- a/src/test/regress/expected/for_portion_of.out +++ b/src/test/regress/expected/for_portion_of.out @@ -1365,6 +1365,9 @@ $$; CREATE TRIGGER fpo_before_stmt BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); +CREATE TRIGGER fpo_before_stmt1 + BEFORE UPDATE OF valid_at ON for_portion_of_test + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); CREATE TRIGGER fpo_after_insert_stmt AFTER INSERT ON for_portion_of_test FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); @@ -1378,6 +1381,9 @@ CREATE TRIGGER fpo_after_delete_stmt CREATE TRIGGER fpo_before_row BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); +CREATE TRIGGER fpo_before_row1 + BEFORE UPDATE OF valid_at ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); CREATE TRIGGER fpo_after_insert_row AFTER INSERT ON for_portion_of_test FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); @@ -1394,9 +1400,15 @@ UPDATE for_portion_of_test NOTICE: fpo_before_stmt: BEFORE UPDATE STATEMENT: NOTICE: old: NOTICE: new: +NOTICE: fpo_before_stmt1: BEFORE UPDATE STATEMENT: +NOTICE: old: +NOTICE: new: NOTICE: fpo_before_row: BEFORE UPDATE ROW: NOTICE: old: [2019-01-01,2030-01-01) NOTICE: new: [2021-01-01,2022-01-01) +NOTICE: fpo_before_row1: BEFORE UPDATE ROW: +NOTICE: old: [2019-01-01,2030-01-01) +NOTICE: new: [2021-01-01,2022-01-01) NOTICE: fpo_before_stmt: BEFORE INSERT STATEMENT: NOTICE: old: NOTICE: new: @@ -2024,6 +2036,7 @@ SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at; DROP TABLE for_portion_of_test2; DROP TYPE mydaterange; -- Test FOR PORTION OF against a partitioned table. +-- Include a GENERATED STORED column to test updatedCols column mapping. -- temporal_partitioned_1 has the same attnums as the root -- temporal_partitioned_3 has the different attnums from the root -- temporal_partitioned_5 has the different attnums too, but reversed @@ -2031,29 +2044,34 @@ CREATE TABLE temporal_partitioned ( id int4range, valid_at daterange, name text, + range_len int GENERATED ALWAYS AS (upper(valid_at) - lower(valid_at)) STORED, CONSTRAINT temporal_paritioned_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) ) PARTITION BY LIST (id); CREATE TABLE temporal_partitioned_1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,2)', '[2,3)'); CREATE TABLE temporal_partitioned_3 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,4)', '[4,5)'); CREATE TABLE temporal_partitioned_5 PARTITION OF temporal_partitioned FOR VALUES IN ('[5,6)', '[6,7)'); ALTER TABLE temporal_partitioned DETACH PARTITION temporal_partitioned_3; -ALTER TABLE temporal_partitioned_3 DROP COLUMN id, DROP COLUMN valid_at; +ALTER TABLE temporal_partitioned_3 DROP COLUMN id, DROP COLUMN valid_at CASCADE; +NOTICE: drop cascades to column range_len of table temporal_partitioned_3 ALTER TABLE temporal_partitioned_3 ADD COLUMN id int4range NOT NULL, ADD COLUMN valid_at daterange NOT NULL; +ALTER TABLE temporal_partitioned_3 ADD COLUMN range_len int GENERATED ALWAYS AS (upper(valid_at) - lower(valid_at)) STORED; ALTER TABLE temporal_partitioned ATTACH PARTITION temporal_partitioned_3 FOR VALUES IN ('[3,4)', '[4,5)'); ALTER TABLE temporal_partitioned DETACH PARTITION temporal_partitioned_5; -ALTER TABLE temporal_partitioned_5 DROP COLUMN id, DROP COLUMN valid_at; +ALTER TABLE temporal_partitioned_5 DROP COLUMN id, DROP COLUMN valid_at CASCADE; +NOTICE: drop cascades to column range_len of table temporal_partitioned_5 ALTER TABLE temporal_partitioned_5 ADD COLUMN valid_at daterange NOT NULL, ADD COLUMN id int4range NOT NULL; +ALTER TABLE temporal_partitioned_5 ADD COLUMN range_len int GENERATED ALWAYS AS (upper(valid_at) - lower(valid_at)) STORED; ALTER TABLE temporal_partitioned ATTACH PARTITION temporal_partitioned_5 FOR VALUES IN ('[5,6)', '[6,7)'); INSERT INTO temporal_partitioned (id, valid_at, name) VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'), 'one'), ('[3,4)', daterange('2000-01-01', '2010-01-01'), 'three'), ('[5,6)', daterange('2000-01-01', '2010-01-01'), 'five'); SELECT * FROM temporal_partitioned; - id | valid_at | name --------+-------------------------+------- - [1,2) | [2000-01-01,2010-01-01) | one - [3,4) | [2000-01-01,2010-01-01) | three - [5,6) | [2000-01-01,2010-01-01) | five + id | valid_at | name | range_len +-------+-------------------------+-------+----------- + [1,2) | [2000-01-01,2010-01-01) | one | 3653 + [3,4) | [2000-01-01,2010-01-01) | three | 3653 + [5,6) | [2000-01-01,2010-01-01) | five | 3653 (3 rows) -- Update without moving within partition 1 @@ -2084,54 +2102,54 @@ UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-0 id = '[3,4)' WHERE id = '[5,6)'; -- Update all partitions at once (each with leftovers) -SELECT * FROM temporal_partitioned ORDER BY id, valid_at; - id | valid_at | name --------+-------------------------+--------- - [1,2) | [2000-01-01,2000-03-01) | one - [1,2) | [2000-03-01,2000-04-01) | one^1 - [1,2) | [2000-04-01,2000-06-01) | one - [1,2) | [2000-07-01,2010-01-01) | one - [2,3) | [2000-06-01,2000-07-01) | three^2 - [3,4) | [2000-01-01,2000-03-01) | three - [3,4) | [2000-03-01,2000-04-01) | three^1 - [3,4) | [2000-04-01,2000-06-01) | three - [3,4) | [2000-06-01,2000-07-01) | five^2 - [3,4) | [2000-07-01,2010-01-01) | three - [4,5) | [2000-06-01,2000-07-01) | one^2 - [5,6) | [2000-01-01,2000-03-01) | five - [5,6) | [2000-03-01,2000-04-01) | five^1 - [5,6) | [2000-04-01,2000-06-01) | five - [5,6) | [2000-07-01,2010-01-01) | five +SELECT *, upper(valid_at) - lower(valid_at) FROM temporal_partitioned ORDER BY id, valid_at; + id | valid_at | name | range_len | ?column? +-------+-------------------------+---------+-----------+---------- + [1,2) | [2000-01-01,2000-03-01) | one | 60 | 60 + [1,2) | [2000-03-01,2000-04-01) | one^1 | 31 | 31 + [1,2) | [2000-04-01,2000-06-01) | one | 61 | 61 + [1,2) | [2000-07-01,2010-01-01) | one | 3471 | 3471 + [2,3) | [2000-06-01,2000-07-01) | three^2 | 30 | 30 + [3,4) | [2000-01-01,2000-03-01) | three | 60 | 60 + [3,4) | [2000-03-01,2000-04-01) | three^1 | 31 | 31 + [3,4) | [2000-04-01,2000-06-01) | three | 61 | 61 + [3,4) | [2000-06-01,2000-07-01) | five^2 | 30 | 30 + [3,4) | [2000-07-01,2010-01-01) | three | 3471 | 3471 + [4,5) | [2000-06-01,2000-07-01) | one^2 | 30 | 30 + [5,6) | [2000-01-01,2000-03-01) | five | 60 | 60 + [5,6) | [2000-03-01,2000-04-01) | five^1 | 31 | 31 + [5,6) | [2000-04-01,2000-06-01) | five | 61 | 61 + [5,6) | [2000-07-01,2010-01-01) | five | 3471 | 3471 (15 rows) SELECT * FROM temporal_partitioned_1 ORDER BY id, valid_at; - id | valid_at | name --------+-------------------------+--------- - [1,2) | [2000-01-01,2000-03-01) | one - [1,2) | [2000-03-01,2000-04-01) | one^1 - [1,2) | [2000-04-01,2000-06-01) | one - [1,2) | [2000-07-01,2010-01-01) | one - [2,3) | [2000-06-01,2000-07-01) | three^2 + id | valid_at | name | range_len +-------+-------------------------+---------+----------- + [1,2) | [2000-01-01,2000-03-01) | one | 60 + [1,2) | [2000-03-01,2000-04-01) | one^1 | 31 + [1,2) | [2000-04-01,2000-06-01) | one | 61 + [1,2) | [2000-07-01,2010-01-01) | one | 3471 + [2,3) | [2000-06-01,2000-07-01) | three^2 | 30 (5 rows) SELECT * FROM temporal_partitioned_3 ORDER BY id, valid_at; - name | id | valid_at ----------+-------+------------------------- - three | [3,4) | [2000-01-01,2000-03-01) - three^1 | [3,4) | [2000-03-01,2000-04-01) - three | [3,4) | [2000-04-01,2000-06-01) - five^2 | [3,4) | [2000-06-01,2000-07-01) - three | [3,4) | [2000-07-01,2010-01-01) - one^2 | [4,5) | [2000-06-01,2000-07-01) + name | id | valid_at | range_len +---------+-------+-------------------------+----------- + three | [3,4) | [2000-01-01,2000-03-01) | 60 + three^1 | [3,4) | [2000-03-01,2000-04-01) | 31 + three | [3,4) | [2000-04-01,2000-06-01) | 61 + five^2 | [3,4) | [2000-06-01,2000-07-01) | 30 + three | [3,4) | [2000-07-01,2010-01-01) | 3471 + one^2 | [4,5) | [2000-06-01,2000-07-01) | 30 (6 rows) SELECT * FROM temporal_partitioned_5 ORDER BY id, valid_at; - name | valid_at | id ---------+-------------------------+------- - five | [2000-01-01,2000-03-01) | [5,6) - five^1 | [2000-03-01,2000-04-01) | [5,6) - five | [2000-04-01,2000-06-01) | [5,6) - five | [2000-07-01,2010-01-01) | [5,6) + name | valid_at | id | range_len +--------+-------------------------+-------+----------- + five | [2000-01-01,2000-03-01) | [5,6) | 60 + five^1 | [2000-03-01,2000-04-01) | [5,6) | 31 + five | [2000-04-01,2000-06-01) | [5,6) | 61 + five | [2000-07-01,2010-01-01) | [5,6) | 3471 (4 rows) DROP TABLE temporal_partitioned; @@ -2190,4 +2208,84 @@ SELECT * FROM fpo_rule ORDER BY f1; (2 rows) DROP TABLE fpo_rule; +-- UPDATE FOR PORTION OF with generated stored columns +-- The generated column depends on the range column, so it must be +-- recomputed when FOR PORTION OF narrows the range. +CREATE TABLE fpo_generated ( + id int, + valid_at int4range, + range_len int GENERATED ALWAYS AS (upper(valid_at) - lower(valid_at)) STORED, + range_lenv int GENERATED ALWAYS AS (upper(valid_at) - lower(valid_at)) +); +INSERT INTO fpo_generated (id, valid_at) VALUES (1, '[10,100)'); +SELECT * FROM fpo_generated ORDER BY valid_at; + id | valid_at | range_len | range_lenv +----+----------+-----------+------------ + 1 | [10,100) | 90 | 90 +(1 row) + +-- After the FOR PORTION OF (FPO) update, all three resulting rows +-- (leftover-before, updated, and leftover-after) must contain the correct +-- values for range_len and range_lenv. +UPDATE fpo_generated + FOR PORTION OF valid_at FROM 30 TO 70 + SET id = 2; +SELECT * FROM fpo_generated ORDER BY valid_at; + id | valid_at | range_len | range_lenv +----+----------+-----------+------------ + 1 | [10,30) | 20 | 20 + 2 | [30,70) | 40 | 40 + 1 | [70,100) | 30 | 30 +(3 rows) + +-- Also test with a generated column that references both a SET column +-- and the range column. +DROP TABLE fpo_generated; +CREATE TABLE fpo_generated ( + id int, + valid_at int4range, + id_plus_len int GENERATED ALWAYS AS (id + upper(valid_at) - lower(valid_at)) STORED, + id_plus_lenv int GENERATED ALWAYS AS (id + upper(valid_at) - lower(valid_at)) +); +INSERT INTO fpo_generated (id, valid_at) VALUES (1, '[10,100)'); +SELECT * FROM fpo_generated ORDER BY valid_at; + id | valid_at | id_plus_len | id_plus_lenv +----+----------+-------------+-------------- + 1 | [10,100) | 91 | 91 +(1 row) + +UPDATE fpo_generated + FOR PORTION OF valid_at FROM 30 TO 70 + SET id = 2; +SELECT * FROM fpo_generated ORDER BY valid_at; + id | valid_at | id_plus_len | id_plus_lenv +----+----------+-------------+-------------- + 1 | [10,30) | 21 | 21 + 2 | [30,70) | 42 | 42 + 1 | [70,100) | 31 | 31 +(3 rows) + +DROP TABLE fpo_generated; +-- Test that UPDATE OF colname triggers fire if colname is valid_at: +CREATE TABLE fpo_update_of_trigger ( + id int, + valid_at int4range +); +INSERT INTO fpo_update_of_trigger (id, valid_at) VALUES (1, '[10,100)'); +CREATE TRIGGER fpo_before_row1 + BEFORE UPDATE OF valid_at ON fpo_update_of_trigger + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); +CREATE TRIGGER fpo_before_row2 + BEFORE UPDATE OF valid_at ON fpo_update_of_trigger + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); +UPDATE fpo_update_of_trigger + FOR PORTION OF valid_at FROM 30 TO 70 + SET id = 2; +NOTICE: fpo_before_row2: BEFORE UPDATE STATEMENT: +NOTICE: old: +NOTICE: new: +NOTICE: fpo_before_row1: BEFORE UPDATE ROW: +NOTICE: old: [10,100) +NOTICE: new: [30,70) +DROP TABLE fpo_update_of_trigger; RESET datestyle; diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out index 0de13612818..f6cc1a1029c 100644 --- a/src/test/regress/expected/privileges.out +++ b/src/test/regress/expected/privileges.out @@ -1152,16 +1152,26 @@ CREATE TABLE t1 ( valid_at tsrange, CONSTRAINT t1pk PRIMARY KEY (c1, valid_at WITHOUT OVERLAPS) ); --- UPDATE requires select permission on the valid_at column (but not update): +-- UPDATE requires select and update permission on the valid_at column: GRANT SELECT (c1) ON t1 TO regress_priv_user2; GRANT UPDATE (c1) ON t1 TO regress_priv_user2; GRANT SELECT (c1, valid_at) ON t1 TO regress_priv_user3; GRANT UPDATE (c1) ON t1 TO regress_priv_user3; +GRANT SELECT (c1) ON t1 TO regress_priv_user4; +GRANT UPDATE (c1, valid_at) ON t1 TO regress_priv_user4; +GRANT SELECT (c1, valid_at) ON t1 TO regress_priv_user5; +GRANT UPDATE (c1, valid_at) ON t1 TO regress_priv_user5; SET SESSION AUTHORIZATION regress_priv_user2; UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)'; ERROR: permission denied for table t1 SET SESSION AUTHORIZATION regress_priv_user3; UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)'; +ERROR: permission denied for table t1 +SET SESSION AUTHORIZATION regress_priv_user4; +UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)'; +ERROR: permission denied for table t1 +SET SESSION AUTHORIZATION regress_priv_user5; +UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)'; SET SESSION AUTHORIZATION regress_priv_user1; -- DELETE requires select permission on the valid_at column: GRANT DELETE ON t1 TO regress_priv_user2; diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql index 63642b1851e..56c66d91a5d 100644 --- a/src/test/regress/sql/for_portion_of.sql +++ b/src/test/regress/sql/for_portion_of.sql @@ -913,6 +913,10 @@ CREATE TRIGGER fpo_before_stmt BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); +CREATE TRIGGER fpo_before_stmt1 + BEFORE UPDATE OF valid_at ON for_portion_of_test + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); + CREATE TRIGGER fpo_after_insert_stmt AFTER INSERT ON for_portion_of_test FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); @@ -931,6 +935,10 @@ CREATE TRIGGER fpo_before_row BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); +CREATE TRIGGER fpo_before_row1 + BEFORE UPDATE OF valid_at ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); + CREATE TRIGGER fpo_after_insert_row AFTER INSERT ON for_portion_of_test FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); @@ -1330,6 +1338,7 @@ DROP TABLE for_portion_of_test2; DROP TYPE mydaterange; -- Test FOR PORTION OF against a partitioned table. +-- Include a GENERATED STORED column to test updatedCols column mapping. -- temporal_partitioned_1 has the same attnums as the root -- temporal_partitioned_3 has the different attnums from the root -- temporal_partitioned_5 has the different attnums too, but reversed @@ -1338,6 +1347,7 @@ CREATE TABLE temporal_partitioned ( id int4range, valid_at daterange, name text, + range_len int GENERATED ALWAYS AS (upper(valid_at) - lower(valid_at)) STORED, CONSTRAINT temporal_paritioned_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) ) PARTITION BY LIST (id); CREATE TABLE temporal_partitioned_1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,2)', '[2,3)'); @@ -1345,13 +1355,15 @@ CREATE TABLE temporal_partitioned_3 PARTITION OF temporal_partitioned FOR VALUES CREATE TABLE temporal_partitioned_5 PARTITION OF temporal_partitioned FOR VALUES IN ('[5,6)', '[6,7)'); ALTER TABLE temporal_partitioned DETACH PARTITION temporal_partitioned_3; -ALTER TABLE temporal_partitioned_3 DROP COLUMN id, DROP COLUMN valid_at; +ALTER TABLE temporal_partitioned_3 DROP COLUMN id, DROP COLUMN valid_at CASCADE; ALTER TABLE temporal_partitioned_3 ADD COLUMN id int4range NOT NULL, ADD COLUMN valid_at daterange NOT NULL; +ALTER TABLE temporal_partitioned_3 ADD COLUMN range_len int GENERATED ALWAYS AS (upper(valid_at) - lower(valid_at)) STORED; ALTER TABLE temporal_partitioned ATTACH PARTITION temporal_partitioned_3 FOR VALUES IN ('[3,4)', '[4,5)'); ALTER TABLE temporal_partitioned DETACH PARTITION temporal_partitioned_5; -ALTER TABLE temporal_partitioned_5 DROP COLUMN id, DROP COLUMN valid_at; +ALTER TABLE temporal_partitioned_5 DROP COLUMN id, DROP COLUMN valid_at CASCADE; ALTER TABLE temporal_partitioned_5 ADD COLUMN valid_at daterange NOT NULL, ADD COLUMN id int4range NOT NULL; +ALTER TABLE temporal_partitioned_5 ADD COLUMN range_len int GENERATED ALWAYS AS (upper(valid_at) - lower(valid_at)) STORED; ALTER TABLE temporal_partitioned ATTACH PARTITION temporal_partitioned_5 FOR VALUES IN ('[5,6)', '[6,7)'); INSERT INTO temporal_partitioned (id, valid_at, name) VALUES @@ -1396,7 +1408,7 @@ UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-0 -- Update all partitions at once (each with leftovers) -SELECT * FROM temporal_partitioned ORDER BY id, valid_at; +SELECT *, upper(valid_at) - lower(valid_at) FROM temporal_partitioned ORDER BY id, valid_at; SELECT * FROM temporal_partitioned_1 ORDER BY id, valid_at; SELECT * FROM temporal_partitioned_3 ORDER BY id, valid_at; SELECT * FROM temporal_partitioned_5 ORDER BY id, valid_at; @@ -1436,4 +1448,62 @@ SELECT * FROM fpo_rule ORDER BY f1; DROP TABLE fpo_rule; +-- UPDATE FOR PORTION OF with generated stored columns +-- The generated column depends on the range column, so it must be +-- recomputed when FOR PORTION OF narrows the range. +CREATE TABLE fpo_generated ( + id int, + valid_at int4range, + range_len int GENERATED ALWAYS AS (upper(valid_at) - lower(valid_at)) STORED, + range_lenv int GENERATED ALWAYS AS (upper(valid_at) - lower(valid_at)) +); +INSERT INTO fpo_generated (id, valid_at) VALUES (1, '[10,100)'); + +SELECT * FROM fpo_generated ORDER BY valid_at; + +-- After the FOR PORTION OF (FPO) update, all three resulting rows +-- (leftover-before, updated, and leftover-after) must contain the correct +-- values for range_len and range_lenv. +UPDATE fpo_generated + FOR PORTION OF valid_at FROM 30 TO 70 + SET id = 2; + +SELECT * FROM fpo_generated ORDER BY valid_at; + +-- Also test with a generated column that references both a SET column +-- and the range column. +DROP TABLE fpo_generated; +CREATE TABLE fpo_generated ( + id int, + valid_at int4range, + id_plus_len int GENERATED ALWAYS AS (id + upper(valid_at) - lower(valid_at)) STORED, + id_plus_lenv int GENERATED ALWAYS AS (id + upper(valid_at) - lower(valid_at)) +); + +INSERT INTO fpo_generated (id, valid_at) VALUES (1, '[10,100)'); +SELECT * FROM fpo_generated ORDER BY valid_at; + +UPDATE fpo_generated + FOR PORTION OF valid_at FROM 30 TO 70 + SET id = 2; +SELECT * FROM fpo_generated ORDER BY valid_at; +DROP TABLE fpo_generated; + +-- Test that UPDATE OF colname triggers fire if colname is valid_at: +CREATE TABLE fpo_update_of_trigger ( + id int, + valid_at int4range +); +INSERT INTO fpo_update_of_trigger (id, valid_at) VALUES (1, '[10,100)'); +CREATE TRIGGER fpo_before_row1 + BEFORE UPDATE OF valid_at ON fpo_update_of_trigger + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); +CREATE TRIGGER fpo_before_row2 + BEFORE UPDATE OF valid_at ON fpo_update_of_trigger + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); +UPDATE fpo_update_of_trigger + FOR PORTION OF valid_at FROM 30 TO 70 + SET id = 2; +DROP TABLE fpo_update_of_trigger; + RESET datestyle; diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql index 95a46854b37..6cd9bb840ff 100644 --- a/src/test/regress/sql/privileges.sql +++ b/src/test/regress/sql/privileges.sql @@ -790,15 +790,23 @@ CREATE TABLE t1 ( valid_at tsrange, CONSTRAINT t1pk PRIMARY KEY (c1, valid_at WITHOUT OVERLAPS) ); --- UPDATE requires select permission on the valid_at column (but not update): +-- UPDATE requires select and update permission on the valid_at column: GRANT SELECT (c1) ON t1 TO regress_priv_user2; GRANT UPDATE (c1) ON t1 TO regress_priv_user2; GRANT SELECT (c1, valid_at) ON t1 TO regress_priv_user3; GRANT UPDATE (c1) ON t1 TO regress_priv_user3; +GRANT SELECT (c1) ON t1 TO regress_priv_user4; +GRANT UPDATE (c1, valid_at) ON t1 TO regress_priv_user4; +GRANT SELECT (c1, valid_at) ON t1 TO regress_priv_user5; +GRANT UPDATE (c1, valid_at) ON t1 TO regress_priv_user5; SET SESSION AUTHORIZATION regress_priv_user2; UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)'; SET SESSION AUTHORIZATION regress_priv_user3; UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)'; +SET SESSION AUTHORIZATION regress_priv_user4; +UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)'; +SET SESSION AUTHORIZATION regress_priv_user5; +UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)'; SET SESSION AUTHORIZATION regress_priv_user1; -- DELETE requires select permission on the valid_at column: GRANT DELETE ON t1 TO regress_priv_user2;