Skip to content

Commit 91bb1ec

Browse files
ShirishaRaoashwinstar
authored andcommitted
Allow DELETE/UPDATE on tables with foreign partitions or inherited foreign tables
Currently, DELETE or UPDATE on a partitioned table with foreign partitions fail with an error as below, if the FDW does not support the operation: `ERROR: cannot delete from foreign table` This occurs because during executor initialization (ExecInitModifyTable), PostgreSQL scans all partitions of the target table and checks whether each one supports the requested operation. If any foreign partition's FDW lacks support for DELETE or UPDATE, the operation is rejected outright, even if that partition would not be affected by the query. A similar issue arises with inherited foreign tables, where DELETE/UPDATEs targeted on non-foreign tables are also blocked. As a result, DELETE/UPDATE operations are blocked even when they only target non-foreign relations. This means the system errors out without considering whether foreign or inherited foreign tables are actually involved in the operation. Even if no matching rows exist in a foreign partition, the operation still fails. This commit defers the FDW check for foreign partitions and inherited foreign tables from `ExecInitModifyTable` to `ExecDelete` and `ExecUpdate`. This change ensures that foreign child tables are checked only when they are actually targeted by the operation. However, if a DELETE or UPDATE is issued on the root table and it includes foreign child tables that do not support the operation, it will still result in an error. This is intentional because the responsibility for managing data in foreign tables lies with the user. Only after the user has removed relevant data from those foreign tables will such operations on the root table succeed. ** Mini repro for partition table with foreign partition: ** ``` CREATE EXTENSION file_fdw; CREATE SERVER file_server FOREIGN DATA WRAPPER file_fdw; CREATE TABLE pt (a int, b numeric) PARTITION BY RANGE(a); CREATE TABLE pt_part1 PARTITION OF pt FOR VALUES FROM (0) TO (10); INSERT INTO pt SELECT 5, 0.1; INSERT INTO pt SELECT 6, 0.2; CREATE FOREIGN TABLE ext (a int, b numeric) SERVER file_server OPTIONS (filename 'path-to-file', format 'csv', delimiter ','); ALTER TABLE pt ATTACH PARTITION ext FOR VALUES FROM (10) TO (20); postgres=# SELECT * FROM pt; a | b ----+----- 5 | 0.1 6 | 0.2 15 | 0.3 21 | 0.4 (4 rows) ``` ** Before Fix: ** ``` postgres=# DELETE FROM pt WHERE b = 0.2; ERROR: cannot delete from foreign table "ext" postgres=# DELETE FROM pt; ERROR: cannot delete from foreign table "ext" postgres=# UPDATE pt set b = 0.5 WHERE b = 0.1; ERROR: cannot update foreign table "ext" postgres=# UPDATE pt SET b = 0.5; ERROR: cannot update foreign table "ext" ``` ** After Fix: ** ``` postgres=# DELETE FROM pt WHERE b = 0.2; DELETE 1 postgres=# DELETE FROM pt; ERROR: cannot delete from foreign table "ext" postgres=# UPDATE pt SET b = 0.5 WHERE b = 0.1; UPDATE 1 postgres=# UPDATE pt SET b = 0.5; ERROR: cannot update foreign table "ext" ``` ** Mini repro for table with inherited foreign table: ** ``` CREATE TABLE pt (a text, b int); INSERT INTO pt VALUES ('AAA', 42); CREATE FOREIGN TABLE ft (a text, b int) server file_server OPTIONS (filename 'path-to-file', format 'csv', delimiter ','); ALTER FOREIGN TABLE ft INHERIT pt; SELECT * FROM pt; a | b -----+---- AAA | 42 BBB | 42 (2 rows) ``` ** Before Fix: ** ``` UPDATE pt SET b = b + 1000 WHERE a = 'AAA'; ERROR: cannot update foreign table "ft" DELETE FROM pt WHERE a = 'AAA'; ERROR: cannot delete from foreign table "ft" ``` ** After Fix: ** ``` UPDATE pt SET b = b + 1000 WHERE a = 'AAA'; UPDATE 1 DELETE FROM pt WHERE a = 'AAA'; DELETE 1 ``` Co-authored-by: Ashwin Agrawal <[email protected]>
1 parent 5092aae commit 91bb1ec

File tree

3 files changed

+199
-15
lines changed

3 files changed

+199
-15
lines changed

contrib/file_fdw/expected/file_fdw.out

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -334,14 +334,18 @@ SELECT * FROM agg_csv WHERE a < 0;
334334
RESET constraint_exclusion;
335335
-- table inheritance tests
336336
CREATE TABLE agg (a int2, b float4);
337+
INSERT INTO agg SELECT 5,0.1;
338+
INSERT INTO agg SELECT 6,0.2;
337339
ALTER FOREIGN TABLE agg_csv INHERIT agg;
338340
SELECT tableoid::regclass, * FROM agg;
339341
tableoid | a | b
340342
----------+-----+---------
343+
agg | 5 | 0.1
344+
agg | 6 | 0.2
341345
agg_csv | 100 | 99.097
342346
agg_csv | 0 | 0.09561
343347
agg_csv | 42 | 324.78
344-
(3 rows)
348+
(5 rows)
345349

346350
SELECT tableoid::regclass, * FROM agg_csv;
347351
tableoid | a | b
@@ -352,16 +356,51 @@ SELECT tableoid::regclass, * FROM agg_csv;
352356
(3 rows)
353357

354358
SELECT tableoid::regclass, * FROM ONLY agg;
355-
tableoid | a | b
356-
----------+---+---
357-
(0 rows)
359+
tableoid | a | b
360+
----------+---+-----
361+
agg | 5 | 0.1
362+
agg | 6 | 0.2
363+
(2 rows)
358364

359-
-- updates aren't supported
360-
UPDATE agg SET a = 1;
365+
-- updates on foreign tables are not supported
366+
UPDATE agg SET a = 10 WHERE b = 99.097::float4;
367+
ERROR: cannot update foreign table "agg_csv"
368+
UPDATE agg SET a = 10 WHERE a = 100;
369+
ERROR: cannot update foreign table "agg_csv"
370+
UPDATE agg SET a = 10;
361371
ERROR: cannot update foreign table "agg_csv"
372+
-- these updates should be allowed
373+
UPDATE agg SET a = 10 WHERE b = 0.1::float4;
374+
UPDATE agg SET a = 20 WHERE a = 10;
375+
SELECT tableoid::regclass, * FROM agg;
376+
tableoid | a | b
377+
----------+-----+---------
378+
agg | 6 | 0.2
379+
agg | 20 | 0.1
380+
agg_csv | 100 | 99.097
381+
agg_csv | 0 | 0.09561
382+
agg_csv | 42 | 324.78
383+
(5 rows)
384+
385+
-- deletes on foreign tables are not supported
386+
DELETE from agg WHERE b = 99.097::float4;
387+
ERROR: cannot delete from foreign table "agg_csv"
362388
DELETE FROM agg WHERE a = 100;
363389
ERROR: cannot delete from foreign table "agg_csv"
364-
-- but this should be allowed
390+
DELETE FROM agg;
391+
ERROR: cannot delete from foreign table "agg_csv"
392+
-- these deletes should be allowed
393+
DELETE FROM agg WHERE b = 0.1::float4;
394+
DELETE FROM agg WHERE a = 6;
395+
SELECT tableoid::regclass, * FROM agg;
396+
tableoid | a | b
397+
----------+-----+---------
398+
agg_csv | 100 | 99.097
399+
agg_csv | 0 | 0.09561
400+
agg_csv | 42 | 324.78
401+
(3 rows)
402+
403+
-- this should be allowed
365404
SELECT tableoid::regclass, * FROM agg FOR UPDATE;
366405
tableoid | a | b
367406
----------+-----+---------
@@ -540,15 +579,63 @@ ALTER FOREIGN TABLE agg_text OPTIONS (SET format 'text');
540579
ERROR: permission denied to set the "filename" option of a file_fdw foreign table
541580
DETAIL: Only roles with privileges of the "pg_read_server_files" role may set this option.
542581
SET ROLE regress_file_fdw_superuser;
582+
-- Test UPDATE/DELETE on partition table with foreign partitions
583+
CREATE TABLE pt_root (a int2, b float4) PARTITION BY range (a);
584+
CREATE TABLE pt_child PARTITION OF pt_root FOR VALUES FROM (0) TO (10);
585+
INSERT INTO pt_root SELECT 5, 0.1;
586+
INSERT INTO pt_root SELECT 6, 0.2;
587+
ALTER TABLE pt_root ATTACH PARTITION agg_csv FOR VALUES FROM (10) TO (20);
588+
SELECT * FROM pt_root;
589+
a | b
590+
-----+---------
591+
5 | 0.1
592+
6 | 0.2
593+
100 | 99.097
594+
0 | 0.09561
595+
42 | 324.78
596+
(5 rows)
597+
598+
-- delete on foreign tables are not supported
599+
DELETE FROM pt_root WHERE b = 99.097::float4;
600+
ERROR: cannot delete from foreign table "agg_csv"
601+
DELETE FROM pt_root;
602+
ERROR: cannot delete from foreign table "agg_csv"
603+
-- this delete should be allowed
604+
DELETE FROM pt_root WHERE b = 0.1::float4;
605+
SELECT * FROM pt_root;
606+
a | b
607+
-----+---------
608+
6 | 0.2
609+
100 | 99.097
610+
0 | 0.09561
611+
42 | 324.78
612+
(4 rows)
613+
614+
-- updates on foreign tables are not supported
615+
UPDATE pt_root SET b = 0.10 WHERE b = 99.097::float4;
616+
ERROR: cannot update foreign table "agg_csv"
617+
UPDATE pt_root SET b = 0.10;
618+
ERROR: cannot update foreign table "agg_csv"
619+
-- this update should be allowed
620+
UPDATE pt_root SET b = 0.6 WHERE b = 0.2::float4;
621+
SELECT * FROM pt_root;
622+
a | b
623+
-----+---------
624+
6 | 0.6
625+
100 | 99.097
626+
0 | 0.09561
627+
42 | 324.78
628+
(4 rows)
629+
630+
DROP TABLE pt_root;
543631
-- cleanup
544632
RESET ROLE;
545633
DROP EXTENSION file_fdw CASCADE;
546-
NOTICE: drop cascades to 9 other objects
634+
NOTICE: drop cascades to 8 other objects
547635
DETAIL: drop cascades to server file_server
548636
drop cascades to user mapping for regress_file_fdw_superuser on server file_server
549637
drop cascades to user mapping for regress_no_priv_user on server file_server
550638
drop cascades to foreign table agg_text
551-
drop cascades to foreign table agg_csv
552639
drop cascades to foreign table agg_bad
553640
drop cascades to foreign table header_match
554641
drop cascades to foreign table header_doesnt_match

contrib/file_fdw/sql/file_fdw.sql

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,14 +207,29 @@ RESET constraint_exclusion;
207207

208208
-- table inheritance tests
209209
CREATE TABLE agg (a int2, b float4);
210+
INSERT INTO agg SELECT 5,0.1;
211+
INSERT INTO agg SELECT 6,0.2;
210212
ALTER FOREIGN TABLE agg_csv INHERIT agg;
211213
SELECT tableoid::regclass, * FROM agg;
212214
SELECT tableoid::regclass, * FROM agg_csv;
213215
SELECT tableoid::regclass, * FROM ONLY agg;
214-
-- updates aren't supported
215-
UPDATE agg SET a = 1;
216+
-- updates on foreign tables are not supported
217+
UPDATE agg SET a = 10 WHERE b = 99.097::float4;
218+
UPDATE agg SET a = 10 WHERE a = 100;
219+
UPDATE agg SET a = 10;
220+
-- these updates should be allowed
221+
UPDATE agg SET a = 10 WHERE b = 0.1::float4;
222+
UPDATE agg SET a = 20 WHERE a = 10;
223+
SELECT tableoid::regclass, * FROM agg;
224+
-- deletes on foreign tables are not supported
225+
DELETE from agg WHERE b = 99.097::float4;
216226
DELETE FROM agg WHERE a = 100;
217-
-- but this should be allowed
227+
DELETE FROM agg;
228+
-- these deletes should be allowed
229+
DELETE FROM agg WHERE b = 0.1::float4;
230+
DELETE FROM agg WHERE a = 6;
231+
SELECT tableoid::regclass, * FROM agg;
232+
-- this should be allowed
218233
SELECT tableoid::regclass, * FROM agg FOR UPDATE;
219234
ALTER FOREIGN TABLE agg_csv NO INHERIT agg;
220235
DROP TABLE agg;
@@ -285,6 +300,27 @@ SET ROLE regress_file_fdw_user;
285300
ALTER FOREIGN TABLE agg_text OPTIONS (SET format 'text');
286301
SET ROLE regress_file_fdw_superuser;
287302

303+
-- Test UPDATE/DELETE on partition table with foreign partitions
304+
CREATE TABLE pt_root (a int2, b float4) PARTITION BY range (a);
305+
CREATE TABLE pt_child PARTITION OF pt_root FOR VALUES FROM (0) TO (10);
306+
INSERT INTO pt_root SELECT 5, 0.1;
307+
INSERT INTO pt_root SELECT 6, 0.2;
308+
ALTER TABLE pt_root ATTACH PARTITION agg_csv FOR VALUES FROM (10) TO (20);
309+
SELECT * FROM pt_root;
310+
-- delete on foreign tables are not supported
311+
DELETE FROM pt_root WHERE b = 99.097::float4;
312+
DELETE FROM pt_root;
313+
-- this delete should be allowed
314+
DELETE FROM pt_root WHERE b = 0.1::float4;
315+
SELECT * FROM pt_root;
316+
-- updates on foreign tables are not supported
317+
UPDATE pt_root SET b = 0.10 WHERE b = 99.097::float4;
318+
UPDATE pt_root SET b = 0.10;
319+
-- this update should be allowed
320+
UPDATE pt_root SET b = 0.6 WHERE b = 0.2::float4;
321+
SELECT * FROM pt_root;
322+
323+
DROP TABLE pt_root;
288324
-- cleanup
289325
RESET ROLE;
290326
DROP EXTENSION file_fdw CASCADE;

src/backend/executor/nodeModifyTable.c

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1585,10 +1585,34 @@ ExecDelete(ModifyTableContext *context,
15851585
TupleTableSlot *slot = NULL;
15861586
TM_Result result;
15871587
bool saveOld;
1588+
FdwRoutine *fdwroutine;
15881589

15891590
if (tupleDeleted)
15901591
*tupleDeleted = false;
15911592

1593+
/*
1594+
* For foreign partitions and inherited foreign tables, raise error
1595+
* during DELETE if the FDW does not support it. This check is deferred
1596+
* from ExecInitModifyTable to allow deletes on non-foreign tables to
1597+
* proceed without error.
1598+
*/
1599+
if (resultRelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
1600+
{
1601+
fdwroutine = resultRelInfo->ri_FdwRoutine;
1602+
if (fdwroutine->ExecForeignDelete == NULL)
1603+
ereport(ERROR,
1604+
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
1605+
errmsg("cannot delete from foreign table \"%s\"",
1606+
RelationGetRelationName(resultRelationDesc))));
1607+
1608+
if (fdwroutine->IsForeignRelUpdatable != NULL &&
1609+
(fdwroutine->IsForeignRelUpdatable(resultRelationDesc) & (1 << CMD_DELETE)) == 0)
1610+
ereport(ERROR,
1611+
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
1612+
errmsg("foreign table \"%s\" does not allow deletes",
1613+
RelationGetRelationName(resultRelationDesc))));
1614+
}
1615+
15921616
/*
15931617
* Prepare for the delete. This includes BEFORE ROW triggers, so we're
15941618
* done if it says we are.
@@ -2466,6 +2490,7 @@ ExecUpdate(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
24662490
Relation resultRelationDesc = resultRelInfo->ri_RelationDesc;
24672491
UpdateContext updateCxt = {0};
24682492
TM_Result result;
2493+
FdwRoutine *fdwroutine;
24692494

24702495
/*
24712496
* abort the operation if not running transactions
@@ -2480,6 +2505,29 @@ ExecUpdate(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
24802505
if (!ExecUpdatePrologue(context, resultRelInfo, tupleid, oldtuple, slot, NULL))
24812506
return NULL;
24822507

2508+
/*
2509+
* For foreign partitions and inherited foreign tables, raise error
2510+
* during UPDATE if the FDW does not support it. This check is deferred
2511+
* from ExecInitModifyTable to allow updates on non-foreign tables to
2512+
* proceed without error.
2513+
*/
2514+
if (resultRelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
2515+
{
2516+
fdwroutine = resultRelInfo->ri_FdwRoutine;
2517+
if (fdwroutine->ExecForeignUpdate == NULL)
2518+
ereport(ERROR,
2519+
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
2520+
errmsg("cannot update foreign table \"%s\"",
2521+
RelationGetRelationName(resultRelationDesc))));
2522+
2523+
if (fdwroutine->IsForeignRelUpdatable != NULL &&
2524+
(fdwroutine->IsForeignRelUpdatable(resultRelationDesc) & (1 << CMD_UPDATE)) == 0)
2525+
ereport(ERROR,
2526+
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
2527+
errmsg("foreign table \"%s\" does not allow updates",
2528+
RelationGetRelationName(resultRelationDesc))));
2529+
}
2530+
24832531
/* INSTEAD OF ROW UPDATE Triggers */
24842532
if (resultRelInfo->ri_TrigDesc &&
24852533
resultRelInfo->ri_TrigDesc->trig_update_instead_row)
@@ -4786,6 +4834,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
47864834
i = 0;
47874835
foreach(l, resultRelations)
47884836
{
4837+
bool skip_rel_check = false;
47894838
Index resultRelation = lfirst_int(l);
47904839
List *mergeActions = NIL;
47914840

@@ -4810,10 +4859,22 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
48104859
bms_is_member(i, node->fdwDirectModifyPlans);
48114860

48124861
/*
4813-
* Verify result relation is a valid target for the current operation
4862+
* Verify result relation is a valid target for the current operation.
4863+
* Skip this verification only when:
4864+
* - the relation is a foreign table,
4865+
* - the operation is DELETE or UPDATE, and
4866+
* - the query involves multiple result relations
4867+
*
4868+
* In such cases, the validation is deferred to ExecDelete or
4869+
* ExecUpdate, where the specific foreign partition is processed.
48144870
*/
4815-
CheckValidResultRel(resultRelInfo, operation, node->onConflictAction,
4816-
mergeActions);
4871+
rel = resultRelInfo->ri_RelationDesc;
4872+
skip_rel_check = (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE &&
4873+
(operation == CMD_DELETE || operation == CMD_UPDATE) &&
4874+
nrels > 1);
4875+
if (!skip_rel_check)
4876+
CheckValidResultRel(resultRelInfo, operation, node->onConflictAction,
4877+
mergeActions);
48174878

48184879
resultRelInfo++;
48194880
i++;

0 commit comments

Comments
 (0)