From 6f87310552574465fb34d58b5c0d4ad59902e5c6 Mon Sep 17 00:00:00 2001 From: jian he Date: Fri, 10 Apr 2026 17:01:12 +0800 Subject: [PATCH v7] Fix some problems with UPDATE FOR PORTION OF - Fixed inserting leftovers with traditional table inheritance. Since there is no tuple routing, we must add them directly to the child table. Also this preserves extra columns in that table. - Added ExecInitForPortionOf. This sets up executor state for child partitions. Previously we did this in ExecForPortionOfLeftovers, but doing it earlier lets us use the child->parent attr mapping in the fixes below. - Made sure GENERATED STORED columns that depend on the application-time column get updated. We exclude that column from the updatedCols bitmapset, because it does not require permissions. But then we must remember to add it later. This also fixes a similar problem with UPDATE OF triggers. - Clarified a comment about the rangetype stored in ForPortionOfState. Discussion: https://postgr.es/m/CAHg+QDcd=t69gLf9yQexO07EJ2mx0Z70NFHo6h94X1EDA=hM0g@mail.gmail.com Discussion: https://postgr.es/m/CAHg+QDcsXsUVaZ+JwM02yDRQEi=cL_rTH_ROLDYgOx004sQu7A@mail.gmail.com --- src/backend/executor/execUtils.c | 34 ++- src/backend/executor/nodeModifyTable.c | 145 ++++++++---- src/include/nodes/execnodes.h | 3 +- src/test/regress/expected/for_portion_of.out | 221 +++++++++++++++---- src/test/regress/sql/for_portion_of.sql | 94 +++++++- 5 files changed, 398 insertions(+), 99 deletions(-) diff --git a/src/backend/executor/execUtils.c b/src/backend/executor/execUtils.c index 1eb6b9f1f40..363830f0158 100644 --- a/src/backend/executor/execUtils.c +++ b/src/backend/executor/execUtils.c @@ -1408,6 +1408,7 @@ Bitmapset * ExecGetUpdatedCols(ResultRelInfo *relinfo, EState *estate) { RTEPermissionInfo *perminfo = GetResultRTEPermissionInfo(relinfo, estate); + Bitmapset *updatedCols = perminfo->updatedCols; if (perminfo == NULL) return NULL; @@ -1418,10 +1419,39 @@ ExecGetUpdatedCols(ResultRelInfo *relinfo, EState *estate) TupleConversionMap *map = ExecGetRootToChildMap(relinfo, estate); if (map) - return execute_attr_map_cols(map->attrMap, perminfo->updatedCols); + updatedCols = execute_attr_map_cols(map->attrMap, updatedCols); } - return perminfo->updatedCols; + /* + * For UPDATE ... FOR PORTION OF, the range column is being modified + * (narrowed via intersection), but it is not included in updatedCols + * because the user does not need UPDATE permission on it. Now manualy + * add it to updatedCols. Since ri_forPortionOf->fp_rangeAttno is already + * mapped for the child partition, we have to add it after the mapping just + * above. Also that makes it unsafe to mutate perminfo. XXX: Always add the + * unmapped attno instead (before mapping), and mutate perminfo, to avoid + * repeated allocations? + */ + if (relinfo->ri_forPortionOf) + { + AttrNumber rangeAttno = relinfo->ri_forPortionOf->fp_rangeAttno; + + if (!bms_is_member(rangeAttno - FirstLowInvalidHeapAttributeNumber, + updatedCols)) + { + MemoryContext oldContext; + + oldContext = MemoryContextSwitchTo(estate->es_query_cxt); + + updatedCols = + bms_add_member(updatedCols, + rangeAttno - FirstLowInvalidHeapAttributeNumber); + + MemoryContextSwitchTo(oldContext); + } + } + + return updatedCols; } /* Return a bitmap representing generated columns being updated */ diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index 4cb057ca4f9..81f5afc9fb7 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -198,6 +198,8 @@ static TupleTableSlot *ExecMergeNotMatched(ModifyTableContext *context, static void ExecSetupTransitionCaptureState(ModifyTableState *mtstate, EState *estate); static void fireBSTriggers(ModifyTableState *node); static void fireASTriggers(ModifyTableState *node); +static void ExecInitForPortionOf(ModifyTableState *mtstate, EState *estate, + ResultRelInfo *resultRelInfo); /* @@ -1409,7 +1411,6 @@ ExecForPortionOfLeftovers(ModifyTableContext *context, ModifyTableState *mtstate = context->mtstate; ModifyTable *node = (ModifyTable *) mtstate->ps.plan; ForPortionOfExpr *forPortionOf = (ForPortionOfExpr *) node->forPortionOf; - AttrNumber rangeAttno; Datum oldRange; TypeCacheEntry *typcache; ForPortionOfState *fpoState; @@ -1424,37 +1425,10 @@ ExecForPortionOfLeftovers(ModifyTableContext *context, ReturnSetInfo rsi; bool didInit = false; bool shouldFree = false; + ResultRelInfo *rootRelInfo = mtstate->rootResultRelInfo; LOCAL_FCINFO(fcinfo, 2); - if (!resultRelInfo->ri_forPortionOf) - { - /* - * If we don't have a ForPortionOfState yet, we must be a partition - * child being hit for the first time. Make a copy from the root, with - * our own TupleTableSlot. We do this lazily so that we don't pay the - * price of unused partitions. - */ - ForPortionOfState *leafState = makeNode(ForPortionOfState); - - if (!mtstate->rootResultRelInfo) - elog(ERROR, "no root relation but ri_forPortionOf is uninitialized"); - - fpoState = mtstate->rootResultRelInfo->ri_forPortionOf; - Assert(fpoState); - - leafState->fp_rangeName = fpoState->fp_rangeName; - leafState->fp_rangeType = fpoState->fp_rangeType; - leafState->fp_rangeAttno = fpoState->fp_rangeAttno; - leafState->fp_targetRange = fpoState->fp_targetRange; - leafState->fp_Leftover = fpoState->fp_Leftover; - /* Each partition needs a slot matching its tuple descriptor */ - leafState->fp_Existing = - table_slot_create(resultRelInfo->ri_RelationDesc, - &mtstate->ps.state->es_tupleTable); - - resultRelInfo->ri_forPortionOf = leafState; - } fpoState = resultRelInfo->ri_forPortionOf; oldtupleSlot = fpoState->fp_Existing; leftoverSlot = fpoState->fp_Leftover; @@ -1475,21 +1449,13 @@ ExecForPortionOfLeftovers(ModifyTableContext *context, if (!table_tuple_fetch_row_version(resultRelInfo->ri_RelationDesc, tupleid, SnapshotAny, oldtupleSlot)) elog(ERROR, "failed to fetch tuple for FOR PORTION OF"); - /* - * Get the old range of the record being updated/deleted. Must read with - * the attno of the leaf partition being updated. - */ - - rangeAttno = forPortionOf->rangeVar->varattno; - if (resultRelInfo->ri_RootResultRelInfo) - map = ExecGetChildToRootMap(resultRelInfo); - if (map != NULL) - rangeAttno = map->attrMap->attnums[rangeAttno - 1]; slot_getallattrs(oldtupleSlot); - if (oldtupleSlot->tts_isnull[rangeAttno - 1]) + /* Get the old range of the record being updated/deleted. */ + + if (oldtupleSlot->tts_isnull[fpoState->fp_rangeAttno - 1]) elog(ERROR, "found a NULL range in a temporal table"); - oldRange = oldtupleSlot->tts_values[rangeAttno - 1]; + oldRange = oldtupleSlot->tts_values[fpoState->fp_rangeAttno - 1]; /* * Get the range's type cache entry. This is worth caching for the whole @@ -1527,12 +1493,20 @@ ExecForPortionOfLeftovers(ModifyTableContext *context, fcinfo->args[1].isnull = false; /* - * If there are partitions, we must insert into the root table, so we get - * tuple routing. We already set up leftoverSlot with the root tuple - * descriptor. + * For partitioned tables, we must read leftovers with the tuple descriptor + * of the child table, but insert into the root table to enable tuple + * routing. So leftoverSlot is configured with the root's tuple + * descriptor. However, for traditional table inheritance, we don't need + * tuple routing and just insert directly into the child table to preserve + * child-specific columns. In that case, leftoverSlot uses the child's + * (resultRelInfo) tuple descriptor. */ - if (resultRelInfo->ri_RootResultRelInfo) + if (rootRelInfo && + rootRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + { + map = ExecGetChildToRootMap(resultRelInfo); resultRelInfo = resultRelInfo->ri_RootResultRelInfo; + } /* * Insert a leftover for each value returned by the without_portion helper @@ -1601,8 +1575,9 @@ ExecForPortionOfLeftovers(ModifyTableContext *context, didInit = true; } - leftoverSlot->tts_values[forPortionOf->rangeVar->varattno - 1] = leftover; - leftoverSlot->tts_isnull[forPortionOf->rangeVar->varattno - 1] = false; + leftoverSlot->tts_values[resultRelInfo->ri_forPortionOf->fp_rangeAttno - 1] = leftover; + leftoverSlot->tts_isnull[resultRelInfo->ri_forPortionOf->fp_rangeAttno - 1] = false; + ExecMaterializeSlot(leftoverSlot); /* @@ -4777,6 +4752,18 @@ ExecModifyTable(PlanState *pstate) false, true); } + /* + * If we don't have a ForPortionOfState yet, we must be a partition + * child being hit for the first time. Make a copy from the root, with + * our own TupleTableSlot. We do this lazily so that we don't pay the + * price of unused partitions. + */ + if ((((ModifyTable *) context.mtstate->ps.plan)->forPortionOf) && + !resultRelInfo->ri_forPortionOf) + { + ExecInitForPortionOf(context.mtstate, estate, resultRelInfo); + } + /* * If resultRelInfo->ri_usesFdwDirectModify is true, all we need to do * here is compute the RETURNING expressions. @@ -5860,3 +5847,67 @@ ExecReScanModifyTable(ModifyTableState *node) */ elog(ERROR, "ExecReScanModifyTable is not implemented"); } + +/* ---------------------------------------------------------------- + * ExecInitForPortionOf + * + * Initializes resultRelInfo->ri_forPortionOf for child tables. + * ---------------------------------------------------------------- + */ +static void +ExecInitForPortionOf(ModifyTableState *mtstate, EState *estate, ResultRelInfo *resultRelInfo) +{ + MemoryContext oldcxt; + ForPortionOfState *leafState; + ResultRelInfo *rootRelInfo = mtstate->rootResultRelInfo; + ForPortionOfState *fpoState; + + if (!rootRelInfo) + elog(ERROR, "no root relation but ri_forPortionOf is uninitialized"); + + fpoState = mtstate->rootResultRelInfo->ri_forPortionOf; + + /* Things built here have to last for the query duration. */ + oldcxt = MemoryContextSwitchTo(estate->es_query_cxt); + + leafState = makeNode(ForPortionOfState); + + leafState->fp_rangeName = fpoState->fp_rangeName; + leafState->fp_rangeType = fpoState->fp_rangeType; + leafState->fp_targetRange = fpoState->fp_targetRange; + + /* + * For partitioned tables we must read the leftovers using the child table's + * tuple descriptor, but then insert them into the root table (using its + * tuple descriptor) so we get tuple routing. + * + * For traditional table inheritance, we read and insert directly into this + * resultRelInfo; no tuple routing to the parent is required. + */ + if (rootRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + { + TupleConversionMap *map = ExecGetChildToRootMap(resultRelInfo); + if (map) + leafState->fp_rangeAttno = map->attrMap->attnums[fpoState->fp_rangeAttno - 1]; + else + leafState->fp_rangeAttno = fpoState->fp_rangeAttno; + leafState->fp_Leftover = fpoState->fp_Leftover; + } + else + { + leafState->fp_rangeAttno = fpoState->fp_rangeAttno; + leafState->fp_Leftover = + ExecInitExtraTupleSlot(mtstate->ps.state, + RelationGetDescr(resultRelInfo->ri_RelationDesc), + &TTSOpsVirtual); + } + + /* Each partition needs a slot matching its tuple descriptor */ + leafState->fp_Existing = + table_slot_create(resultRelInfo->ri_RelationDesc, + &mtstate->ps.state->es_tupleTable); + + resultRelInfo->ri_forPortionOf = leafState; + + MemoryContextSwitchTo(oldcxt); +} diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 13359180d25..53c138310db 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -477,7 +477,8 @@ typedef struct ForPortionOfState NodeTag type; char *fp_rangeName; /* the column named in FOR PORTION OF */ - Oid fp_rangeType; /* the type of the FOR PORTION OF expression */ + Oid fp_rangeType; /* the base type (not domain) of the FOR + * PORTION OF expression */ int fp_rangeAttno; /* the attno of the range column */ Datum fp_targetRange; /* the range/multirange from FOR PORTION OF */ TypeCacheEntry *fp_leftoverstypcache; /* type cache entry of the range */ diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out index 31f772c723d..2edcf9aa7b0 100644 --- a/src/test/regress/expected/for_portion_of.out +++ b/src/test/regress/expected/for_portion_of.out @@ -1365,6 +1365,9 @@ $$; 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); @@ -1378,6 +1381,9 @@ CREATE TRIGGER fpo_after_delete_stmt 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); @@ -1394,9 +1400,15 @@ UPDATE for_portion_of_test NOTICE: fpo_before_stmt: BEFORE UPDATE STATEMENT: NOTICE: old: NOTICE: new: +NOTICE: fpo_before_stmt1: BEFORE UPDATE STATEMENT: +NOTICE: old: +NOTICE: new: NOTICE: fpo_before_row: BEFORE UPDATE ROW: NOTICE: old: [2019-01-01,2030-01-01) NOTICE: new: [2021-01-01,2022-01-01) +NOTICE: fpo_before_row1: BEFORE UPDATE ROW: +NOTICE: old: [2019-01-01,2030-01-01) +NOTICE: new: [2021-01-01,2022-01-01) NOTICE: fpo_before_stmt: BEFORE INSERT STATEMENT: NOTICE: old: NOTICE: new: @@ -1986,6 +1998,7 @@ 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 GENERATED STORED 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 @@ -1993,29 +2006,34 @@ 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; +ALTER TABLE temporal_partitioned_3 DROP COLUMN id, DROP COLUMN valid_at CASCADE; +NOTICE: drop cascades to column range_len of table temporal_partitioned_3 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; +ALTER TABLE temporal_partitioned_5 DROP COLUMN id, DROP COLUMN valid_at CASCADE; +NOTICE: drop cascades to column range_len of table temporal_partitioned_5 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; - id | valid_at | name --------+-------------------------+------- - [1,2) | [2000-01-01,2010-01-01) | one - [3,4) | [2000-01-01,2010-01-01) | three - [5,6) | [2000-01-01,2010-01-01) | five + id | valid_at | name | range_len +-------+-------------------------+-------+----------- + [1,2) | [2000-01-01,2010-01-01) | one | 3653 + [3,4) | [2000-01-01,2010-01-01) | three | 3653 + [5,6) | [2000-01-01,2010-01-01) | five | 3653 (3 rows) -- Update without moving within partition 1 @@ -2046,55 +2064,166 @@ UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-0 id = '[3,4)' WHERE id = '[5,6)'; -- Update all partitions at once (each with leftovers) -SELECT * FROM temporal_partitioned ORDER BY id, valid_at; - id | valid_at | name --------+-------------------------+--------- - [1,2) | [2000-01-01,2000-03-01) | one - [1,2) | [2000-03-01,2000-04-01) | one^1 - [1,2) | [2000-04-01,2000-06-01) | one - [1,2) | [2000-07-01,2010-01-01) | one - [2,3) | [2000-06-01,2000-07-01) | three^2 - [3,4) | [2000-01-01,2000-03-01) | three - [3,4) | [2000-03-01,2000-04-01) | three^1 - [3,4) | [2000-04-01,2000-06-01) | three - [3,4) | [2000-06-01,2000-07-01) | five^2 - [3,4) | [2000-07-01,2010-01-01) | three - [4,5) | [2000-06-01,2000-07-01) | one^2 - [5,6) | [2000-01-01,2000-03-01) | five - [5,6) | [2000-03-01,2000-04-01) | five^1 - [5,6) | [2000-04-01,2000-06-01) | five - [5,6) | [2000-07-01,2010-01-01) | five +SELECT *, upper(valid_at) - lower(valid_at) FROM temporal_partitioned ORDER BY id, valid_at; + id | valid_at | name | range_len | ?column? +-------+-------------------------+---------+-----------+---------- + [1,2) | [2000-01-01,2000-03-01) | one | 60 | 60 + [1,2) | [2000-03-01,2000-04-01) | one^1 | 31 | 31 + [1,2) | [2000-04-01,2000-06-01) | one | 61 | 61 + [1,2) | [2000-07-01,2010-01-01) | one | 3471 | 3471 + [2,3) | [2000-06-01,2000-07-01) | three^2 | 30 | 30 + [3,4) | [2000-01-01,2000-03-01) | three | 60 | 60 + [3,4) | [2000-03-01,2000-04-01) | three^1 | 31 | 31 + [3,4) | [2000-04-01,2000-06-01) | three | 61 | 61 + [3,4) | [2000-06-01,2000-07-01) | five^2 | 30 | 30 + [3,4) | [2000-07-01,2010-01-01) | three | 3471 | 3471 + [4,5) | [2000-06-01,2000-07-01) | one^2 | 30 | 30 + [5,6) | [2000-01-01,2000-03-01) | five | 60 | 60 + [5,6) | [2000-03-01,2000-04-01) | five^1 | 31 | 31 + [5,6) | [2000-04-01,2000-06-01) | five | 61 | 61 + [5,6) | [2000-07-01,2010-01-01) | five | 3471 | 3471 (15 rows) SELECT * FROM temporal_partitioned_1 ORDER BY id, valid_at; - id | valid_at | name --------+-------------------------+--------- - [1,2) | [2000-01-01,2000-03-01) | one - [1,2) | [2000-03-01,2000-04-01) | one^1 - [1,2) | [2000-04-01,2000-06-01) | one - [1,2) | [2000-07-01,2010-01-01) | one - [2,3) | [2000-06-01,2000-07-01) | three^2 + id | valid_at | name | range_len +-------+-------------------------+---------+----------- + [1,2) | [2000-01-01,2000-03-01) | one | 60 + [1,2) | [2000-03-01,2000-04-01) | one^1 | 31 + [1,2) | [2000-04-01,2000-06-01) | one | 61 + [1,2) | [2000-07-01,2010-01-01) | one | 3471 + [2,3) | [2000-06-01,2000-07-01) | three^2 | 30 (5 rows) SELECT * FROM temporal_partitioned_3 ORDER BY id, valid_at; - name | id | valid_at ----------+-------+------------------------- - three | [3,4) | [2000-01-01,2000-03-01) - three^1 | [3,4) | [2000-03-01,2000-04-01) - three | [3,4) | [2000-04-01,2000-06-01) - five^2 | [3,4) | [2000-06-01,2000-07-01) - three | [3,4) | [2000-07-01,2010-01-01) - one^2 | [4,5) | [2000-06-01,2000-07-01) + name | id | valid_at | range_len +---------+-------+-------------------------+----------- + three | [3,4) | [2000-01-01,2000-03-01) | 60 + three^1 | [3,4) | [2000-03-01,2000-04-01) | 31 + three | [3,4) | [2000-04-01,2000-06-01) | 61 + five^2 | [3,4) | [2000-06-01,2000-07-01) | 30 + three | [3,4) | [2000-07-01,2010-01-01) | 3471 + one^2 | [4,5) | [2000-06-01,2000-07-01) | 30 (6 rows) SELECT * FROM temporal_partitioned_5 ORDER BY id, valid_at; - name | valid_at | id ---------+-------------------------+------- - five | [2000-01-01,2000-03-01) | [5,6) - five^1 | [2000-03-01,2000-04-01) | [5,6) - five | [2000-04-01,2000-06-01) | [5,6) - five | [2000-07-01,2010-01-01) | [5,6) + name | valid_at | id | range_len +--------+-------------------------+-------+----------- + five | [2000-01-01,2000-03-01) | [5,6) | 60 + five^1 | [2000-03-01,2000-04-01) | [5,6) | 31 + five | [2000-04-01,2000-06-01) | [5,6) | 61 + five | [2000-07-01,2010-01-01) | [5,6) | 3471 (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 ( + 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; + id | valid_at | range_len | range_lenv +----+----------+-----------+------------ + 1 | [10,100) | 90 | 90 +(1 row) + +CREATE TRIGGER fpo_before_row1 + BEFORE UPDATE OF valid_at ON fpo_generated + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); +CREATE TRIGGER fpo_before_row2 + BEFORE UPDATE OF valid_at ON fpo_generated + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); +-- 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. +-- Triggers fpo_before_row1 and fpo_before_row2 should also be fired. +UPDATE fpo_generated + FOR PORTION OF valid_at FROM 30 TO 70 + SET id = 2; +NOTICE: fpo_before_row2: BEFORE UPDATE STATEMENT: +NOTICE: old: +NOTICE: new: +NOTICE: fpo_before_row1: BEFORE UPDATE ROW: +NOTICE: old: [10,100) +NOTICE: new: [30,70) +SELECT * FROM fpo_generated ORDER BY valid_at; + id | valid_at | range_len | range_lenv +----+----------+-----------+------------ + 1 | [10,30) | 20 | 20 + 2 | [30,70) | 40 | 40 + 1 | [70,100) | 30 | 30 +(3 rows) + +-- 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; + id | valid_at | id_plus_len | id_plus_lenv +----+----------+-------------+-------------- + 1 | [10,100) | 91 | 91 +(1 row) + +UPDATE fpo_generated + FOR PORTION OF valid_at FROM 30 TO 70 + SET id = 2; +SELECT * FROM fpo_generated ORDER BY valid_at; + id | valid_at | id_plus_len | id_plus_lenv +----+----------+-------------+-------------- + 1 | [10,30) | 21 | 21 + 2 | [30,70) | 42 | 42 + 1 | [70,100) | 31 | 31 +(3 rows) + +DROP TABLE fpo_generated; +-- UPDATE 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); +INSERT INTO fpo_inh_child (id, valid_at, name, description) VALUES + ('[1,2)', '[2018-01-01,2019-01-01)', 'one', 'initial'); +-- Update targets the parent; the matching row lives in the child. +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; + tableoid | id | valid_at | name +---------------+-------+-------------------------+------- + fpo_inh_child | [1,2) | [2018-01-01,2018-04-01) | one + fpo_inh_child | [1,2) | [2018-04-01,2018-10-01) | one^1 + fpo_inh_child | [1,2) | [2018-10-01,2019-01-01) | one +(3 rows) + +SELECT * FROM fpo_inh_child ORDER BY valid_at; + id | valid_at | name | description +-------+-------------------------+-------+------------- + [1,2) | [2018-01-01,2018-04-01) | one | initial + [1,2) | [2018-04-01,2018-10-01) | one^1 | initial + [1,2) | [2018-10-01,2019-01-01) | one | initial +(3 rows) + +-- No rows should have leaked into the parent. +SELECT * FROM ONLY fpo_inh_parent ORDER BY valid_at; + id | valid_at | name +----+----------+------ +(0 rows) + +DROP TABLE fpo_inh_parent CASCADE; +NOTICE: drop cascades to table fpo_inh_child RESET datestyle; diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql index d4062acf1d1..8e00a1863a2 100644 --- a/src/test/regress/sql/for_portion_of.sql +++ b/src/test/regress/sql/for_portion_of.sql @@ -913,6 +913,10 @@ 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); @@ -931,6 +935,10 @@ 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); @@ -1292,6 +1300,7 @@ DROP TABLE for_portion_of_test2; DROP TYPE mydaterange; -- Test FOR PORTION OF against a partitioned table. +-- Include a GENERATED STORED 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 @@ -1300,6 +1309,7 @@ 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)'); @@ -1307,13 +1317,15 @@ CREATE TABLE temporal_partitioned_3 PARTITION OF temporal_partitioned FOR VALUES 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; +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; +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 @@ -1358,11 +1370,87 @@ UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-0 -- Update all partitions at once (each with leftovers) -SELECT * FROM temporal_partitioned ORDER BY id, valid_at; +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 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 ( + 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; + +CREATE TRIGGER fpo_before_row1 + BEFORE UPDATE OF valid_at ON fpo_generated + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); + +CREATE TRIGGER fpo_before_row2 + BEFORE UPDATE OF valid_at ON fpo_generated + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); + +-- 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. +-- Triggers fpo_before_row1 and fpo_before_row2 should also be fired. +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; + + +-- UPDATE 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); +INSERT INTO fpo_inh_child (id, valid_at, name, description) VALUES + ('[1,2)', '[2018-01-01,2019-01-01)', 'one', 'initial'); + +-- Update targets the parent; the matching row lives in the child. +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; + +DROP TABLE fpo_inh_parent CASCADE; + RESET datestyle; -- 2.47.3