diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index dfd7b33a..c0250e65 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -475,6 +475,38 @@ ExecInitGenerated(ResultRelInfo *resultRelInfo, else updatedCols = NULL; + /* + * For UPDATE ... FOR PORTION OF, the range column is also being modified + * (narrowed via intersection), but it is not included in updatedCols + * because the user does not need UPDATE permission on it. We must + * account for it here so that generated columns referencing the range + * column are recomputed. + */ + if (updatedCols) + { + ForPortionOfState *fpoState = resultRelInfo->ri_forPortionOf; + + if (fpoState == NULL && resultRelInfo->ri_RootResultRelInfo) + fpoState = resultRelInfo->ri_RootResultRelInfo->ri_forPortionOf; + if (fpoState != NULL) + { + int rangeAttno = fpoState->fp_rangeAttno; + + /* Map from root attno to child attno if needed */ + if (resultRelInfo->ri_RootResultRelInfo) + { + TupleConversionMap *map = ExecGetRootToChildMap(resultRelInfo, + estate); + + if (map) + rangeAttno = map->attrMap->attnums[rangeAttno - 1]; + } + + updatedCols = bms_add_member(bms_copy(updatedCols), + rangeAttno - FirstLowInvalidHeapAttributeNumber); + } + } + /* * Make sure these data structures are built in the per-query memory * context so they'll survive throughout the query. diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out index 31f772c7..a9917b4a 100644 --- a/src/test/regress/expected/for_portion_of.out +++ b/src/test/regress/expected/for_portion_of.out @@ -2097,4 +2097,62 @@ SELECT * FROM temporal_partitioned_5 ORDER BY id, valid_at; (4 rows) DROP TABLE temporal_partitioned; +-- 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_stored ( + id int, + valid_at int4range, + range_len int GENERATED ALWAYS AS (upper(valid_at) - lower(valid_at)) STORED +); +INSERT INTO fpo_generated_stored (id, valid_at) VALUES + (1, '[10,100)'); +SELECT * FROM fpo_generated_stored ORDER BY valid_at; + id | valid_at | range_len +----+----------+----------- + 1 | [10,100) | 90 +(1 row) + +-- After FPO, all three rows (leftover-before, updated, leftover-after) +-- must have correct range_len values. +UPDATE fpo_generated_stored + FOR PORTION OF valid_at FROM 30 TO 70 + SET id = 2; +SELECT * FROM fpo_generated_stored ORDER BY valid_at; + id | valid_at | range_len +----+----------+----------- + 1 | [10,30) | 20 + 2 | [30,70) | 40 + 1 | [70,100) | 30 +(3 rows) + +-- Also test with a generated column that references both a SET column +-- and the range column. +TRUNCATE fpo_generated_stored; +DROP TABLE fpo_generated_stored; +CREATE TABLE fpo_generated_stored ( + id int, + valid_at int4range, + id_plus_len int GENERATED ALWAYS AS (id + upper(valid_at) - lower(valid_at)) STORED +); +INSERT INTO fpo_generated_stored (id, valid_at) VALUES + (1, '[10,100)'); +SELECT * FROM fpo_generated_stored ORDER BY valid_at; + id | valid_at | id_plus_len +----+----------+------------- + 1 | [10,100) | 91 +(1 row) + +UPDATE fpo_generated_stored + FOR PORTION OF valid_at FROM 30 TO 70 + SET id = 2; +SELECT * FROM fpo_generated_stored ORDER BY valid_at; + id | valid_at | id_plus_len +----+----------+------------- + 1 | [10,30) | 21 + 2 | [30,70) | 42 + 1 | [70,100) | 31 +(3 rows) + +DROP TABLE fpo_generated_stored; RESET datestyle; diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql index d4062acf..f1e3937c 100644 --- a/src/test/regress/sql/for_portion_of.sql +++ b/src/test/regress/sql/for_portion_of.sql @@ -1365,4 +1365,44 @@ SELECT * FROM temporal_partitioned_5 ORDER BY id, valid_at; DROP TABLE temporal_partitioned; +-- 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_stored ( + id int, + valid_at int4range, + range_len int GENERATED ALWAYS AS (upper(valid_at) - lower(valid_at)) STORED +); +INSERT INTO fpo_generated_stored (id, valid_at) VALUES + (1, '[10,100)'); +SELECT * FROM fpo_generated_stored ORDER BY valid_at; + +-- After FPO, all three rows (leftover-before, updated, leftover-after) +-- must have correct range_len values. +UPDATE fpo_generated_stored + FOR PORTION OF valid_at FROM 30 TO 70 + SET id = 2; +SELECT * FROM fpo_generated_stored ORDER BY valid_at; + +-- Also test with a generated column that references both a SET column +-- and the range column. +TRUNCATE fpo_generated_stored; +DROP TABLE fpo_generated_stored; +CREATE TABLE fpo_generated_stored ( + id int, + valid_at int4range, + id_plus_len int GENERATED ALWAYS AS (id + upper(valid_at) - lower(valid_at)) STORED +); +INSERT INTO fpo_generated_stored (id, valid_at) VALUES + (1, '[10,100)'); +SELECT * FROM fpo_generated_stored ORDER BY valid_at; + +UPDATE fpo_generated_stored + FOR PORTION OF valid_at FROM 30 TO 70 + SET id = 2; +SELECT * FROM fpo_generated_stored ORDER BY valid_at; + +DROP TABLE fpo_generated_stored; + RESET datestyle;