Hi Hackers, Here is a fix for finding D11 from [0].
When FOR PORTION OF through a view violates WITH CHECK OPTION *both* by the updated row and by the inserted temporal leftover, we show an error complaining about the leftover. It would be clearer to show an error about the top-level row. This commit moves the check a little earlier (but still after constraints & uniqueness violations), when FOR PORTION OF is used. I think this patch would be simpler if we unconditionally shuffled the order in ExecUpdateEpilogue to be (1) ExecWithCheckOptions, (2) ExecForPortionOfLeftovers, (3) ExecARUpdateTriggers. I couldn't find any problems there. But it felt too drastic so close to release. If you'd like that patch instead, I'll send a revision. [0] https://www.postgresql.org/message-id/CA%2BrenyV6QLOJYmLo3gbsg1Y%2BCrho8NqME1jJXgPbO_NgxfBaKQ%40mail.gmail.com Yours, -- Paul ~{:-) [email protected]
From ff47a5356ee9490a36216f33c55c44bb6507d828 Mon Sep 17 00:00:00 2001 From: "Paul A. Jungwirth" <[email protected]> Date: Thu, 3 Sep 2026 12:54:34 -0700 Subject: [PATCH v1] Report the user's row, not a leftover, for FOR PORTION OF check options If UPDATE FOR PORTION OF violates a view's CHECK OPTION *both* from a leftover and from the top-level update, we show a message complaining about the leftover. It would be clearer to complain about the top-level update. This commit does the check before inserting leftovers, and then skips it in ExecUpdateEpilogue. Reported-by: Noah Misch <[email protected]> Author: Paul A. Jungwirth <[email protected]> Backpatch-through: 19 --- src/backend/executor/nodeModifyTable.c | 16 ++++++- src/test/regress/expected/for_portion_of.out | 46 ++++++++++++++++++++ src/test/regress/sql/for_portion_of.sql | 40 +++++++++++++++++ 3 files changed, 101 insertions(+), 1 deletion(-) diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index 5681505d31c..bfe2259acd0 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -2629,7 +2629,18 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt, /* Compute temporal leftovers in FOR PORTION OF */ if (((ModifyTable *) context->mtstate->ps.plan)->forPortionOf) + { + /* + * Enforce WITH CHECK OPTION on views early. If an update violates the + * condition *and* a temporal leftover violates it, we want to complain + * about the top-level update. + */ + if (resultRelInfo->ri_WithCheckOptions != NIL) + ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo, + slot, context->estate); + ExecForPortionOfLeftovers(context, context->estate, resultRelInfo, tupleid); + } /* AFTER ROW UPDATE Triggers */ ExecARUpdateTriggers(context->estate, resultRelInfo, @@ -2651,8 +2662,11 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt, * * ExecWithCheckOptions() will skip any WCOs which are not of the kind we * are looking for at this point. + * + * With FOR PORTION OF, we already did this just above. */ - if (resultRelInfo->ri_WithCheckOptions != NIL) + if (resultRelInfo->ri_WithCheckOptions != NIL && + !((ModifyTable *) context->mtstate->ps.plan)->forPortionOf) ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo, slot, context->estate); } diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out index 64789d1777b..73e8fa166b0 100644 --- a/src/test/regress/expected/for_portion_of.out +++ b/src/test/regress/expected/for_portion_of.out @@ -2793,4 +2793,50 @@ SELECT * FROM fpo_rls ORDER BY valid_at; DROP TABLE fpo_rls; DROP ROLE regress_fpo_rls; +-- +-- WITH CHECK OPTION on a view +-- +-- The row the statement produced is checked first, so a violation names the +-- row the user asked for and not an untouched leftover. +-- +CREATE TABLE fpo_wco ( + id int, + valid_at daterange, + name text +); +INSERT INTO fpo_wco VALUES + (1, daterange('2000-01-01', '2010-01-01'), 'keepme'); +CREATE VIEW fpo_wco_v AS + SELECT * FROM fpo_wco WHERE valid_at @> '2005-01-01'::date + WITH CHECK OPTION; +-- Both the updated row and the leftovers fall outside the view. The updated +-- row is the one the user wrote, so that is the one to report. +UPDATE fpo_wco_v FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' + SET name = 'changed'; +ERROR: new row violates check option for view "fpo_wco_v" +DETAIL: Failing row contains (1, [2002-01-01,2003-01-01), changed). +-- Here the updated row still satisfies the view and only a leftover does not, +-- so the leftover is correctly the one reported. +UPDATE fpo_wco_v FOR PORTION OF valid_at FROM '2004-01-01' TO '2006-01-01' + SET name = 'changed'; +ERROR: new row violates check option for view "fpo_wco_v" +DETAIL: Failing row contains (1, [2000-01-01,2004-01-01), keepme). +-- Nothing was written by either statement. +SELECT * FROM fpo_wco ORDER BY valid_at; + id | valid_at | name +----+-------------------------+-------- + 1 | [2000-01-01,2010-01-01) | keepme +(1 row) + +-- A portion covering the whole row leaves no leftovers and is accepted. +UPDATE fpo_wco_v FOR PORTION OF valid_at FROM '2000-01-01' TO '2010-01-01' + SET name = 'changed'; +SELECT * FROM fpo_wco ORDER BY valid_at; + id | valid_at | name +----+-------------------------+--------- + 1 | [2000-01-01,2010-01-01) | changed +(1 row) + +DROP VIEW fpo_wco_v; +DROP TABLE fpo_wco; RESET datestyle; diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql index b61fe10478e..8c9f903c224 100644 --- a/src/test/regress/sql/for_portion_of.sql +++ b/src/test/regress/sql/for_portion_of.sql @@ -1849,4 +1849,44 @@ SELECT * FROM fpo_rls ORDER BY valid_at; DROP TABLE fpo_rls; DROP ROLE regress_fpo_rls; +-- +-- WITH CHECK OPTION on a view +-- +-- The row the statement produced is checked first, so a violation names the +-- row the user asked for and not an untouched leftover. +-- + +CREATE TABLE fpo_wco ( + id int, + valid_at daterange, + name text +); +INSERT INTO fpo_wco VALUES + (1, daterange('2000-01-01', '2010-01-01'), 'keepme'); + +CREATE VIEW fpo_wco_v AS + SELECT * FROM fpo_wco WHERE valid_at @> '2005-01-01'::date + WITH CHECK OPTION; + +-- Both the updated row and the leftovers fall outside the view. The updated +-- row is the one the user wrote, so that is the one to report. +UPDATE fpo_wco_v FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' + SET name = 'changed'; + +-- Here the updated row still satisfies the view and only a leftover does not, +-- so the leftover is correctly the one reported. +UPDATE fpo_wco_v FOR PORTION OF valid_at FROM '2004-01-01' TO '2006-01-01' + SET name = 'changed'; + +-- Nothing was written by either statement. +SELECT * FROM fpo_wco ORDER BY valid_at; + +-- A portion covering the whole row leaves no leftovers and is accepted. +UPDATE fpo_wco_v FOR PORTION OF valid_at FROM '2000-01-01' TO '2010-01-01' + SET name = 'changed'; +SELECT * FROM fpo_wco ORDER BY valid_at; + +DROP VIEW fpo_wco_v; +DROP TABLE fpo_wco; + RESET datestyle; -- 2.47.3
