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;
