1
0
mirror of https://github.com/postgres/postgres.git synced 2026-10-23 13:29:09 +03:00

Fix assertion failures in DELETE FOR PORTION OF tuple routing

We must handle cases where an inserted temporal leftover gets routed to
a different partition than the original tuple's. This happens if the
partition key depends on the application-time column used in the FOR
PORTION OF clause. Everything works for UPDATE, but for DELETE we can
hit some Asserts, since that operation never needed to be covered
before. One Assert is for the RETURNING list; the other, for WITH CHECK
OPTION. This commit updates both to include CMD_DELETE.

Author: Paul A Jungwirth <pj@illuminatedcomputing.com>
Reported-by: Peter Eisentraut <peter@eisentraut.org>
Discussion: https://postgr.es/m/03276924-6d55-470d-8192-a49c053c940f@eisentraut.org
This commit is contained in:
Peter Eisentraut
2026-09-01 20:45:13 +02:00
parent 61e64cb19a
commit 938f4f8f34
5 changed files with 195 additions and 6 deletions

View File

@@ -591,7 +591,8 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
/*
* Verify result relation is a valid target for an INSERT. An UPDATE of a
* partition-key becomes a DELETE+INSERT operation, so this check is still
* required when the operation is CMD_UPDATE.
* required when the operation is CMD_UPDATE. It is also required for
* CMD_DELETE, because DELETE ... FOR PORTION OF inserts leftover rows.
*/
CheckValidResultRel(leaf_part_rri, CMD_INSERT,
node ? node->onConflictAction : ONCONFLICT_NONE, NIL, node);
@@ -612,8 +613,9 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
* Build WITH CHECK OPTION constraints for the partition. Note that we
* didn't build the withCheckOptionList for partitions within the planner,
* but simple translation of varattnos will suffice. This only occurs for
* the INSERT case or in the case of UPDATE/MERGE tuple routing where we
* didn't find a result rel to reuse.
* the INSERT case or in the case of UPDATE/DELETE/MERGE tuple routing
* where we didn't find a result rel to reuse. We reach here with DELETE
* only when inserting temporal leftovers.
*/
if (node && node->withCheckOptionLists != NIL)
{
@@ -624,7 +626,8 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
/*
* In the case of INSERT on a partitioned table, there is only one
* plan. Likewise, there is only one WCO list, not one per partition.
* For UPDATE/MERGE, there are as many WCO lists as there are plans.
* For UPDATE/DELETE/MERGE, there are as many WCO lists as there are
* plans.
*/
Assert((node->operation == CMD_INSERT &&
list_length(node->withCheckOptionLists) == 1 &&
@@ -632,6 +635,9 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
(node->operation == CMD_UPDATE &&
list_length(node->withCheckOptionLists) ==
list_length(node->resultRelations)) ||
(node->operation == CMD_DELETE &&
list_length(node->withCheckOptionLists) ==
list_length(node->resultRelations)) ||
(node->operation == CMD_MERGE &&
list_length(node->withCheckOptionLists) ==
list_length(node->resultRelations)));
@@ -679,8 +685,9 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
* Build the RETURNING projection for the partition. Note that we didn't
* build the returningList for partitions within the planner, but simple
* translation of varattnos will suffice. This only occurs for the INSERT
* case or in the case of UPDATE/MERGE tuple routing where we didn't find
* a result rel to reuse.
* case or in the case of UPDATE/DELETE/MERGE tuple routing where we
* didn't find a result rel to reuse. We reach here with DELETE only when
* inserting temporal leftovers.
*/
if (node && node->returningLists != NIL)
{
@@ -695,6 +702,9 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
(node->operation == CMD_UPDATE &&
list_length(node->returningLists) ==
list_length(node->resultRelations)) ||
(node->operation == CMD_DELETE &&
list_length(node->returningLists) ==
list_length(node->resultRelations)) ||
(node->operation == CMD_MERGE &&
list_length(node->returningLists) ==
list_length(node->resultRelations)));

View File

@@ -2218,6 +2218,65 @@ SELECT * FROM temporal_partitioned_5 ORDER BY id, valid_at;
DROP FUNCTION fpo_returning_row;
DROP TABLE temporal_partitioned;
-- Test FOR PORTION OF when the partition key depends on the range column.
-- Then a leftover can belong to a partition that is not one of the plan's
-- result relations, and we must build a new ResultRelInfo for it.
CREATE TABLE temporal_partitioned_by_valid_at (
id int4range,
valid_at daterange,
name text
) PARTITION BY RANGE (lower(valid_at));
CREATE TABLE temporal_partitioned_early
PARTITION OF temporal_partitioned_by_valid_at
FOR VALUES FROM (MINVALUE) TO ('2000-06-01');
CREATE TABLE temporal_partitioned_late
PARTITION OF temporal_partitioned_by_valid_at
FOR VALUES FROM ('2000-06-01') TO (MAXVALUE);
INSERT INTO temporal_partitioned_by_valid_at (id, valid_at, name) VALUES
('[1,2)', daterange('2000-01-01', '2010-01-01'), 'one');
-- The WHERE clause prunes away the late partition, but the second leftover
-- belongs there. The leftovers must not appear in the RETURNING output.
DELETE FROM temporal_partitioned_by_valid_at
FOR PORTION OF valid_at FROM '2000-03-01' TO '2000-07-01'
WHERE lower(valid_at) < '2000-06-01'
RETURNING id, valid_at, name;
id | valid_at | name
-------+-------------------------+------
[1,2) | [2000-01-01,2010-01-01) | one
(1 row)
SELECT tableoid::regclass, * FROM temporal_partitioned_by_valid_at
ORDER BY id, valid_at;
tableoid | id | valid_at | name
----------------------------+-------+-------------------------+------
temporal_partitioned_early | [1,2) | [2000-01-01,2000-03-01) | one
temporal_partitioned_late | [1,2) | [2000-07-01,2010-01-01) | one
(2 rows)
-- The same thing for UPDATE
DELETE FROM temporal_partitioned_by_valid_at;
INSERT INTO temporal_partitioned_by_valid_at (id, valid_at, name) VALUES
('[1,2)', daterange('2000-01-01', '2010-01-01'), 'one');
UPDATE temporal_partitioned_by_valid_at
FOR PORTION OF valid_at FROM '2000-03-01' TO '2000-07-01'
SET name = 'one^1'
WHERE lower(valid_at) < '2000-06-01'
RETURNING id, valid_at, name;
id | valid_at | name
-------+-------------------------+-------
[1,2) | [2000-03-01,2000-07-01) | one^1
(1 row)
SELECT tableoid::regclass, * FROM temporal_partitioned_by_valid_at
ORDER BY id, valid_at;
tableoid | id | valid_at | name
----------------------------+-------+-------------------------+-------
temporal_partitioned_early | [1,2) | [2000-01-01,2000-03-01) | one
temporal_partitioned_early | [1,2) | [2000-03-01,2000-07-01) | one^1
temporal_partitioned_late | [1,2) | [2000-07-01,2010-01-01) | one
(3 rows)
DROP TABLE temporal_partitioned_by_valid_at;
-- UPDATE/DELETE FOR PORTION OF with RULEs
CREATE TABLE fpo_rule (f1 bigint, f2 int4range);
INSERT INTO fpo_rule VALUES (1, '[1, 11)');

View File

@@ -3824,6 +3824,51 @@ select * from uv_fpo_wco_tab order by valid_at;
drop view uv_fpo_wco_view;
drop view uv_fpo_nowco_view;
drop table uv_fpo_wco_tab;
-- On a partitioned table a leftover can be routed to a partition that is not
-- one of the plan's result relations. WITH CHECK OPTION must be enforced
-- there too.
create table uv_fpo_wco_part (id int4range, valid_at daterange, b int)
partition by range (lower(valid_at));
create table uv_fpo_wco_part1 partition of uv_fpo_wco_part
for values from (minvalue) to ('2000-06-01');
create table uv_fpo_wco_part2 partition of uv_fpo_wco_part
for values from ('2000-06-01') to (maxvalue);
insert into uv_fpo_wco_part values ('[1,1]', '[2000-01-01,2004-01-01)', 0);
-- This view accepts both leftovers. The where clause prunes away part2, but
-- the second leftover is routed there anyway:
create view uv_fpo_wco_part_view as
select * from uv_fpo_wco_part where b = 0 with check option;
delete from uv_fpo_wco_part_view
for portion of valid_at from '2000-03-01' to '2000-07-01'
where lower(valid_at) < '2000-06-01';
select tableoid::regclass, * from uv_fpo_wco_part order by valid_at;
tableoid | id | valid_at | b
------------------+-------+-------------------------+---
uv_fpo_wco_part1 | [1,2) | [01-01-2000,03-01-2000) | 0
uv_fpo_wco_part2 | [1,2) | [07-01-2000,01-01-2004) | 0
(2 rows)
-- This view rejects the leftover that lands in part2:
delete from uv_fpo_wco_part;
insert into uv_fpo_wco_part values ('[1,1]', '[2000-01-01,2004-01-01)', 0);
create view uv_fpo_wco_part_view2 as
select * from uv_fpo_wco_part where lower(valid_at) < '2000-02-01'
with check option;
delete from uv_fpo_wco_part_view2
for portion of valid_at from '2000-03-01' to '2000-07-01'
where lower(valid_at) < '2000-06-01';
ERROR: new row violates check option for view "uv_fpo_wco_part_view2"
DETAIL: Failing row contains ([1,2), [07-01-2000,01-01-2004), 0).
-- The base table is unchanged:
select tableoid::regclass, * from uv_fpo_wco_part order by valid_at;
tableoid | id | valid_at | b
------------------+-------+-------------------------+---
uv_fpo_wco_part1 | [1,2) | [01-01-2000,01-01-2004) | 0
(1 row)
drop view uv_fpo_wco_part_view;
drop view uv_fpo_wco_part_view2;
drop table uv_fpo_wco_part;
-- Test whole-row references to the view
create table uv_iocu_tab (a int unique, b text);
create view uv_iocu_view as

View File

@@ -1472,6 +1472,48 @@ SELECT * FROM temporal_partitioned_5 ORDER BY id, valid_at;
DROP FUNCTION fpo_returning_row;
DROP TABLE temporal_partitioned;
-- Test FOR PORTION OF when the partition key depends on the range column.
-- Then a leftover can belong to a partition that is not one of the plan's
-- result relations, and we must build a new ResultRelInfo for it.
CREATE TABLE temporal_partitioned_by_valid_at (
id int4range,
valid_at daterange,
name text
) PARTITION BY RANGE (lower(valid_at));
CREATE TABLE temporal_partitioned_early
PARTITION OF temporal_partitioned_by_valid_at
FOR VALUES FROM (MINVALUE) TO ('2000-06-01');
CREATE TABLE temporal_partitioned_late
PARTITION OF temporal_partitioned_by_valid_at
FOR VALUES FROM ('2000-06-01') TO (MAXVALUE);
INSERT INTO temporal_partitioned_by_valid_at (id, valid_at, name) VALUES
('[1,2)', daterange('2000-01-01', '2010-01-01'), 'one');
-- The WHERE clause prunes away the late partition, but the second leftover
-- belongs there. The leftovers must not appear in the RETURNING output.
DELETE FROM temporal_partitioned_by_valid_at
FOR PORTION OF valid_at FROM '2000-03-01' TO '2000-07-01'
WHERE lower(valid_at) < '2000-06-01'
RETURNING id, valid_at, name;
SELECT tableoid::regclass, * FROM temporal_partitioned_by_valid_at
ORDER BY id, valid_at;
-- The same thing for UPDATE
DELETE FROM temporal_partitioned_by_valid_at;
INSERT INTO temporal_partitioned_by_valid_at (id, valid_at, name) VALUES
('[1,2)', daterange('2000-01-01', '2010-01-01'), 'one');
UPDATE temporal_partitioned_by_valid_at
FOR PORTION OF valid_at FROM '2000-03-01' TO '2000-07-01'
SET name = 'one^1'
WHERE lower(valid_at) < '2000-06-01'
RETURNING id, valid_at, name;
SELECT tableoid::regclass, * FROM temporal_partitioned_by_valid_at
ORDER BY id, valid_at;
DROP TABLE temporal_partitioned_by_valid_at;
-- UPDATE/DELETE FOR PORTION OF with RULEs
CREATE TABLE fpo_rule (f1 bigint, f2 int4range);
INSERT INTO fpo_rule VALUES (1, '[1, 11)');

View File

@@ -1943,6 +1943,39 @@ drop view uv_fpo_wco_view;
drop view uv_fpo_nowco_view;
drop table uv_fpo_wco_tab;
-- On a partitioned table a leftover can be routed to a partition that is not
-- one of the plan's result relations. WITH CHECK OPTION must be enforced
-- there too.
create table uv_fpo_wco_part (id int4range, valid_at daterange, b int)
partition by range (lower(valid_at));
create table uv_fpo_wco_part1 partition of uv_fpo_wco_part
for values from (minvalue) to ('2000-06-01');
create table uv_fpo_wco_part2 partition of uv_fpo_wco_part
for values from ('2000-06-01') to (maxvalue);
insert into uv_fpo_wco_part values ('[1,1]', '[2000-01-01,2004-01-01)', 0);
-- This view accepts both leftovers. The where clause prunes away part2, but
-- the second leftover is routed there anyway:
create view uv_fpo_wco_part_view as
select * from uv_fpo_wco_part where b = 0 with check option;
delete from uv_fpo_wco_part_view
for portion of valid_at from '2000-03-01' to '2000-07-01'
where lower(valid_at) < '2000-06-01';
select tableoid::regclass, * from uv_fpo_wco_part order by valid_at;
-- This view rejects the leftover that lands in part2:
delete from uv_fpo_wco_part;
insert into uv_fpo_wco_part values ('[1,1]', '[2000-01-01,2004-01-01)', 0);
create view uv_fpo_wco_part_view2 as
select * from uv_fpo_wco_part where lower(valid_at) < '2000-02-01'
with check option;
delete from uv_fpo_wco_part_view2
for portion of valid_at from '2000-03-01' to '2000-07-01'
where lower(valid_at) < '2000-06-01';
-- The base table is unchanged:
select tableoid::regclass, * from uv_fpo_wco_part order by valid_at;
drop view uv_fpo_wco_part_view;
drop view uv_fpo_wco_part_view2;
drop table uv_fpo_wco_part;
-- Test whole-row references to the view
create table uv_iocu_tab (a int unique, b text);
create view uv_iocu_view as