/
githubmirror
/
postgres
Обзор
Документация
Войти
/
githubmirror
/
postgres
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/test/regress/sql/for_portion_of.sql
1 810 строк
59 KB
Dean Rasheed
Avoid RETURNING side effects for FOR PORTION OF leftovers.
28 июл 2026, 11:44
28 июл 2026, 11:44
62d1a5f
Код
Авторство
О чём код?
-- Tests for UPDATE/DELETE FOR PORTION OF SET datestyle TO ISO, YMD; -- Works on non-PK columns CREATE TABLE for_portion_of_test ( id int4range, valid_at daterange, name text NOT NULL ); INSERT INTO for_portion_of_test (id, valid_at, name) VALUES ('[1,2)', '[2018-01-02,2020-01-01)', 'one'); UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01' SET name = 'one^1'; SELECT * FROM for_portion_of_test ORDER BY id, valid_at; DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM '2019-01-15' TO '2019-01-20'; SELECT * FROM for_portion_of_test ORDER BY id, valid_at; -- With a table alias with AS UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2019-02-01' TO '2019-02-03' AS t SET name = 'one^2'; DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM '2019-02-03' TO '2019-02-04' AS t; -- With a table alias without AS UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2019-02-04' TO '2019-02-05' t SET name = 'one^3'; DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM '2019-02-05' TO '2019-02-06' t; -- UPDATE with FROM UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2019-03-01' to '2019-03-02' SET name = 'one^4' FROM (SELECT '[1,2)'::int4range) AS t2(id) WHERE for_portion_of_test.id = t2.id; -- DELETE with USING DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM '2019-03-02' TO '2019-03-03' USING (SELECT '[1,2)'::int4range) AS t2(id) WHERE for_portion_of_test.id = t2.id; SELECT * FROM for_portion_of_test ORDER BY id, valid_at; -- Works on more than one range DROP TABLE for_portion_of_test; CREATE TABLE for_portion_of_test ( id int4range, valid1_at daterange, valid2_at daterange, name text NOT NULL ); INSERT INTO for_portion_of_test (id, valid1_at, valid2_at, name) VALUES ('[1,2)', '[2018-01-02,2018-02-03)', '[2015-01-01,2025-01-01)', 'one'); UPDATE for_portion_of_test FOR PORTION OF valid1_at FROM '2018-01-15' TO NULL SET name = 'foo'; SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at; UPDATE for_portion_of_test FOR PORTION OF valid2_at FROM '2018-01-15' TO NULL SET name = 'bar'; SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at; DELETE FROM for_portion_of_test FOR PORTION OF valid1_at FROM '2018-01-20' TO NULL; SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at; DELETE FROM for_portion_of_test FOR PORTION OF valid2_at FROM '2018-01-20' TO NULL; SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at; -- Test with NULLs in the scalar/range key columns. -- This won't happen if there is a PRIMARY KEY or UNIQUE constraint -- but FOR PORTION OF shouldn't require that. DROP TABLE for_portion_of_test; CREATE UNLOGGED TABLE for_portion_of_test ( id int4range, valid_at daterange, name text ); INSERT INTO for_portion_of_test (id, valid_at, name) VALUES ('[1,2)', NULL, '1 null'), ('[1,2)', '(,)', '1 unbounded'), ('[1,2)', 'empty', '1 empty'), (NULL, NULL, NULL), (NULL, daterange('2018-01-01', '2019-01-01'), 'null key'); UPDATE for_portion_of_test FOR PORTION OF valid_at FROM NULL TO NULL SET name = 'NULL to NULL'; SELECT * FROM for_portion_of_test ORDER BY id, valid_at; DROP TABLE for_portion_of_test; -- -- UPDATE tests -- CREATE TABLE for_portion_of_test ( id int4range NOT NULL, valid_at daterange NOT NULL, name text NOT NULL, CONSTRAINT for_portion_of_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) ); INSERT INTO for_portion_of_test (id, valid_at, name) VALUES ('[1,2)', '[2018-01-02,2018-02-03)', 'one'), ('[1,2)', '[2018-02-03,2018-03-03)', 'one'), ('[1,2)', '[2018-03-03,2018-04-04)', 'one'), ('[2,3)', '[2018-01-01,2018-01-05)', 'two'), ('[3,4)', '[2018-01-01,)', 'three'), ('[4,5)', '(,2018-04-01)', 'four'), ('[5,6)', '(,)', 'five') ; \set QUIET false -- Updating with a missing column fails UPDATE for_portion_of_test FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL SET name = 'foo' WHERE id = '[5,6)'; -- Updating the range fails UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-06-01' TO NULL SET valid_at = '[1990-01-01,1999-01-01)' WHERE id = '[5,6)'; -- The wrong start type fails UPDATE for_portion_of_test FOR PORTION OF valid_at FROM 1 TO '2020-01-01' SET name = 'nope' WHERE id = '[3,4)'; -- The wrong end type fails UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2000-01-01' TO 4 SET name = 'nope' WHERE id = '[3,4)'; -- Updating with timestamps reversed fails UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01' SET name = 'three^1' WHERE id = '[3,4)'; -- Updating with a subquery fails UPDATE for_portion_of_test FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '2018-06-01' SET name = 'nope' WHERE id = '[3,4)'; -- Updating with a column fails UPDATE for_portion_of_test FOR PORTION OF valid_at FROM lower(valid_at) TO NULL SET name = 'nope' WHERE id = '[3,4)'; -- Updating with timestamps equal does nothing UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01' SET name = 'three^0' WHERE id = '[3,4)'; -- Updating a finite/open portion with a finite/open target UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-06-01' TO NULL SET name = 'three^1' WHERE id = '[3,4)'; SELECT * FROM for_portion_of_test WHERE id = '[3,4)' ORDER BY id, valid_at; -- Updating a finite/open portion with an open/finite target UPDATE for_portion_of_test FOR PORTION OF valid_at FROM NULL TO '2018-03-01' SET name = 'three^2' WHERE id = '[3,4)'; SELECT * FROM for_portion_of_test WHERE id = '[3,4)' ORDER BY id, valid_at; -- Updating an open/finite portion with an open/finite target UPDATE for_portion_of_test FOR PORTION OF valid_at FROM NULL TO '2018-02-01' SET name = 'four^1' WHERE id = '[4,5)'; SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at; -- Updating an open/finite portion with a finite/open target UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2017-01-01' TO NULL SET name = 'four^2' WHERE id = '[4,5)'; SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at; -- Updating a finite/finite portion with an exact fit UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2017-01-01' TO '2018-02-01' SET name = 'four^3' WHERE id = '[4,5)'; SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at; -- Updating an enclosed span UPDATE for_portion_of_test FOR PORTION OF valid_at FROM NULL TO NULL SET name = 'two^2' WHERE id = '[2,3)'; SELECT * FROM for_portion_of_test WHERE id = '[2,3)' ORDER BY id, valid_at; -- Updating an open/open portion with a finite/finite target UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-01-01' TO '2019-01-01' SET name = 'five^1' WHERE id = '[5,6)'; SELECT * FROM for_portion_of_test WHERE id = '[5,6)' ORDER BY id, valid_at; -- Updating an enclosed span with separate protruding spans UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2017-01-01' TO '2020-01-01' SET name = 'five^2' WHERE id = '[5,6)'; SELECT * FROM for_portion_of_test WHERE id = '[5,6)' ORDER BY id, valid_at; -- Updating multiple enclosed spans UPDATE for_portion_of_test FOR PORTION OF valid_at FROM NULL TO NULL SET name = 'one^2' WHERE id = '[1,2)'; SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; -- Updating with a direct target UPDATE for_portion_of_test FOR PORTION OF valid_at (daterange('2018-03-10', '2018-03-15')) SET name = 'one^3' WHERE id = '[1,2)'; SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; -- Updating with a direct target, coerced from a string UPDATE for_portion_of_test FOR PORTION OF valid_at ('[2018-03-15,2018-03-17)') SET name = 'one^3' WHERE id = '[1,2)'; SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; -- Updating with a direct target of the wrong range subtype fails UPDATE for_portion_of_test FOR PORTION OF valid_at (int4range(1, 4)) SET name = 'one^3' WHERE id = '[1,2)'; -- Updating with a direct target of a non-rangetype fails UPDATE for_portion_of_test FOR PORTION OF valid_at (4) SET name = 'one^3' WHERE id = '[1,2)'; -- Updating with a direct target of NULL fails UPDATE for_portion_of_test FOR PORTION OF valid_at (NULL) SET name = 'one^3' WHERE id = '[1,2)'; -- Updating with a direct target of empty does nothing UPDATE for_portion_of_test FOR PORTION OF valid_at ('empty') SET name = 'one^3' WHERE id = '[1,2)'; SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; -- Updating the non-range part of the PK: UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-02-15' TO NULL SET id = '[6,7)' WHERE id = '[1,2)'; SELECT * FROM for_portion_of_test WHERE id IN ('[1,2)', '[6,7)') ORDER BY id, valid_at; -- UPDATE with no WHERE clause UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2030-01-01' TO NULL SET name = name || '*'; SELECT * FROM for_portion_of_test ORDER BY id, valid_at; \set QUIET true -- Updating with a shift/reduce conflict -- (requires a tsrange column) CREATE UNLOGGED TABLE for_portion_of_test2 ( id int4range, valid_at tsrange, name text ); INSERT INTO for_portion_of_test2 (id, valid_at, name) VALUES ('[1,2)', '[2000-01-01,2020-01-01)', 'one'); -- updates [2011-03-01 01:02:00, 2012-01-01) (note 2 minutes) UPDATE for_portion_of_test2 FOR PORTION OF valid_at FROM '2011-03-01'::timestamp + INTERVAL '1:02:03' HOUR TO MINUTE TO '2012-01-01' SET name = 'one^1' WHERE id = '[1,2)'; -- TO is used for the bound but not the INTERVAL: -- syntax error UPDATE for_portion_of_test2 FOR PORTION OF valid_at FROM '2013-03-01'::timestamp + INTERVAL '1:02:03' HOUR TO '2014-01-01' SET name = 'one^2' WHERE id = '[1,2)'; -- adding parens fixes it -- updates [2015-03-01 01:00:00, 2016-01-01) (no minutes) UPDATE for_portion_of_test2 FOR PORTION OF valid_at FROM ('2015-03-01'::timestamp + INTERVAL '1:02:03' HOUR) TO '2016-01-01' SET name = 'one^3' WHERE id = '[1,2)'; SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at; DROP TABLE for_portion_of_test2; -- UPDATE FOR PORTION OF in a CTE: -- The outer query sees the table how it was before the updates, -- and with no leftovers yet, -- but it also sees the new values via the RETURNING clause. -- (We test RETURNING more directly, without a CTE, below.) INSERT INTO for_portion_of_test (id, valid_at, name) VALUES ('[10,11)', '[2018-01-01,2020-01-01)', 'ten'); WITH update_apr AS ( UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-05-01' SET name = 'Apr 2018' WHERE id = '[10,11)' RETURNING id, valid_at, name ) SELECT * FROM for_portion_of_test AS t, update_apr WHERE t.id = update_apr.id; SELECT * FROM for_portion_of_test WHERE id = '[10,11)' ORDER BY id, valid_at; -- UPDATE FOR PORTION OF with current_date -- (We take care not to make the expectation depend on the timestamp.) INSERT INTO for_portion_of_test (id, valid_at, name) VALUES ('[99,100)', '[2000-01-01,)', 'foo'); UPDATE for_portion_of_test FOR PORTION OF valid_at FROM current_date TO null SET name = 'bar' WHERE id = '[99,100)'; SELECT name, lower(valid_at) FROM for_portion_of_test WHERE id = '[99,100)' AND valid_at @> current_date - 1; SELECT name, upper(valid_at) FROM for_portion_of_test WHERE id = '[99,100)' AND valid_at @> current_date + 1; -- UPDATE FOR PORTION OF with clock_timestamp() -- fails because the function is volatile: UPDATE for_portion_of_test FOR PORTION OF valid_at FROM clock_timestamp()::date TO null SET name = 'baz' WHERE id = '[99,100)'; -- clean up: DELETE FROM for_portion_of_test WHERE id = '[99,100)'; -- Not visible to UPDATE: -- Tuples updated/inserted within the CTE are not visible to the main query yet, -- but neither are old tuples the CTE changed: -- (This is the same behavior as without FOR PORTION OF.) INSERT INTO for_portion_of_test (id, valid_at, name) VALUES ('[11,12)', '[2018-01-01,2020-01-01)', 'eleven'); WITH update_apr AS ( UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-05-01' SET name = 'Apr 2018' WHERE id = '[11,12)' RETURNING id, valid_at, name ) UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01' AS t SET name = 'May 2018' FROM update_apr AS j WHERE t.id = j.id; SELECT * FROM for_portion_of_test WHERE id = '[11,12)' ORDER BY id, valid_at; DELETE FROM for_portion_of_test WHERE id IN ('[10,11)', '[11,12)'); -- UPDATE FOR PORTION OF in a PL/pgSQL function INSERT INTO for_portion_of_test (id, valid_at, name) VALUES ('[10,11)', '[2018-01-01,2020-01-01)', 'ten'); CREATE FUNCTION fpo_update(_id int4range, _target_from date, _target_til date) RETURNS void LANGUAGE plpgsql AS $$ BEGIN UPDATE for_portion_of_test FOR PORTION OF valid_at FROM $2 TO $3 SET name = concat(_target_from::text, ' to ', _target_til::text) WHERE id = $1; END; $$; SELECT fpo_update('[10,11)', '2015-01-01', '2019-01-01'); SELECT * FROM for_portion_of_test WHERE id = '[10,11)'; -- UPDATE FOR PORTION OF in a compiled SQL function CREATE FUNCTION fpo_update() RETURNS text BEGIN ATOMIC UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01' SET name = 'one^1' RETURNING name; END; \sf+ fpo_update() CREATE OR REPLACE function fpo_update() RETURNS text BEGIN ATOMIC UPDATE for_portion_of_test FOR PORTION OF valid_at (daterange('2018-01-15', '2020-01-01') * daterange('2019-01-01', '2022-01-01')) SET name = 'one^1' RETURNING name; END; \sf+ fpo_update() DROP FUNCTION fpo_update(); DROP TABLE for_portion_of_test; -- -- DELETE tests -- CREATE TABLE for_portion_of_test ( id int4range NOT NULL, valid_at daterange NOT NULL, name text NOT NULL, CONSTRAINT for_portion_of_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) ); INSERT INTO for_portion_of_test (id, valid_at, name) VALUES ('[1,2)', '[2018-01-02,2018-02-03)', 'one'), ('[1,2)', '[2018-02-03,2018-03-03)', 'one'), ('[1,2)', '[2018-03-03,2018-04-04)', 'one'), ('[2,3)', '[2018-01-01,2018-01-05)', 'two'), ('[3,4)', '[2018-01-01,)', 'three'), ('[4,5)', '(,2018-04-01)', 'four'), ('[5,6)', '(,)', 'five'), ('[6,7)', '[2018-01-01,)', 'six'), ('[7,8)', '(,2018-04-01)', 'seven'), ('[8,9)', '[2018-01-02,2018-02-03)', 'eight'), ('[8,9)', '[2018-02-03,2018-03-03)', 'eight'), ('[8,9)', '[2018-03-03,2018-04-04)', 'eight') ; \set QUIET false -- Deleting with a missing column fails DELETE FROM for_portion_of_test FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL WHERE id = '[5,6)'; -- The wrong start type fails DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM 1 TO '2020-01-01' WHERE id = '[3,4)'; -- The wrong end type fails DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM '2000-01-01' TO 4 WHERE id = '[3,4)'; -- Deleting with timestamps reversed fails DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01' WHERE id = '[3,4)'; -- Deleting with a subquery fails DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '2018-06-01' WHERE id = '[3,4)'; -- Deleting with a column fails DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM lower(valid_at) TO NULL WHERE id = '[3,4)'; -- Deleting with timestamps equal does nothing DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01' WHERE id = '[3,4)'; -- Deleting a finite/open portion with a finite/open target DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM '2018-06-01' TO NULL WHERE id = '[3,4)'; SELECT * FROM for_portion_of_test WHERE id = '[3,4)' ORDER BY id, valid_at; -- Deleting a finite/open portion with an open/finite target DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM NULL TO '2018-03-01' WHERE id = '[6,7)'; SELECT * FROM for_portion_of_test WHERE id = '[6,7)' ORDER BY id, valid_at; -- Deleting an open/finite portion with an open/finite target DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM NULL TO '2018-02-01' WHERE id = '[4,5)'; SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at; -- Deleting an open/finite portion with a finite/open target DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM '2017-01-01' TO NULL WHERE id = '[7,8)'; SELECT * FROM for_portion_of_test WHERE id = '[7,8)' ORDER BY id, valid_at; -- Deleting a finite/finite portion with an exact fit DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-04-01' WHERE id = '[4,5)'; SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at; -- Deleting an enclosed span DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM NULL TO NULL WHERE id = '[2,3)'; SELECT * FROM for_portion_of_test WHERE id = '[2,3)' ORDER BY id, valid_at; -- Deleting an open/open portion with a finite/finite target DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM '2018-01-01' TO '2019-01-01' WHERE id = '[5,6)'; SELECT * FROM for_portion_of_test WHERE id = '[5,6)' ORDER BY id, valid_at; -- Deleting an enclosed span with separate protruding spans DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM '2018-02-03' TO '2018-03-03' WHERE id = '[1,2)'; SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; -- Deleting multiple enclosed spans DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM NULL TO NULL WHERE id = '[8,9)'; SELECT * FROM for_portion_of_test WHERE id = '[8,9)' ORDER BY id, valid_at; -- Deleting with a direct target DELETE FROM for_portion_of_test FOR PORTION OF valid_at (daterange('2018-03-10', '2018-03-15')) WHERE id = '[1,2)'; SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; -- Deleting with a direct target, coerced from a string DELETE FROM for_portion_of_test FOR PORTION OF valid_at ('[2018-03-15,2018-03-17)') WHERE id = '[1,2)'; SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; -- Deleting with a direct target of the wrong range subtype fails DELETE FROM for_portion_of_test FOR PORTION OF valid_at (int4range(1, 4)) WHERE id = '[1,2)'; -- Deleting with a direct target of a non-rangetype fails DELETE FROM for_portion_of_test FOR PORTION OF valid_at (4) WHERE id = '[1,2)'; -- Deleting with a direct target of NULL fails DELETE FROM for_portion_of_test FOR PORTION OF valid_at (NULL) WHERE id = '[1,2)'; -- Deleting with a direct target of empty does nothing DELETE FROM for_portion_of_test FOR PORTION OF valid_at ('empty') WHERE id = '[1,2)'; SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; -- DELETE with no WHERE clause DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM '2030-01-01' TO NULL; SELECT * FROM for_portion_of_test ORDER BY id, valid_at; \set QUIET true -- UPDATE ... RETURNING returns only the updated values -- (not the inserted side values, which are added by a separate "statement"): CREATE FUNCTION fpo_returning_row(text) RETURNS text LANGUAGE plpgsql AS $$ BEGIN RAISE NOTICE 'RETURNING %', $1; RETURN $1; END; $$; UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-02-15' SET name = 'three^3' WHERE id = '[3,4)' RETURNING *, fpo_returning_row(for_portion_of_test::text); -- UPDATE ... RETURNING supports NEW and OLD valid_at UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-02-10' TO '2018-02-20' SET name = 'three^4' WHERE id = '[3,4)' RETURNING OLD.name, NEW.name, OLD.valid_at, NEW.valid_at; -- DELETE FOR PORTION OF with current_date -- (We take care not to make the expectation depend on the timestamp.) INSERT INTO for_portion_of_test (id, valid_at, name) VALUES ('[99,100)', '[2000-01-01,)', 'foo'); DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM current_date TO null WHERE id = '[99,100)'; SELECT name, lower(valid_at) FROM for_portion_of_test WHERE id = '[99,100)' AND valid_at @> current_date - 1; SELECT name, upper(valid_at) FROM for_portion_of_test WHERE id = '[99,100)' AND valid_at @> current_date + 1; -- DELETE FOR PORTION OF with clock_timestamp() -- fails because the function is volatile: DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM clock_timestamp()::date TO null WHERE id = '[99,100)'; -- clean up: DELETE FROM for_portion_of_test WHERE id = '[99,100)'; -- DELETE ... RETURNING returns the deleted values, regardless of bounds -- (not the inserted side values, which are added by a separate "statement"): DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM '2018-02-02' TO '2018-02-03' WHERE id = '[3,4)' RETURNING *, fpo_returning_row(for_portion_of_test::text); -- DELETE FOR PORTION OF in a PL/pgSQL function INSERT INTO for_portion_of_test (id, valid_at, name) VALUES ('[10,11)', '[2018-01-01,2020-01-01)', 'ten'); CREATE FUNCTION fpo_delete(_id int4range, _target_from date, _target_til date) RETURNS void LANGUAGE plpgsql AS $$ BEGIN DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM $2 TO $3 WHERE id = $1; END; $$; SELECT fpo_delete('[10,11)', '2015-01-01', '2019-01-01'); SELECT * FROM for_portion_of_test WHERE id = '[10,11)'; DELETE FROM for_portion_of_test WHERE id IN ('[10,11)'); -- DELETE FOR PORTION OF in a compiled SQL function CREATE FUNCTION fpo_delete() RETURNS text BEGIN ATOMIC DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01' RETURNING name; END; \sf+ fpo_delete() CREATE OR REPLACE function fpo_delete() RETURNS text BEGIN ATOMIC DELETE FROM for_portion_of_test FOR PORTION OF valid_at (daterange('2018-01-15', '2020-01-01') * daterange('2019-01-01', '2022-01-01')) RETURNING name; END; \sf+ fpo_delete() DROP FUNCTION fpo_delete(); -- test domains and CHECK constraints -- With a domain on a rangetype CREATE DOMAIN daterange_d AS daterange CHECK (upper(VALUE) <> '2005-05-05'::date); CREATE TABLE for_portion_of_test2 ( id integer, valid_at daterange_d, name text ); INSERT INTO for_portion_of_test2 VALUES (1, '[2000-01-01,2020-01-01)', 'one'), (2, '[2000-01-01,2020-01-01)', 'two'); INSERT INTO for_portion_of_test2 VALUES (1, '[2000-01-01,2005-05-05)', 'nope'); -- UPDATE works: UPDATE for_portion_of_test2 FOR PORTION OF valid_at FROM '2010-01-01' TO '2010-01-05' SET name = 'one^1' WHERE id = 1; UPDATE for_portion_of_test2 FOR PORTION OF valid_at ('[2010-01-07,2010-01-09)') SET name = 'one^2' WHERE id = 1; SELECT * FROM for_portion_of_test2 WHERE id = 1 ORDER BY valid_at; -- The target is allowed to violate the domain: UPDATE for_portion_of_test2 FOR PORTION OF valid_at FROM '1999-01-01' TO '2005-05-05' SET name = 'miss' WHERE id = -1; UPDATE for_portion_of_test2 FOR PORTION OF valid_at ('[1999-01-01,2005-05-05)') SET name = 'miss' WHERE id = -1; -- test the updated row violating the domain UPDATE for_portion_of_test2 FOR PORTION OF valid_at FROM '1999-01-01' TO '2005-05-05' SET name = 'one^3' WHERE id = 1; -- test inserts violating the domain UPDATE for_portion_of_test2 FOR PORTION OF valid_at FROM '2005-05-05' TO '2010-01-01' SET name = 'one^3' WHERE id = 1; -- test updated row violating CHECK constraints ALTER TABLE for_portion_of_test2 ADD CONSTRAINT fpo2_check CHECK (upper(valid_at) <> '2001-01-11'); UPDATE for_portion_of_test2 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-11' SET name = 'one^3' WHERE id = 1; ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check; -- test inserts violating CHECK constraints ALTER TABLE for_portion_of_test2 ADD CONSTRAINT fpo2_check CHECK (lower(valid_at) <> '2002-02-02'); UPDATE for_portion_of_test2 FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-02-02' SET name = 'one^3' WHERE id = 1; ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check; SELECT * FROM for_portion_of_test2 WHERE id = 1 ORDER BY valid_at; -- DELETE works: DELETE FROM for_portion_of_test2 FOR PORTION OF valid_at FROM '2010-01-01' TO '2010-01-05' WHERE id = 2; DELETE FROM for_portion_of_test2 FOR PORTION OF valid_at ('[2010-01-07,2010-01-09)') WHERE id = 2; SELECT * FROM for_portion_of_test2 WHERE id = 2 ORDER BY valid_at; -- The target is allowed to violate the domain: DELETE FROM for_portion_of_test2 FOR PORTION OF valid_at FROM '1999-01-01' TO '2005-05-05' WHERE id = -1; DELETE FROM for_portion_of_test2 FOR PORTION OF valid_at ('[1999-01-01,2005-05-05)') WHERE id = -1; -- test inserts violating the domain DELETE FROM for_portion_of_test2 FOR PORTION OF valid_at FROM '2005-05-05' TO '2010-01-01' WHERE id = 2; -- test inserts violating CHECK constraints ALTER TABLE for_portion_of_test2 ADD CONSTRAINT fpo2_check CHECK (lower(valid_at) <> '2002-02-02'); DELETE FROM for_portion_of_test2 FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-02-02' WHERE id = 2; ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check; SELECT * FROM for_portion_of_test2 WHERE id = 2 ORDER BY valid_at; DROP TABLE for_portion_of_test2; -- With a domain on a multirangetype CREATE FUNCTION multirange_lowers(mr anymultirange) RETURNS anyarray LANGUAGE sql AS $$ SELECT array_agg(lower(r)) FROM UNNEST(mr) u(r); $$; CREATE FUNCTION multirange_uppers(mr anymultirange) RETURNS anyarray LANGUAGE sql AS $$ SELECT array_agg(upper(r)) FROM UNNEST(mr) u(r); $$; CREATE DOMAIN datemultirange_d AS datemultirange CHECK (NOT '2005-05-05'::date = ANY (multirange_uppers(VALUE))); CREATE TABLE for_portion_of_test2 ( id integer, valid_at datemultirange_d, name text ); INSERT INTO for_portion_of_test2 VALUES (1, '{[2000-01-01,2020-01-01)}', 'one'), (2, '{[2000-01-01,2020-01-01)}', 'two'); INSERT INTO for_portion_of_test2 VALUES (1, '{[2000-01-01,2005-05-05)}', 'nope'); -- UPDATE works: UPDATE for_portion_of_test2 FOR PORTION OF valid_at ('{[2010-01-07,2010-01-09)}') SET name = 'one^2' WHERE id = 1; SELECT * FROM for_portion_of_test2 WHERE id = 1 ORDER BY valid_at; -- The target is allowed to violate the domain: UPDATE for_portion_of_test2 FOR PORTION OF valid_at ('{[1999-01-01,2005-05-05)}') SET name = 'miss' WHERE id = -1; -- test the updated row violating the domain UPDATE for_portion_of_test2 FOR PORTION OF valid_at ('{[1999-01-01,2005-05-05)}') SET name = 'one^3' WHERE id = 1; -- test inserts violating the domain UPDATE for_portion_of_test2 FOR PORTION OF valid_at ('{[2005-05-05,2010-01-01)}') SET name = 'one^3' WHERE id = 1; -- test updated row violating CHECK constraints ALTER TABLE for_portion_of_test2 ADD CONSTRAINT fpo2_check CHECK (upper(valid_at) <> '2001-01-11'); UPDATE for_portion_of_test2 FOR PORTION OF valid_at ('{[2000-01-01,2001-01-11)}') SET name = 'one^3' WHERE id = 1; ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check; -- test inserts violating CHECK constraints ALTER TABLE for_portion_of_test2 ADD CONSTRAINT fpo2_check CHECK (NOT '2002-02-02'::date = ANY (multirange_lowers(valid_at))); UPDATE for_portion_of_test2 FOR PORTION OF valid_at ('{[2001-01-01,2002-02-02)}') SET name = 'one^3' WHERE id = 1; ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check; SELECT * FROM for_portion_of_test2 WHERE id = 1 ORDER BY valid_at; -- DELETE works: DELETE FROM for_portion_of_test2 FOR PORTION OF valid_at ('{[2010-01-07,2010-01-09)}') WHERE id = 2; SELECT * FROM for_portion_of_test2 WHERE id = 2 ORDER BY valid_at; -- The target is allowed to violate the domain: DELETE FROM for_portion_of_test2 FOR PORTION OF valid_at ('{[1999-01-01,2005-05-05)}') WHERE id = -1; -- test inserts violating the domain DELETE FROM for_portion_of_test2 FOR PORTION OF valid_at ('{[2005-05-05,2010-01-01)}') WHERE id = 2; -- test inserts violating CHECK constraints ALTER TABLE for_portion_of_test2 ADD CONSTRAINT fpo2_check CHECK (NOT '2002-02-02'::date = ANY (multirange_lowers(valid_at))); DELETE FROM for_portion_of_test2 FOR PORTION OF valid_at ('{[2001-01-01,2002-02-02)}') WHERE id = 2; ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check; SELECT * FROM for_portion_of_test2 WHERE id = 2 ORDER BY valid_at; DROP TABLE for_portion_of_test2; -- test on non-range/multirange columns -- With a direct target and a scalar column CREATE TABLE for_portion_of_test2 ( id integer, valid_at date, name text ); INSERT INTO for_portion_of_test2 VALUES (1, '2020-01-01', 'one'); UPDATE for_portion_of_test2 FOR PORTION OF valid_at ('2010-01-01') SET name = 'one^1'; DELETE FROM for_portion_of_test2 FOR PORTION OF valid_at ('2010-01-01'); DROP TABLE for_portion_of_test2; -- With a direct target and a non-{,multi}range gistable column without overlaps CREATE TABLE for_portion_of_test2 ( id integer, valid_at point, name text ); INSERT INTO for_portion_of_test2 VALUES (1, '0,0', 'one'); UPDATE for_portion_of_test2 FOR PORTION OF valid_at ('1,1') SET name = 'one^1'; DELETE FROM for_portion_of_test2 FOR PORTION OF valid_at ('1,1'); DROP TABLE for_portion_of_test2; -- With a direct target and a non-{,multi}range column with overlaps CREATE TABLE for_portion_of_test2 ( id integer, valid_at box, name text ); INSERT INTO for_portion_of_test2 VALUES (1, '0,0,4,4', 'one'); UPDATE for_portion_of_test2 FOR PORTION OF valid_at ('1,1,2,2') SET name = 'one^1'; DELETE FROM for_portion_of_test2 FOR PORTION OF valid_at ('1,1,2,2'); DROP TABLE for_portion_of_test2; -- test that we run triggers on the UPDATE/DELETEd row and the INSERTed rows CREATE FUNCTION dump_trigger() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RAISE NOTICE '%: % % %:', TG_NAME, TG_WHEN, TG_OP, TG_LEVEL; IF TG_ARGV[0] THEN RAISE NOTICE ' old: %', (SELECT string_agg(old_table::text, '\n ') FROM old_table); ELSE RAISE NOTICE ' old: %', OLD.valid_at; END IF; IF TG_ARGV[1] THEN RAISE NOTICE ' new: %', (SELECT string_agg(new_table::text, '\n ') FROM new_table); ELSE RAISE NOTICE ' new: %', NEW.valid_at; END IF; IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN RETURN NEW; ELSIF TG_OP = 'DELETE' THEN RETURN OLD; END IF; END; $$; -- statement triggers: 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); CREATE TRIGGER fpo_after_update_stmt AFTER UPDATE ON for_portion_of_test FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); CREATE TRIGGER fpo_after_delete_stmt AFTER DELETE ON for_portion_of_test FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); -- row triggers: 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); CREATE TRIGGER fpo_after_update_row AFTER UPDATE ON for_portion_of_test FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); CREATE TRIGGER fpo_after_delete_row AFTER DELETE ON for_portion_of_test FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2021-01-01' TO '2022-01-01' SET name = 'five^3' WHERE id = '[5,6)'; DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM '2023-01-01' TO '2024-01-01' WHERE id = '[5,6)'; SELECT * FROM for_portion_of_test ORDER BY id, valid_at; -- Triggers with a custom transition table name: DROP TABLE for_portion_of_test; CREATE TABLE for_portion_of_test ( id int4range, valid_at daterange, name text ); INSERT INTO for_portion_of_test VALUES ('[1,2)', '[2018-01-01,2020-01-01)', 'one'); -- statement triggers: 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_after_insert_stmt AFTER INSERT ON for_portion_of_test REFERENCING NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, true); CREATE TRIGGER fpo_after_update_stmt AFTER UPDATE ON for_portion_of_test REFERENCING NEW TABLE AS new_table OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(true, true); CREATE TRIGGER fpo_after_delete_stmt AFTER DELETE ON for_portion_of_test REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(true, false); -- row triggers: 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_after_insert_row AFTER INSERT ON for_portion_of_test REFERENCING NEW TABLE AS new_table FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, true); CREATE TRIGGER fpo_after_update_row AFTER UPDATE ON for_portion_of_test REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table FOR EACH ROW EXECUTE PROCEDURE dump_trigger(true, true); CREATE TRIGGER fpo_after_delete_row AFTER DELETE ON for_portion_of_test REFERENCING OLD TABLE AS old_table FOR EACH ROW EXECUTE PROCEDURE dump_trigger(true, false); BEGIN; UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01' SET name = '2018-01-15_to_2019-01-01'; ROLLBACK; BEGIN; DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM NULL TO '2018-01-21'; ROLLBACK; BEGIN; UPDATE for_portion_of_test FOR PORTION OF valid_at FROM NULL TO '2018-01-02' SET name = 'NULL_to_2018-01-01'; ROLLBACK; -- Deferred triggers -- (must be CONSTRAINT triggers thus AFTER ROW with no transition tables) DROP TABLE for_portion_of_test; CREATE TABLE for_portion_of_test ( id int4range, valid_at daterange, name text ); INSERT INTO for_portion_of_test (id, valid_at, name) VALUES ('[1,2)', '[2018-01-01,2020-01-01)', 'one'); CREATE CONSTRAINT TRIGGER fpo_after_insert_row AFTER INSERT ON for_portion_of_test DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); CREATE CONSTRAINT TRIGGER fpo_after_update_row AFTER UPDATE ON for_portion_of_test DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); CREATE CONSTRAINT TRIGGER fpo_after_delete_row AFTER DELETE ON for_portion_of_test DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); BEGIN; UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01' SET name = '2018-01-15_to_2019-01-01'; COMMIT; BEGIN; DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM NULL TO '2018-01-21'; COMMIT; BEGIN; UPDATE for_portion_of_test FOR PORTION OF valid_at FROM NULL TO '2018-01-02' SET name = 'NULL_to_2018-01-01'; COMMIT; SELECT * FROM for_portion_of_test; -- test FOR PORTION OF from triggers during FOR PORTION OF: DROP TABLE for_portion_of_test; CREATE TABLE for_portion_of_test ( id int4range, valid_at daterange, name text ); INSERT INTO for_portion_of_test (id, valid_at, name) VALUES ('[1,2)', '[2018-01-01,2020-01-01)', 'one'), ('[2,3)', '[2018-01-01,2020-01-01)', 'two'), ('[3,4)', '[2018-01-01,2020-01-01)', 'three'), ('[4,5)', '[2018-01-01,2020-01-01)', 'four'); CREATE FUNCTION trg_fpo_update() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN IF pg_trigger_depth() = 1 THEN UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-03-01' SET name = CONCAT(name, '^') WHERE id = OLD.id; END IF; RETURN CASE WHEN 'TG_OP' = 'DELETE' THEN OLD ELSE NEW END; END; $$; CREATE FUNCTION trg_fpo_delete() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN IF pg_trigger_depth() = 1 THEN DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM '2018-03-01' TO '2018-04-01' WHERE id = OLD.id; END IF; RETURN CASE WHEN 'TG_OP' = 'DELETE' THEN OLD ELSE NEW END; END; $$; -- UPDATE FOR PORTION OF from a trigger fired by UPDATE FOR PORTION OF CREATE TRIGGER fpo_after_update_row AFTER UPDATE ON for_portion_of_test FOR EACH ROW EXECUTE PROCEDURE trg_fpo_update(); UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01' SET name = CONCAT(name, '*') WHERE id = '[1,2)'; SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; DROP TRIGGER fpo_after_update_row ON for_portion_of_test; -- UPDATE FOR PORTION OF from a trigger fired by DELETE FOR PORTION OF CREATE TRIGGER fpo_after_delete_row AFTER DELETE ON for_portion_of_test FOR EACH ROW EXECUTE PROCEDURE trg_fpo_update(); DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01' WHERE id = '[2,3)'; SELECT * FROM for_portion_of_test WHERE id = '[2,3)' ORDER BY id, valid_at; DROP TRIGGER fpo_after_delete_row ON for_portion_of_test; -- DELETE FOR PORTION OF from a trigger fired by UPDATE FOR PORTION OF CREATE TRIGGER fpo_after_update_row AFTER UPDATE ON for_portion_of_test FOR EACH ROW EXECUTE PROCEDURE trg_fpo_delete(); UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01' SET name = CONCAT(name, '*') WHERE id = '[3,4)'; SELECT * FROM for_portion_of_test WHERE id = '[3,4)' ORDER BY id, valid_at; DROP TRIGGER fpo_after_update_row ON for_portion_of_test; -- DELETE FOR PORTION OF from a trigger fired by DELETE FOR PORTION OF CREATE TRIGGER fpo_after_delete_row AFTER DELETE ON for_portion_of_test FOR EACH ROW EXECUTE PROCEDURE trg_fpo_delete(); DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01' WHERE id = '[4,5)'; SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at; DROP TRIGGER fpo_after_delete_row ON for_portion_of_test; -- Test that a tuple-modifying BEFORE INSERT ROW trigger acts -- consistently on both temporal leftovers. -- When FOR PORTION OF splits a row into two leftovers, both triggers -- should get the original row's values. DROP TABLE for_portion_of_test; CREATE TABLE for_portion_of_test ( id int, valid_at daterange, name text ); CREATE FUNCTION fpo_append_name_suffix() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN NEW.name := NEW.name || '+insert'; RETURN NEW; END; $$; CREATE TRIGGER fpo_before_insert_row BEFORE INSERT ON for_portion_of_test FOR EACH ROW EXECUTE PROCEDURE fpo_append_name_suffix(); INSERT INTO for_portion_of_test VALUES (1, '[2020-01-01,2020-12-31)', 'foo'); UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2020-04-01' TO '2020-08-01' SET name = 'bar' WHERE id = 1; -- Both leftovers should have the same name: 'foo+insert+insert'. SELECT * FROM for_portion_of_test ORDER BY valid_at; DROP FUNCTION fpo_append_name_suffix CASCADE; DROP TABLE for_portion_of_test; -- A BEFORE UPDATE trigger that changes the application-time column is allowed, -- even if the results are senseless. -- Note this is likely to cause a primary key violation. CREATE TABLE for_portion_of_test ( id int4range, valid_at daterange, name text ); CREATE FUNCTION trg_fpo_change_valid_at() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN NEW.valid_at = daterange('2018-01-01', '2019-01-01'); RETURN NEW; END; $$; CREATE TRIGGER fpo_before_update_row BEFORE UPDATE ON for_portion_of_test FOR EACH ROW EXECUTE PROCEDURE trg_fpo_change_valid_at(); INSERT INTO for_portion_of_test VALUES ('[1,2)', '[2010-01-01,2020-01-01)', 'foo'); UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01' SET name = CONCAT(name, '!') WHERE id = '[1,2)'; SELECT * FROM for_portion_of_test ORDER BY id, valid_at; -- A primary key should reject anything invalid: TRUNCATE for_portion_of_test; ALTER TABLE for_portion_of_test ADD CONSTRAINT for_portion_of_test_key PRIMARY KEY (id, valid_at WITHOUT OVERLAPS); INSERT INTO for_portion_of_test VALUES ('[1,2)', '[2010-01-01,2020-01-01)', 'foo'); UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01' SET name = CONCAT(name, '!') WHERE id = '[1,2)'; DROP TRIGGER fpo_before_update_row ON for_portion_of_test; DROP FUNCTION trg_fpo_change_valid_at(); DROP TABLE for_portion_of_test; -- Test with multiranges CREATE TABLE for_portion_of_test2 ( id int4range NOT NULL, valid_at datemultirange NOT NULL, name text NOT NULL, CONSTRAINT for_portion_of_test2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) ); INSERT INTO for_portion_of_test2 (id, valid_at, name) VALUES ('[1,2)', datemultirange(daterange('2018-01-02', '2018-02-03)'), daterange('2018-02-04', '2018-03-03')), 'one'), ('[1,2)', datemultirange(daterange('2018-03-03', '2018-04-04)')), 'one'), ('[2,3)', datemultirange(daterange('2018-01-01', '2018-05-01)')), 'two'), ('[3,4)', datemultirange(daterange('2018-01-01', null)), 'three'); ; -- Updating with FROM/TO UPDATE for_portion_of_test2 FOR PORTION OF valid_at FROM '2000-01-01' TO '2010-01-01' SET name = 'one^1' WHERE id = '[1,2)'; -- Updating with multirange UPDATE for_portion_of_test2 FOR PORTION OF valid_at (datemultirange(daterange('2018-01-10', '2018-02-10'), daterange('2018-03-05', '2018-05-01'))) SET name = 'one^1' WHERE id = '[1,2)'; SELECT * FROM for_portion_of_test2 WHERE id = '[1,2)' ORDER BY valid_at; -- Updating with string coercion UPDATE for_portion_of_test2 FOR PORTION OF valid_at ('{[2018-03-05,2018-03-10)}') SET name = 'one^2' WHERE id = '[1,2)'; SELECT * FROM for_portion_of_test2 WHERE id = '[1,2)' ORDER BY valid_at; -- Updating with the wrong range subtype fails UPDATE for_portion_of_test2 FOR PORTION OF valid_at ('{[1,4)}'::int4multirange) SET name = 'one^3' WHERE id = '[1,2)'; -- Updating with a non-multirangetype fails UPDATE for_portion_of_test2 FOR PORTION OF valid_at (4) SET name = 'one^3' WHERE id = '[1,2)'; -- Updating with NULL fails UPDATE for_portion_of_test2 FOR PORTION OF valid_at (NULL) SET name = 'one^3' WHERE id = '[1,2)'; -- Updating with empty does nothing UPDATE for_portion_of_test2 FOR PORTION OF valid_at ('{}') SET name = 'one^3' WHERE id = '[1,2)'; SELECT * FROM for_portion_of_test2 WHERE id = '[1,2)' ORDER BY valid_at; -- Deleting with FROM/TO UPDATE for_portion_of_test2 FOR PORTION OF valid_at FROM '2000-01-01' TO '2010-01-01' SET name = 'one^1' WHERE id = '[1,2)'; -- Deleting with multirange DELETE FROM for_portion_of_test2 FOR PORTION OF valid_at (datemultirange(daterange('2018-01-15', '2018-02-15'), daterange('2018-03-01', '2018-03-15'))) WHERE id = '[2,3)'; SELECT * FROM for_portion_of_test2 WHERE id = '[2,3)' ORDER BY valid_at; -- Deleting with string coercion DELETE FROM for_portion_of_test2 FOR PORTION OF valid_at ('{[2018-03-05,2018-03-20)}') WHERE id = '[2,3)'; SELECT * FROM for_portion_of_test2 WHERE id = '[2,3)' ORDER BY valid_at; -- Deleting with the wrong range subtype fails DELETE FROM for_portion_of_test2 FOR PORTION OF valid_at ('{[1,4)}'::int4multirange) WHERE id = '[2,3)'; -- Deleting with a non-multirangetype fails DELETE FROM for_portion_of_test2 FOR PORTION OF valid_at (4) WHERE id = '[2,3)'; -- Deleting with NULL fails DELETE FROM for_portion_of_test2 FOR PORTION OF valid_at (NULL) WHERE id = '[2,3)'; -- Deleting with empty does nothing DELETE FROM for_portion_of_test2 FOR PORTION OF valid_at ('{}') WHERE id = '[2,3)'; SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at; DROP TABLE for_portion_of_test2; -- Test with a custom range type CREATE TYPE mydaterange AS range(subtype=date); CREATE TABLE for_portion_of_test2 ( id int4range NOT NULL, valid_at mydaterange NOT NULL, name text NOT NULL, CONSTRAINT for_portion_of_test2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) ); INSERT INTO for_portion_of_test2 (id, valid_at, name) VALUES ('[1,2)', '[2018-01-02,2018-02-03)', 'one'), ('[1,2)', '[2018-02-03,2018-03-03)', 'one'), ('[1,2)', '[2018-03-03,2018-04-04)', 'one'), ('[2,3)', '[2018-01-01,2018-05-01)', 'two'), ('[3,4)', '[2018-01-01,)', 'three'); ; UPDATE for_portion_of_test2 FOR PORTION OF valid_at FROM '2018-01-10' TO '2018-02-10' SET name = 'one^1' WHERE id = '[1,2)'; DELETE FROM for_portion_of_test2 FOR PORTION OF valid_at FROM '2018-01-15' TO '2018-02-15' WHERE id = '[2,3)'; 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 stored generated 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 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 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 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 ('[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; -- Update without moving within partition 1 UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-03-01' TO '2000-04-01' SET name = 'one^1' WHERE id = '[1,2)'; -- Update without moving within partition 3 UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-03-01' TO '2000-04-01' SET name = 'three^1' WHERE id = '[3,4)'; -- Update without moving within partition 5 UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-03-01' TO '2000-04-01' SET name = 'five^1' WHERE id = '[5,6)'; -- Move from partition 1 to partition 3 UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-07-01' SET name = 'one^2', id = '[4,5)' WHERE id = '[1,2)' RETURNING id, valid_at, name, fpo_returning_row(temporal_partitioned::text); -- Move from partition 3 to partition 1 UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-07-01' SET name = 'three^2', id = '[2,3)' WHERE id = '[3,4)'; -- Move from partition 5 to partition 3 UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-07-01' SET name = 'five^2', id = '[3,4)' WHERE id = '[5,6)'; -- Update all partitions at once (each with leftovers) 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; DROP FUNCTION fpo_returning_row; DROP TABLE temporal_partitioned; -- UPDATE/DELETE FOR PORTION OF with RULEs CREATE TABLE fpo_rule (f1 bigint, f2 int4range); INSERT INTO fpo_rule VALUES (1, '[1, 11)'); CREATE RULE fpo_rule1 AS ON INSERT TO fpo_rule DO INSTEAD UPDATE fpo_rule FOR PORTION OF f2 FROM 1 TO 4 SET f1 = 2; INSERT INTO fpo_rule VALUES (1, '[1, 11)'); SELECT * FROM fpo_rule ORDER BY f1; CREATE RULE fpo_rule2 AS ON INSERT TO fpo_rule DO INSTEAD DELETE FROM fpo_rule FOR PORTION OF f2 FROM 1 TO 4; INSERT INTO fpo_rule VALUES (1, '[1, 11)'); SELECT * FROM fpo_rule ORDER BY f1; CREATE RULE fpo_rule3 AS ON DELETE TO fpo_rule DO INSTEAD UPDATE fpo_rule FOR PORTION OF f2 FROM 1 TO 8 SET f1 = 2; DELETE FROM fpo_rule FOR PORTION OF f2 FROM 1 TO 5; SELECT * FROM fpo_rule ORDER BY f1; DROP RULE fpo_rule3 ON fpo_rule; CREATE RULE fpo_rule4 AS ON UPDATE TO fpo_rule DO INSTEAD DELETE FROM fpo_rule FOR PORTION OF f2 FROM 6 TO 9; UPDATE fpo_rule FOR PORTION OF f2 FROM 4 TO 9 SET f1 = 12; SELECT * FROM fpo_rule ORDER BY f1; DROP RULE fpo_rule4 ON fpo_rule; CREATE RULE fpo_rule5 AS ON UPDATE TO fpo_rule DO ALSO DELETE FROM fpo_rule FOR PORTION OF f2 FROM 4 TO 6; UPDATE fpo_rule FOR PORTION OF f2 FROM 9 TO 10 SET f1 = 3; SELECT * FROM fpo_rule ORDER BY f1; DROP TABLE fpo_rule; -- Deparsing FOR PORTION OF must use the range column's current name, -- not the name it had when the rule was created. CREATE TABLE fpo_rename (f1 bigint, f2 int4range); CREATE TABLE fpo_rename_src (x int); CREATE RULE fpo_rename_rule1 AS ON UPDATE TO fpo_rename_src DO INSTEAD UPDATE fpo_rename FOR PORTION OF f2 FROM 3 TO 6 SET f1 = 2; CREATE RULE fpo_rename_rule2 AS ON DELETE TO fpo_rename_src DO INSTEAD DELETE FROM fpo_rename FOR PORTION OF f2 (int4range(3, 6)); \d+ fpo_rename_src ALTER TABLE fpo_rename RENAME COLUMN f1 TO ff1; ALTER TABLE fpo_rename RENAME COLUMN f2 TO ff2; \d+ fpo_rename_src DROP TABLE fpo_rename, fpo_rename_src; -- UPDATE/DELETE FOR PORTION OF on a GENERATED VIRTUAL range column: CREATE TABLE fpo_gen_virtual ( a int, b int4range GENERATED ALWAYS AS (int4range(a, a + 1)) VIRTUAL ); INSERT INTO fpo_gen_virtual VALUES (1); DELETE FROM fpo_gen_virtual FOR PORTION OF b FROM 1 TO 2; -- fails UPDATE fpo_gen_virtual FOR PORTION OF b FROM 1 TO 2 SET a = 5; -- fails DROP TABLE fpo_gen_virtual; -- UPDATE/DELETE FOR PORTION OF on a GENERATED STORED range column: CREATE TABLE fpo_gen_stored ( a int, b int4range GENERATED ALWAYS AS (int4range(a, a + 1)) STORED ); INSERT INTO fpo_gen_stored VALUES (1); DELETE FROM fpo_gen_stored FOR PORTION OF b FROM 1 TO 2; -- fails UPDATE fpo_gen_stored FOR PORTION OF b FROM 1 TO 2 SET a = 5; -- fails DROP TABLE fpo_gen_stored; -- FOR PORTION OF a generated column reached through an updatable view. -- The view hides that b is generated during parse analysis, so the check -- must happen later (in the planner), after the view is rewritten to its -- underlying table. CREATE TABLE fpo_gen_view ( a int, b int4range GENERATED ALWAYS AS (int4range(a, a + 1)) STORED ); INSERT INTO fpo_gen_view VALUES (1); CREATE VIEW fpo_gen_view_v AS SELECT * FROM fpo_gen_view; DELETE FROM fpo_gen_view_v FOR PORTION OF b FROM 1 TO 2; -- fails UPDATE fpo_gen_view_v FOR PORTION OF b FROM 1 TO 2 SET a = 5; -- fails DROP VIEW fpo_gen_view_v; DROP TABLE fpo_gen_view; -- A new-style SQL function is parsed at CREATE FUNCTION time, but our -- generated-column check is in the planner, so it sees the column's -- current attgenerated when the function's plan is built at run time. CREATE TABLE fpo_func_test ( a int, b int4range GENERATED ALWAYS AS (int4range(a, a + 1)) STORED ); INSERT INTO fpo_func_test VALUES (1); -- Definition succeeds even though b is a generated column today. CREATE FUNCTION fpo_delete() RETURNS void LANGUAGE SQL BEGIN ATOMIC DELETE FROM fpo_func_test FOR PORTION OF b FROM 1 TO 2; END; SELECT fpo_delete(); -- fails: b is generated -- Drop the generation expression and the same function now succeeds. ALTER TABLE fpo_func_test ALTER COLUMN b DROP EXPRESSION; SELECT fpo_delete(); TABLE fpo_func_test ORDER BY a, b; DROP FUNCTION fpo_delete(); DROP TABLE fpo_func_test; -- UPDATE/DELETE FOR PORTION OF with table inheritance -- Leftover rows must stay in the child table, preserving child-specific columns. CREATE TABLE fpo_inh_parent ( id int4range, valid_at daterange, name text ); CREATE TABLE fpo_inh_child ( description text ) INHERITS (fpo_inh_parent); -- Update targets the parent; the matching row lives in the child. INSERT INTO fpo_inh_child (id, valid_at, name, description) VALUES ('[1,2)', '[2018-01-01,2019-01-01)', 'one', 'initial'); UPDATE fpo_inh_parent FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-10-01' SET name = 'one^1'; -- All three rows should be in the child, with description preserved. SELECT tableoid::regclass, * FROM fpo_inh_parent ORDER BY valid_at; SELECT * FROM fpo_inh_child ORDER BY valid_at; -- No rows should have leaked into the parent. SELECT * FROM ONLY fpo_inh_parent ORDER BY valid_at; -- Same test for DELETE instead of UPDATE: TRUNCATE fpo_inh_child, fpo_inh_parent; INSERT INTO fpo_inh_child (id, valid_at, name, description) VALUES ('[1,2)', '[2018-01-01,2019-01-01)', 'one', 'initial'); DELETE FROM fpo_inh_parent FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-10-01'; -- Both rows should be in the child, with description preserved. SELECT tableoid::regclass, * FROM fpo_inh_parent ORDER BY valid_at; SELECT * FROM fpo_inh_child ORDER BY valid_at; -- No rows should have leaked into the parent. SELECT * FROM ONLY fpo_inh_parent ORDER BY valid_at; DROP TABLE fpo_inh_parent CASCADE; -- UPDATE FOR PORTION OF with multiple inheritance -- Leftover rows must stay in the child table, even if the range column's -- attnum differs between the target parent and child. CREATE TABLE temporal_parent ( id int, valid_at daterange, name text ); CREATE TABLE other_parent ( prefix text, note text ); CREATE TABLE mi_child () INHERITS (other_parent, temporal_parent); -- attnum of the range column is different in temporal_parent and mi_child SELECT attnum, attname FROM pg_attribute WHERE attrelid = 'temporal_parent'::regclass AND attnum > 0 AND NOT attisdropped ORDER BY attnum; SELECT attnum, attname FROM pg_attribute WHERE attrelid = 'mi_child'::regclass AND attnum > 0 AND NOT attisdropped ORDER BY attnum; INSERT INTO mi_child (prefix, note, id, valid_at, name) VALUES ('pfx', 'memo', 1, daterange('2000-01-01', '2010-01-01'), 'old'); UPDATE temporal_parent FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-01-01' SET name = 'new' WHERE id = 1; SELECT tableoid::regclass, * FROM temporal_parent ORDER BY valid_at; SELECT * FROM mi_child ORDER BY valid_at; SELECT * FROM ONLY temporal_parent ORDER BY valid_at; TRUNCATE mi_child, other_parent, temporal_parent; INSERT INTO mi_child (prefix, note, id, valid_at, name) VALUES ('pfx', 'memo', 1, daterange('2000-01-01', '2010-01-01'), 'old'); DELETE FROM temporal_parent FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-01-01' WHERE id = 1; SELECT tableoid::regclass, * FROM temporal_parent ORDER BY valid_at; SELECT * FROM mi_child ORDER BY valid_at; SELECT * FROM ONLY temporal_parent ORDER BY valid_at; DROP TABLE temporal_parent CASCADE; -- UPDATE FOR PORTION OF with generated 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; -- CURSORs CREATE TABLE fpo_cursed ( id int, valid_at int4range ); INSERT INTO fpo_cursed (id, valid_at) VALUES (1, '[10,100)'); -- UPDATE FOR PORTION OF is not permitted with a CURSOR: BEGIN; DECLARE fpo_cur CURSOR FOR SELECT * FROM fpo_cursed; FETCH NEXT FROM fpo_cur; UPDATE fpo_cursed FOR PORTION OF valid_at FROM 5 TO 6 SET id = 2 WHERE CURRENT OF fpo_cur; ROLLBACK; -- DELETE FOR PORTION OF is not permitted with a CURSOR: BEGIN; DECLARE fpo_cur CURSOR FOR SELECT * FROM fpo_cursed; FETCH NEXT FROM fpo_cur; DELETE FROM fpo_cursed FOR PORTION OF valid_at FROM 8 TO 9 WHERE CURRENT OF fpo_cur; ROLLBACK; SELECT * FROM fpo_cursed; DROP TABLE fpo_cursed; -- UPDATE/DELETE FOR PORTION OF leftover rows must satisfy RLS INSERT checks. CREATE ROLE regress_fpo_rls; CREATE TABLE fpo_rls ( id int, valid_at int4range ); ALTER TABLE fpo_rls ENABLE ROW LEVEL SECURITY; CREATE POLICY fpo_rls_select ON fpo_rls FOR SELECT TO regress_fpo_rls USING (true); CREATE POLICY fpo_rls_update ON fpo_rls FOR UPDATE TO regress_fpo_rls USING (lower(valid_at) < 50) WITH CHECK (lower(valid_at) < 50); CREATE POLICY fpo_rls_delete ON fpo_rls FOR DELETE TO regress_fpo_rls USING (lower(valid_at) < 50); CREATE POLICY fpo_rls_insert ON fpo_rls FOR INSERT TO regress_fpo_rls WITH CHECK (lower(valid_at) < 50); GRANT SELECT, UPDATE, DELETE ON fpo_rls TO regress_fpo_rls; INSERT INTO fpo_rls VALUES (1, '[10,100)'); SET ROLE regress_fpo_rls; UPDATE fpo_rls FOR PORTION OF valid_at FROM 30 TO 100 SET id = 2; RESET ROLE; SELECT * FROM fpo_rls ORDER BY valid_at; TRUNCATE fpo_rls; INSERT INTO fpo_rls VALUES (1, '[10,100)'); SET ROLE regress_fpo_rls; DELETE FROM fpo_rls FOR PORTION OF valid_at FROM 30 TO 100; RESET ROLE; SELECT * FROM fpo_rls ORDER BY valid_at; TRUNCATE fpo_rls; INSERT INTO fpo_rls VALUES (1, '[10,100)'); SET ROLE regress_fpo_rls; UPDATE fpo_rls FOR PORTION OF valid_at FROM 30 TO 70 SET id = 2; RESET ROLE; SELECT * FROM fpo_rls ORDER BY valid_at; TRUNCATE fpo_rls; INSERT INTO fpo_rls VALUES (1, '[10,100)'); SET ROLE regress_fpo_rls; DELETE FROM fpo_rls FOR PORTION OF valid_at FROM 30 TO 70; RESET ROLE; SELECT * FROM fpo_rls ORDER BY valid_at; DROP TABLE fpo_rls; DROP ROLE regress_fpo_rls; RESET datestyle;