From 976a464f84cda534f9531c2d7ec2f68b3baca7a3 Mon Sep 17 00:00:00 2001 From: Ewan Young Date: Thu, 3 Sep 2026 17:55:42 +0800 Subject: [PATCH] Reject FOR PORTION OF on views with unqualified INSTEAD rules A view with an unqualified DO INSTEAD rule replaces the original query with the rule's action during rewriting, which discards the query's FOR PORTION OF clause. As a result an UPDATE/DELETE ... FOR PORTION OF through such a view silently modified or deleted the entire temporal row instead of just the requested portion, with no error. Commit dfce19c2300 added the analogous guard for views with INSTEAD OF triggers; do the same for unqualified INSTEAD rules, raising the same "do not support FOR PORTION OF" feature-not-supported error. A DO ALSO rule does not replace the query, so FOR PORTION OF continues to work on the automatically-updatable path. --- src/backend/rewrite/rewriteHandler.c | 11 ++++ src/test/regress/expected/updatable_views.out | 50 +++++++++++++++++++ src/test/regress/sql/updatable_views.sql | 41 +++++++++++++++ 3 files changed, 102 insertions(+) diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c index 36a724042c6..703c3321dc0 100644 --- a/src/backend/rewrite/rewriteHandler.c +++ b/src/backend/rewrite/rewriteHandler.c @@ -4377,6 +4377,17 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length, &returning, &qual_product); + /* + * An unqualified INSTEAD rule replaces the query with the rule + * action, dropping any FOR PORTION OF clause; reject it as we do for + * views with INSTEAD OF triggers. + */ + if (parsetree->forPortionOf && instead && + rt_entry_relation->rd_rel->relkind == RELKIND_VIEW) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("views with INSTEAD rules do not support FOR PORTION OF"))); + /* * If we have a VALUES RTE with any remaining untouched DEFAULT items, * and we got any product queries, finalize the VALUES RTE for each diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out index 5f9adf91029..47bc5b34124 100644 --- a/src/test/regress/expected/updatable_views.out +++ b/src/test/regress/expected/updatable_views.out @@ -4296,6 +4296,56 @@ delete from uv_fpo_instead_view ERROR: views with INSTEAD OF triggers do not support FOR PORTION OF drop view uv_fpo_instead_view; drop function uv_fpo_instead_trig(); +-- FOR PORTION OF is likewise not supported through an unqualified INSTEAD +-- rule, which would replace the query (dropping the FOR PORTION OF clause) +-- and so modify the whole temporal row instead of the requested portion. +create view uv_fpo_rule_view2 as select id, valid_at, b from uv_fpo_tab; +create rule uv_fpo_rule_upd as on update to uv_fpo_rule_view2 do instead + update uv_fpo_tab set b = new.b where id = old.id; +create rule uv_fpo_rule_del as on delete to uv_fpo_rule_view2 do instead + delete from uv_fpo_tab where id = old.id; +update uv_fpo_rule_view2 + for portion of valid_at from '2021-01-01' to '2022-01-01' + set b = 99 where id = '[1,1]'; -- error +ERROR: views with INSTEAD rules do not support FOR PORTION OF +delete from uv_fpo_rule_view2 + for portion of valid_at from '2021-01-01' to '2022-01-01' + where id = '[1,1]'; -- error +ERROR: views with INSTEAD rules do not support FOR PORTION OF +-- As for INSTEAD OF triggers, the check does not depend on which rows match. +update uv_fpo_rule_view2 + for portion of valid_at from '2021-01-01' to '2022-01-01' + set b = 99 where id = '[9,9]'; -- error, even with no matching rows +ERROR: views with INSTEAD rules do not support FOR PORTION OF +drop view uv_fpo_rule_view2 cascade; +-- A DO ALSO rule does not replace the query, so FOR PORTION OF still works. +create table uv_fpo_also_tab (id int4range, valid_at tsrange, b float, + constraint pk_uv_fpo_also_tab primary key (id, valid_at without overlaps)); +insert into uv_fpo_also_tab values ('[1,1]', '[2020-01-01, 2030-01-01)', 0); +create view uv_fpo_also_view as select id, valid_at, b from uv_fpo_also_tab; +create table uv_fpo_also_log (t text); +create rule uv_fpo_also as on update to uv_fpo_also_view do also + insert into uv_fpo_also_log values ('updated'); +update uv_fpo_also_view + for portion of valid_at from '2022-01-01' to '2023-01-01' + set b = 88 where id = '[1,1]'; -- ok: splits the row and runs the DO ALSO action +select id, valid_at, b from uv_fpo_also_tab order by valid_at; + id | valid_at | b +-------+---------------------------------------------------------+---- + [1,2) | ["Wed Jan 01 00:00:00 2020","Sat Jan 01 00:00:00 2022") | 0 + [1,2) | ["Sat Jan 01 00:00:00 2022","Sun Jan 01 00:00:00 2023") | 88 + [1,2) | ["Sun Jan 01 00:00:00 2023","Tue Jan 01 00:00:00 2030") | 0 +(3 rows) + +select count(*) from uv_fpo_also_log; + count +------- + 1 +(1 row) + +drop view uv_fpo_also_view cascade; +drop table uv_fpo_also_log; +drop table uv_fpo_also_tab; -- Forbid INSTEAD OF triggers with FOR PORTION OF even if the FOR PORTION OF -- statement is parsed before the trigger exists. -- This can happen in at least a couple ways: a rewrite rule or a BEGIN ATOMIC function. diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql index 4158bf26b74..ec8330b334c 100644 --- a/src/test/regress/sql/updatable_views.sql +++ b/src/test/regress/sql/updatable_views.sql @@ -2245,6 +2245,47 @@ delete from uv_fpo_instead_view drop view uv_fpo_instead_view; drop function uv_fpo_instead_trig(); +-- FOR PORTION OF is likewise not supported through an unqualified INSTEAD +-- rule, which would replace the query (dropping the FOR PORTION OF clause) +-- and so modify the whole temporal row instead of the requested portion. +create view uv_fpo_rule_view2 as select id, valid_at, b from uv_fpo_tab; +create rule uv_fpo_rule_upd as on update to uv_fpo_rule_view2 do instead + update uv_fpo_tab set b = new.b where id = old.id; +create rule uv_fpo_rule_del as on delete to uv_fpo_rule_view2 do instead + delete from uv_fpo_tab where id = old.id; + +update uv_fpo_rule_view2 + for portion of valid_at from '2021-01-01' to '2022-01-01' + set b = 99 where id = '[1,1]'; -- error + +delete from uv_fpo_rule_view2 + for portion of valid_at from '2021-01-01' to '2022-01-01' + where id = '[1,1]'; -- error + +-- As for INSTEAD OF triggers, the check does not depend on which rows match. +update uv_fpo_rule_view2 + for portion of valid_at from '2021-01-01' to '2022-01-01' + set b = 99 where id = '[9,9]'; -- error, even with no matching rows + +drop view uv_fpo_rule_view2 cascade; + +-- A DO ALSO rule does not replace the query, so FOR PORTION OF still works. +create table uv_fpo_also_tab (id int4range, valid_at tsrange, b float, + constraint pk_uv_fpo_also_tab primary key (id, valid_at without overlaps)); +insert into uv_fpo_also_tab values ('[1,1]', '[2020-01-01, 2030-01-01)', 0); +create view uv_fpo_also_view as select id, valid_at, b from uv_fpo_also_tab; +create table uv_fpo_also_log (t text); +create rule uv_fpo_also as on update to uv_fpo_also_view do also + insert into uv_fpo_also_log values ('updated'); +update uv_fpo_also_view + for portion of valid_at from '2022-01-01' to '2023-01-01' + set b = 88 where id = '[1,1]'; -- ok: splits the row and runs the DO ALSO action +select id, valid_at, b from uv_fpo_also_tab order by valid_at; +select count(*) from uv_fpo_also_log; +drop view uv_fpo_also_view cascade; +drop table uv_fpo_also_log; +drop table uv_fpo_also_tab; + -- Forbid INSTEAD OF triggers with FOR PORTION OF even if the FOR PORTION OF -- statement is parsed before the trigger exists. -- This can happen in at least a couple ways: a rewrite rule or a BEGIN ATOMIC function. -- 2.47.3